diff --git a/benchmarks/cuquantum_benchmarks/__init__.py b/benchmarks/cuquantum_benchmarks/__init__.py index 336ad30..1f9e1ee 100644 --- a/benchmarks/cuquantum_benchmarks/__init__.py +++ b/benchmarks/cuquantum_benchmarks/__init__.py @@ -2,4 +2,4 @@ # # SPDX-License-Identifier: BSD-3-Clause -__version__ = '0.3.0' +__version__ = '0.3.1' diff --git a/benchmarks/cuquantum_benchmarks/_utils.py b/benchmarks/cuquantum_benchmarks/_utils.py index 76ecbde..5797be6 100644 --- a/benchmarks/cuquantum_benchmarks/_utils.py +++ b/benchmarks/cuquantum_benchmarks/_utils.py @@ -229,7 +229,7 @@ def get_cpu_name(): if m: return m.group(0).split(':')[-1].strip() else: - assert False, f"getting cpu info failed" + return f"unknown" def get_gpu_driver_version(): diff --git a/benchmarks/cuquantum_benchmarks/backends/__init__.py b/benchmarks/cuquantum_benchmarks/backends/__init__.py index 0cb2cf2..e3de1ef 100644 --- a/benchmarks/cuquantum_benchmarks/backends/__init__.py +++ b/benchmarks/cuquantum_benchmarks/backends/__init__.py @@ -4,15 +4,10 @@ from .backend_cirq import Cirq from .backend_cutn import cuTensorNet -from .backend_pny import (Pny, PnyLightningGpu, PnyLightningCpu, - PnyLightningKokkos, PnyDumper) +from .backend_pny import Pny, PnyLightningGpu, PnyLightningCpu, PnyLightningKokkos from .backend_qsim import Qsim, QsimCuda, QsimCusv, QsimMgpu from .backend_qiskit import Aer, AerCuda, AerCusv, CusvAer from .backend_qulacs import QulacsGpu, QulacsCpu -try: - from .backend_naive import Naive -except ImportError: - Naive = None backends = { @@ -30,12 +25,9 @@ 'pennylane-lightning-gpu': PnyLightningGpu, 'pennylane-lightning-qubit': PnyLightningCpu, 'pennylane-lightning-kokkos': PnyLightningKokkos, - 'pennylane-dumper': PnyDumper, 'qulacs-cpu': QulacsCpu, 'qulacs-gpu': QulacsGpu, } -if Naive: - backends['naive'] = Naive def createBackend(backend_name, ngpus, ncpu_threads, precision, *args, **kwargs): diff --git a/benchmarks/cuquantum_benchmarks/backends/backend_cirq.py b/benchmarks/cuquantum_benchmarks/backends/backend_cirq.py index 7e1b087..429b64f 100644 --- a/benchmarks/cuquantum_benchmarks/backends/backend_cirq.py +++ b/benchmarks/cuquantum_benchmarks/backends/backend_cirq.py @@ -2,19 +2,24 @@ # # SPDX-License-Identifier: BSD-3-Clause +import functools import warnings try: import cirq except ImportError: cirq = None - + +try: + from .. import _internal_utils +except ImportError: + _internal_utils = None from .backend import Backend -class Cirq(Backend): +class _Cirq(Backend): - def __init__(self, ngpus, ncpu_threads, precision, *args, **kwargs): + def __init__(self, ngpus, ncpu_threads, precision, *args, identifier=None, **kwargs): if cirq is None: raise RuntimeError("cirq is not installed") if ngpus > 0: @@ -25,7 +30,15 @@ def __init__(self, ngpus, ncpu_threads, precision, *args, **kwargs): raise ValueError("the cirq backend only supports single precision") self.backend = cirq.Simulator() - + self.identifier = identifier + self.version = cirq.__version__ + + def preprocess_circuit(self, circuit, *args, **kwargs): + if _internal_utils is not None: + _internal_utils.preprocess_circuit(self.identifier, circuit, *args, **kwargs) + + return {} + def run(self, circuit, nshots=1024): run_data = {} if nshots > 0: @@ -34,3 +47,6 @@ def run(self, circuit, nshots=1024): results = self.backend.simulate(circuit) post_res = results.measurements['result'] return {'results': results, 'post_results': post_res, 'run_data': run_data} + + +Cirq = functools.partial(_Cirq, identifier='cirq') diff --git a/benchmarks/cuquantum_benchmarks/backends/backend_cutn.py b/benchmarks/cuquantum_benchmarks/backends/backend_cutn.py index ba58fb7..b7f5925 100644 --- a/benchmarks/cuquantum_benchmarks/backends/backend_cutn.py +++ b/benchmarks/cuquantum_benchmarks/backends/backend_cutn.py @@ -51,6 +51,7 @@ def __init__(self, ngpus, ncpu_threads, precision, **kwargs): opts = cutn.NetworkOptions(handle=self.handle) self.network_opts = opts self.n_samples = kwargs.pop('nhypersamples') + self.version = cutn.get_version() def __del__(self): cutn.destroy(self.handle) diff --git a/benchmarks/cuquantum_benchmarks/backends/backend_pny.py b/benchmarks/cuquantum_benchmarks/backends/backend_pny.py index 31f3920..a94b7c5 100644 --- a/benchmarks/cuquantum_benchmarks/backends/backend_pny.py +++ b/benchmarks/cuquantum_benchmarks/backends/backend_pny.py @@ -7,7 +7,6 @@ import os import time import warnings -import sys import numpy as np try: @@ -15,8 +14,11 @@ except ImportError: pennylane = None +try: + from .. import _internal_utils +except ImportError: + _internal_utils = None from .backend import Backend -from .._utils import call_by_root, EarlyReturnError, is_running_mpi # set up a logger @@ -35,16 +37,36 @@ def __init__(self, ngpus, ncpu_threads, precision, *args, identifier=None, **kwa self.ncpu_threads = ncpu_threads self.nqubits = kwargs.pop('nqubits') self.circuit = None + self.version = self.find_version(identifier) - def _make_qnode(self, circuit, nshots=1024, **kwargs): - if self.identifier == "pennylane-lightning-gpu": + def find_version(self, identifier): + if identifier == "pennylane-lightning-gpu": if self.ngpus == 1: try: import pennylane_lightning_gpu except ImportError as e: raise RuntimeError("PennyLane-Lightning-GPU plugin is not installed") from e else: - raise ValueError(f"cannot specify --ngpus > 1 for the backend {self.identifier}") + raise ValueError(f"cannot specify --ngpus > 1 for the backend {identifier}") + ver = pennylane_lightning_gpu.__version__ + elif identifier == "pennylane-lightning-kokkos": + try: + import pennylane_lightning_kokkos + except ImportError as e: + raise RuntimeError("PennyLane-Lightning-Kokkos plugin is not installed") from e + ver = pennylane_lightning_kokkos.__version__ + elif identifier == "pennylane-lightning-qubit": + try: + from pennylane_lightning import lightning_qubit + except ImportError as e: + raise RuntimeError("PennyLane-Lightning plugin is not installed") from e + ver = lightning_qubit.__version__ + else: # identifier == "pennylane" + ver = pennylane.__version__ + return ver + + def _make_qnode(self, circuit, nshots=1024, **kwargs): + if self.identifier == "pennylane-lightning-gpu": dev = pennylane.device("lightning.gpu", wires=self.nqubits, shots=nshots, c_dtype=self.dtype) elif self.identifier == "pennylane-lightning-kokkos": # there's no way for us to query what execution space (=backend) that kokkos supports at runtime, @@ -67,10 +89,6 @@ def _make_qnode(self, circuit, nshots=1024, **kwargs): sync=False, kokkos_args=args) elif self.identifier == "pennylane-lightning-qubit": - try: - import pennylane_lightning - except ImportError as e: - raise RuntimeError("PennyLane-Lightning plugin is not installed") from e if self.ngpus != 0: raise ValueError(f"cannot specify --ngpus for the backend {self.identifier}") if self.ncpu_threads > 1 and self.ncpu_threads != int(os.environ.get("OMP_NUM_THREADS", "-1")): @@ -81,23 +99,6 @@ def _make_qnode(self, circuit, nshots=1024, **kwargs): if self.ngpus != 0: raise ValueError(f"cannot specify --ngpus for the backend {self.identifier}") dev = pennylane.device("default.qubit", wires=self.nqubits, shots=nshots, c_dtype=self.dtype) - elif self.identifier == "pennylane-dumper": - import cloudpickle - import cuquantum_benchmarks - cloudpickle.register_pickle_by_value(cuquantum_benchmarks) - - # note: before loading the pickle, one should check if the Python version agrees - # (probably pennylane's version too) - py_major_minor = f'{sys.version_info.major}.{sys.version_info.minor}' - circuit_filename = kwargs.pop('circuit_filename') - circuit_filename += f"_pny_raw_py{py_major_minor}.pickle" - def dump(): - logger.info(f"dumping pennylane (raw) circuit as {circuit_filename} ...") - with open(circuit_filename, 'wb') as f: - cloudpickle.dump(circuit, f) # use highest protocol - logger.info("early exiting as the dumper task is completed") - call_by_root(dump) - raise EarlyReturnError else: raise ValueError(f"the backend {self.identifier} is not recognized") @@ -105,6 +106,9 @@ def dump(): return qnode def preprocess_circuit(self, circuit, *args, **kwargs): + if _internal_utils is not None: + _internal_utils.preprocess_circuit(self.identifier, circuit, *args, **kwargs) + nshots = kwargs.get('nshots', 1024) t1 = time.perf_counter() self.circuit = self._make_qnode(circuit, nshots, **kwargs) @@ -125,4 +129,3 @@ def run(self, circuit, nshots=1024): PnyLightningCpu = functools.partial(Pennylane, identifier='pennylane-lightning-qubit') PnyLightningKokkos = functools.partial(Pennylane, identifier='pennylane-lightning-kokkos') Pny = functools.partial(Pennylane, identifier='pennylane') -PnyDumper = functools.partial(Pennylane, identifier='pennylane-dumper') diff --git a/benchmarks/cuquantum_benchmarks/backends/backend_qiskit.py b/benchmarks/cuquantum_benchmarks/backends/backend_qiskit.py index 2b1bde5..245e63d 100644 --- a/benchmarks/cuquantum_benchmarks/backends/backend_qiskit.py +++ b/benchmarks/cuquantum_benchmarks/backends/backend_qiskit.py @@ -15,8 +15,13 @@ except ImportError: qiskit = None +try: + from .. import _internal_utils +except ImportError: + _internal_utils = None from .backend import Backend from .._utils import get_mpi_size, get_mpi_rank +from .._utils import call_by_root, EarlyReturnError # set up a logger @@ -32,9 +37,18 @@ def __init__(self, ngpus, ncpu_threads, precision, *args, identifier=None, **kwa self.precision = precision self.identifier = identifier self.nqubits = kwargs.pop('nqubits') - self.backend = self.create_aer_backend(identifier, ngpus, ncpu_threads, *args, **kwargs) + self.version = self.find_version(identifier) + self.backend = self.create_aer_backend(self.identifier, ngpus, ncpu_threads, *args, **kwargs) + def find_version(self, identifier): + if identifier == 'cusvaer': + return version('cusvaer') + return qiskit.__qiskit_version__['qiskit-aer'] + def preprocess_circuit(self, circuit, *args, **kwargs): + if _internal_utils is not None: + _internal_utils.preprocess_circuit(self.identifier, circuit, *args, **kwargs) + t0 = time.perf_counter() self.transpiled_qc = qiskit.transpile(circuit, self.backend) # (circuit, basis_gates=['u3', 'cx'], backend=self.backend) t1 = time.perf_counter() @@ -165,8 +179,7 @@ def create_aer_backend(self, identifier, ngpus, ncpu_threads, *args, **kwargs): blocking_enable=blocking_enable, blocking_qubits=blocking_qubits, fusion_max_qubit=nfused, precision=self.precision) else: - raise ValueError(f"the backend {identifier} is not recognized") - + backend = None return backend def get_aer_blocking_setup(self, ngpus=None): diff --git a/benchmarks/cuquantum_benchmarks/backends/backend_qsim.py b/benchmarks/cuquantum_benchmarks/backends/backend_qsim.py index 3959103..f5164bc 100644 --- a/benchmarks/cuquantum_benchmarks/backends/backend_qsim.py +++ b/benchmarks/cuquantum_benchmarks/backends/backend_qsim.py @@ -22,6 +22,7 @@ def __init__(self, ngpus, ncpu_threads, precision, *args, identifier=None, **kwa raise ValueError("all qsim backends only support single precision") self.identifier = identifier qsim_options = self.create_qsim_options(identifier, ngpus, ncpu_threads, **kwargs) + self.version = qsimcirq.__version__ self.backend = qsimcirq.QSimSimulator(qsim_options=qsim_options) def run(self, circuit, nshots=1024): diff --git a/benchmarks/cuquantum_benchmarks/backends/backend_qulacs.py b/benchmarks/cuquantum_benchmarks/backends/backend_qulacs.py index dad0659..8d7c309 100644 --- a/benchmarks/cuquantum_benchmarks/backends/backend_qulacs.py +++ b/benchmarks/cuquantum_benchmarks/backends/backend_qulacs.py @@ -27,6 +27,7 @@ def __init__(self, ngpus, ncpu_threads, precision, *args, identifier=None, **kwa self.ncpu_threads = ncpu_threads self.nqubits = kwargs.pop('nqubits') self.state = self.create_qulacs_state() + self.version = qulacs.__version__ def create_qulacs_state(self): if self.identifier == 'qulacs-gpu': diff --git a/benchmarks/cuquantum_benchmarks/config.py b/benchmarks/cuquantum_benchmarks/config.py index 04581c6..5572b23 100644 --- a/benchmarks/cuquantum_benchmarks/config.py +++ b/benchmarks/cuquantum_benchmarks/config.py @@ -195,16 +195,6 @@ }, }, - 'naive': { - 'config': { - 'nshots': 1024, - 'nfused': None, - 'ngpus': 1, - 'ncputhreads': 0, - 'precision': 'single', - }, - }, - 'pennylane': { 'config': { 'nshots': 1024, @@ -245,17 +235,6 @@ }, }, - # dummy - 'pennylane-dumper': { - 'config': { - 'nshots': 1024, - 'nfused': None, - 'ngpus': 0, - 'ncputhreads': 1, - 'precision': 'single', - }, - }, - 'qulacs-gpu': { 'config': { 'nshots': 1024, diff --git a/benchmarks/cuquantum_benchmarks/frontends/__init__.py b/benchmarks/cuquantum_benchmarks/frontends/__init__.py index 3f0ae06..965adca 100644 --- a/benchmarks/cuquantum_benchmarks/frontends/__init__.py +++ b/benchmarks/cuquantum_benchmarks/frontends/__init__.py @@ -6,10 +6,6 @@ from .frontend_qiskit import Qiskit from .frontend_pny import Pennylane from .frontend_qulacs import Qulacs -try: - from .frontend_naive import Naive -except ImportError: - Naive = None frontends = { @@ -18,8 +14,7 @@ 'pennylane': Pennylane, 'qulacs': Qulacs } -if Naive: - frontends['naive'] = Naive + def createFrontend(frontend_name, nqubits, config): return frontends[frontend_name](nqubits, config) diff --git a/benchmarks/cuquantum_benchmarks/frontends/frontend_pny.py b/benchmarks/cuquantum_benchmarks/frontends/frontend_pny.py index 1265262..fe5c32b 100644 --- a/benchmarks/cuquantum_benchmarks/frontends/frontend_pny.py +++ b/benchmarks/cuquantum_benchmarks/frontends/frontend_pny.py @@ -25,7 +25,7 @@ def __init__(self, nqubits, config): def generateCircuit(self, gateSeq): last_g = gateSeq[-1] assert last_g.id == "measure" # TODO: relax this? - + def circuit(): measured_qs = None @@ -71,5 +71,5 @@ def circuit(): raise NotImplementedError(f"The gate type {g.id} is not defined") return pennylane.sample(wires=measured_qs) - - return circuit + + return circuit \ No newline at end of file diff --git a/benchmarks/cuquantum_benchmarks/run.py b/benchmarks/cuquantum_benchmarks/run.py index 6f7daa3..f73f40d 100644 --- a/benchmarks/cuquantum_benchmarks/run.py +++ b/benchmarks/cuquantum_benchmarks/run.py @@ -14,6 +14,14 @@ from .run_interface import BenchApiRunner, BenchCircuitRunner from ._utils import (EarlyReturnError, MPHandler, RawTextAndDefaultArgFormatter, str_to_seq,) +try: + from . import _internal_utils +except ImportError: + _internal_utils = None + + +if _internal_utils is not None: + _internal_utils.init() frontend_names = [f for f in frontends.keys()] @@ -300,8 +308,8 @@ def run(args=None): del args.benchmark config = backend_config[args.backend] - if ((args.frontend == 'cirq' and args.backend not in ('cirq', 'cutn', *[k for k in backends.keys() if k.startswith('qsim')])) - or (args.frontend == 'qiskit' and args.backend not in ('cutn', *[k for k in backends.keys() if 'aer' in k])) + if ((args.frontend == 'cirq' and args.backend not in (*[k for k in backends.keys() if k.startswith('cirq')], 'cutn', *[k for k in backends.keys() if k.startswith('qsim')])) + or (args.frontend == 'qiskit' and args.backend not in ('cutn', *[k for k in backends.keys() if k.startswith('qiskit')], *[k for k in backends.keys() if 'aer' in k])) or (args.frontend == 'naive' and args.backend != 'naive') or (args.frontend == 'pennylane' and not args.backend.startswith('pennylane')) or (args.frontend == 'qulacs' and not args.backend.startswith('qulacs'))): diff --git a/benchmarks/cuquantum_benchmarks/run_interface.py b/benchmarks/cuquantum_benchmarks/run_interface.py index c6fd005..92c7e09 100644 --- a/benchmarks/cuquantum_benchmarks/run_interface.py +++ b/benchmarks/cuquantum_benchmarks/run_interface.py @@ -203,43 +203,6 @@ def _fix_filename_for_cutn(self, circuit_filename, nqubits): circuit_filename += f"_{''.join(pauli)}" return circuit_filename, target, pauli - def extract_backend_version(self): - if 'aer' in self.backend: - import qiskit - version = qiskit.__qiskit_version__['qiskit-aer'] - elif 'qsim' in self.backend: - import qsimcirq - version = qsimcirq.__version__ - elif self.backend == 'cutn': - import cuquantum - version = cuquantum.cutensornet.get_version() - elif self.backend == 'cirq': - import cirq - version = cirq.__version__ - elif self.backend == 'naive': - from .backends import backends - version = backends['naive'].version - elif self.backend == 'pennylane': - import pennylane - version = pennylane.__version__ - elif self.backend == 'pennylane-lightning-gpu': - import pennylane_lightning_gpu - version = pennylane_lightning_gpu.__version__ - elif self.backend == 'pennylane-lightning-qubit': - import pennylane_lightning - version = pennylane_lightning.__version__ - elif self.backend == 'pennylane-lightning-kokkos': - import pennylane_lightning_kokkos - version = pennylane_lightning_kokkos.__version__ - elif self.backend == 'pennylane-dumper': - version = '0' # dummy - elif self.backend in ('qulacs-gpu', 'qulacs-cpu'): - import qulacs - version = qulacs.__version__ - else: - assert False - return version - def extract_frontend_version(self): if self.frontend == 'qiskit': import qiskit @@ -315,7 +278,7 @@ def _run(self): # get versions; it's assumed up to this point, the existence of Python modules for # both frontend and backend is confirmed - backend_version = self.extract_backend_version() + backend_version = backend.version frontend_version = self.extract_frontend_version() glue_layer_version = self.extract_glue_layer_version() if glue_layer_version is not None: @@ -337,7 +300,7 @@ def _run(self): # only cutn needs these, TODO: backend config circuit_filename=os.path.join(self.cache_dir, circuit_filename), target=target, - pauli=pauli, + pauli=pauli ) for k in preprocess_data.keys(): diff --git a/python/builder/pep517.py b/python/builder/pep517.py index 66f7c09..3df2bcf 100644 --- a/python/builder/pep517.py +++ b/python/builder/pep517.py @@ -30,8 +30,8 @@ def get_requires_for_build_wheel(config_settings=None): # set up version constraints: note that CalVer like 22.03 is normalized to # 22.3 by setuptools, so we must follow the same practice in the constraints; # also, we don't need the patch number here - cuqnt_require = [f'custatevec-cu{utils.cuda_major_ver}~=1.4', # ">=1.4.0,<2" - f'cutensornet-cu{utils.cuda_major_ver}~=2.2', # ">=2.2.0,<3" + cuqnt_require = [f'custatevec-cu{utils.cuda_major_ver}~=1.5', # ">=1.5.0,<2" + f'cutensornet-cu{utils.cuda_major_ver}~=2.3', # ">=2.3.0,<3" ] return _build_meta.get_requires_for_build_wheel(config_settings) + cuqnt_require diff --git a/python/cuquantum/_version.py b/python/cuquantum/_version.py index 19b2289..d67bed9 100644 --- a/python/cuquantum/_version.py +++ b/python/cuquantum/_version.py @@ -5,4 +5,4 @@ # Note: cuQuantum Python follows the cuQuantum SDK version, which is now # switched to YY.MM and is different from individual libraries' (semantic) # versioning scheme. -__version__ = '23.06.0' +__version__ = '23.10.0' diff --git a/python/cuquantum/custatevec/custatevec.pxd b/python/cuquantum/custatevec/custatevec.pxd index c654a5a..e32b2aa 100644 --- a/python/cuquantum/custatevec/custatevec.pxd +++ b/python/cuquantum/custatevec/custatevec.pxd @@ -53,6 +53,7 @@ cdef extern from '' nogil: _Index transferSize ctypedef void* _DistIndexBitSwapSchedulerDescriptor 'custatevecDistIndexBitSwapSchedulerDescriptor_t' ctypedef void* _SVSwapWorkerDescriptor 'custatevecSVSwapWorkerDescriptor_t' + ctypedef void* _SubSVMigratorDescriptor 'custatevecSubSVMigratorDescriptor_t' # cuStateVec enums diff --git a/python/cuquantum/custatevec/custatevec.pyx b/python/cuquantum/custatevec/custatevec.pyx index 86fca61..93e92c4 100644 --- a/python/cuquantum/custatevec/custatevec.pyx +++ b/python/cuquantum/custatevec/custatevec.pyx @@ -176,7 +176,7 @@ cdef extern from * nogil: _Handle, _DistIndexBitSwapSchedulerDescriptor, int32_t, int32_t, _SVSwapParameters*) int custatevecSVSwapWorkerCreate( - _Handle, _SVSwapWorkerDescriptor, _CommunicatorDescriptor, void*, + _Handle, _SVSwapWorkerDescriptor*, _CommunicatorDescriptor, void*, int32_t, Event, DataType, Stream, size_t*, size_t*) int custatevecSVSwapWorkerDestroy( _Handle, _SVSwapWorkerDescriptor) @@ -190,6 +190,12 @@ cdef extern from * nogil: _Handle, _SVSwapWorkerDescriptor, _SVSwapParameters*, int) int custatevecSVSwapWorkerExecute( _Handle, _SVSwapWorkerDescriptor, _Index, _Index) + int custatevecSubSVMigratorCreate( + _Handle, _SubSVMigratorDescriptor*, void*, DataType, int, int) + int custatevecSubSVMigratorDestroy( + _Handle, _SubSVMigratorDescriptor) + int custatevecSubSVMigratorMigrate( + _Handle, _SubSVMigratorDescriptor, int, const void*, void*, _Index, _Index) # TODO: make this cpdef? @@ -3094,6 +3100,74 @@ cpdef sv_swap_worker_execute( check_status(status) +cpdef intptr_t sub_sv_migrator_create( + intptr_t handle, intptr_t device_slots, int sv_data_type, + int n_device_slots, int n_local_index_bits) except*: + """Create a cuStateVec sub state vector migrator. + + Args: + handle (intptr_t): The library handle. + device_slots (intptr_t): The pointer address to a device slots. + sv_data_type (cuquantum.cudaDataType): The data type of the device slots + n_device_slots (int): The number of device slots + n_local_index_bits (int): The number of index bits of sub state vectors. + + Returns: + An instance of the opaque migrator descriptor (as Python :class:`int`). + + .. seealso:: `custatevecSubSVMigratorCreate` + """ + cdef _SubSVMigratorDescriptor migrator + with nogil: + status = custatevecSubSVMigratorCreate( + <_Handle>handle, &migrator, device_slots, + sv_data_type, n_device_slots, n_local_index_bits) + check_status(status) + return migrator + + +cpdef sub_sv_migrator_destroy( + intptr_t handle, intptr_t migrator): + """Destroy the sub state vector migrator. + + Args: + handle (intptr_t): The library handle. + migrator (intptr_t): The sub state vector migrator descriptor. + + .. seealso:: `custatevecSubSVMigratorDestroy` + """ + with nogil: + status = custatevecSubSVMigratorDestroy( + <_Handle>handle, <_SubSVMigratorDescriptor>migrator) + check_status(status) + + +cpdef sub_sv_migrator_migrate( + intptr_t handle, intptr_t migrator, int device_slot_idx, + intptr_t src_sub_sv, intptr_t dst_sub_sv, _Index begin, _Index end): + """Performs state vector migration between device slots and given sub state vectors + + Args: + handle (intptr_t): The library handle. + migrator (intptr_t): The sub state vector migrator descriptor. + device_slot_idx (int): The slot index of a device slot + src_sub_sv (intptr_t): The pointer address (as Python :class:`int`) to the + src sub state vector pointer. + dst_sub_sv (intptr_t): The pointer address (as Python :class:`int`) to the + dst sub state vector pointer. + begin (int64_t): The index in a device slot to start sub state vector migration + end (int64_t): The index in a device slot to end sub state vector migration + + .. seealso:: `custatevecSubSVMigratorMigrate` + """ + with nogil: + status = custatevecSubSVMigratorMigrate( + <_Handle>handle, <_SubSVMigratorDescriptor>migrator, + device_slot_idx, src_sub_sv, dst_sub_sv, + begin, end) + check_status(status) + + # can't be cpdef because args & kwargs can't be handled in a C signature def logger_set_callback_data(callback, *args, **kwargs): """Set the logger callback along with arguments. diff --git a/python/cuquantum/cutensornet/_internal/decomposition_utils.py b/python/cuquantum/cutensornet/_internal/decomposition_utils.py index 56c911f..814fdae 100644 --- a/python/cuquantum/cutensornet/_internal/decomposition_utils.py +++ b/python/cuquantum/cutensornet/_internal/decomposition_utils.py @@ -44,7 +44,8 @@ 'rel_cutoff': cutn.TensorSVDConfigAttribute.REL_CUTOFF, 'partition': cutn.TensorSVDConfigAttribute.S_PARTITION, 'normalization': cutn.TensorSVDConfigAttribute.S_NORMALIZATION, - 'algorithm': cutn.TensorSVDConfigAttribute.ALGO} + 'algorithm': cutn.TensorSVDConfigAttribute.ALGO, + 'discarded_weight_cutoff': cutn.TensorSVDConfigAttribute.DISCARDED_WEIGHT_CUTOFF} SVD_INFO_MAP = {'full_extent': cutn.TensorSVDInfoAttribute.FULL_EXTENT, 'reduced_extent': cutn.TensorSVDInfoAttribute.REDUCED_EXTENT, @@ -294,20 +295,20 @@ def get_svd_info_dict(handle, svd_info): return info -def parse_decompose_operands_options(options, wrapped_operands, allowed_dtype_names=None): +def parse_decompose_operands_options(options, wrapped_operands, stream, allowed_dtype_names=None): """ Given initially wrapped tensors and network options, wrap the operands to device and create an internal NetworkOptions object. If cutensornet library handle is not provided in `options`, one will be created in the internal options. """ - device_id = utils.get_network_device_id(wrapped_operands) - logger = logging.getLogger() if options.logger is None else options.logger + package = utils.get_operands_package(wrapped_operands) operands_location = 'cuda' + device_id = utils.get_network_device_id(wrapped_operands) if device_id is None: + package = wrapped_operands[0].name + if package == 'numpy': + package = 'cupy' operands_location = 'cpu' device_id = options.device_id - logger.info(f"Begin transferring input data from host to device {device_id}") - wrapped_operands = tensor_wrapper.to(wrapped_operands, device_id) - logger.info("Input data transfer finished") # initialize handle once if not provided if options.handle is not None: @@ -323,7 +324,14 @@ def parse_decompose_operands_options(options, wrapped_operands, allowed_dtype_na raise ValueError(f"dtype {dtype_name} not supported") compute_type = options.compute_type if options.compute_type is not None else typemaps.NAME_TO_COMPUTE_TYPE[dtype_name] - package = utils.get_operands_package(wrapped_operands) + stream_holder = utils.get_or_create_stream(options.device_id, stream, package) + + logger = logging.getLogger() if options.logger is None else options.logger + if operands_location == 'cpu': + logger.info(f"Begin transferring input data from host to device {device_id}") + wrapped_operands = tensor_wrapper.to(wrapped_operands, device_id, stream_holder) + logger.info("Input data transfer finished") + allocator = options.allocator if options.allocator is not None else memory._MEMORY_MANAGER[package](device_id, logger) internal_options = options.__class__(device_id=device_id, @@ -334,17 +342,17 @@ def parse_decompose_operands_options(options, wrapped_operands, allowed_dtype_na memory_limit=options.memory_limit, allocator=allocator) - return wrapped_operands, internal_options, own_handle, operands_location + return wrapped_operands, internal_options, own_handle, operands_location, stream_holder -def allocate_and_set_workspace(handle, allocator, workspace_desc, pref, mem_space, workspace_kind, device_id, stream, stream_ctx, logger, task_name=''): +def allocate_and_set_workspace(handle, allocator, workspace_desc, pref, mem_space, workspace_kind, device_id, stream_holder, logger, task_name=''): """ Allocate and set the workspace in the workspace descriptor. """ workspace_size = cutn.workspace_get_memory_size(handle, workspace_desc, pref, mem_space, workspace_kind) # Allocate and set workspace if mem_space == cutn.Memspace.DEVICE: - with utils.device_ctx(device_id), stream_ctx: + with utils.device_ctx(device_id), stream_holder.ctx: try: logger.debug(f"Allocating device memory for {task_name}") workspace_ptr = allocator.memalloc(workspace_size) @@ -353,7 +361,7 @@ def allocate_and_set_workspace(handle, allocator, workspace_desc, pref, mem_spac "'BaseCUDAMemoryManager' protocol." raise TypeError(message) from e - logger.debug(f"Finished allocating device memory of size {formatters.MemoryStr(workspace_size)} for decomposition in the context of stream {stream}.") + logger.debug(f"Finished allocating device memory of size {formatters.MemoryStr(workspace_size)} for decomposition in the context of stream {stream_holder.obj}.") device_ptr = utils.get_ptr_from_memory_pointer(workspace_ptr) cutn.workspace_set_memory(handle, workspace_desc, mem_space, workspace_kind, device_ptr, workspace_size) logger.debug(f"The workspace memory (device pointer = {device_ptr}) has been set in the workspace descriptor.") @@ -376,7 +384,8 @@ def _destroy_tensor_descriptors(desc_tensors): cutn.destroy_tensor_descriptor(t) -def create_operands_and_descriptors(handle, wrapped_operands, size_dict, inputs, outputs, mid_extent, method, device_id, stream_ctx, logger): +def create_operands_and_descriptors( + handle, wrapped_operands, size_dict, inputs, outputs, mid_extent, method, device_id, stream_holder, logger): """ Create empty tensor operands and corresponding tensor descriptors for a decomposition problem. """ @@ -402,7 +411,7 @@ def create_operands_and_descriptors(handle, wrapped_operands, size_dict, inputs, output_operands = [] with utils.device_ctx(device_id): for extent, tensor_modes in zip(output_extents, outputs): - operand = utils.create_empty_tensor(output_class, extent, dtype_name, device_id, stream_ctx) + operand = utils.create_empty_tensor(output_class, extent, dtype_name, device_id, stream_holder) output_operands.append(operand) output_tensor_descriptors.append(operand.create_tensor_descriptor(handle, tensor_modes)) @@ -413,7 +422,7 @@ def create_operands_and_descriptors(handle, wrapped_operands, size_dict, inputs, s_dtype_name = 'float64' else: raise ValueError(f"{dtype_name} data type not supported") - s = utils.create_empty_tensor(output_class, (mid_extent, ), s_dtype_name, device_id, stream_ctx) + s = utils.create_empty_tensor(output_class, (mid_extent, ), s_dtype_name, device_id, stream_holder) s_ptr = s.data_ptr logger.debug("The output tensors and descriptors have been created.") except: @@ -423,13 +432,13 @@ def create_operands_and_descriptors(handle, wrapped_operands, size_dict, inputs, return input_tensor_descriptors, output_operands, output_tensor_descriptors, s, s_ptr -def get_return_operand_data(tensor, target_location): +def get_return_operand_data(tensor, target_location, stream_holder): """ Given wrapped tensors, fetch the return operands based on target location. """ if tensor is None: # potentially for s return tensor if target_location == 'cpu': - return tensor.to('cpu') + return tensor.to('cpu', stream_holder=stream_holder) else: # already on device return tensor.tensor diff --git a/python/cuquantum/cutensornet/_internal/einsum_parser.py b/python/cuquantum/cutensornet/_internal/einsum_parser.py index 44da696..0bd4204 100644 --- a/python/cuquantum/cutensornet/_internal/einsum_parser.py +++ b/python/cuquantum/cutensornet/_internal/einsum_parser.py @@ -371,6 +371,8 @@ def parse_einsum(*operands): # Map data to ordinals for cutensornet. inputs, output, mode_map_user_to_ord, mode_map_ord_to_user, label_end = map_modes(inputs, output, num_extra_labels, morpher) + has_user_output = (output is not None) + mapper = ModeLabelMapper(mode_map_ord_to_user) mapping_morpher = select_morpher(interleaved, mapper) @@ -383,4 +385,4 @@ def parse_einsum(*operands): # Create mode-extent map based on internal mode numbers. size_dict = create_size_dict(inputs, operands) - return operands, inputs, output, size_dict, mode_map_user_to_ord, mode_map_ord_to_user, interleaved or ellipses + return operands, inputs, output, has_user_output, size_dict, mode_map_user_to_ord, mode_map_ord_to_user, interleaved, ellipses diff --git a/python/cuquantum/cutensornet/_internal/grad_torch.py b/python/cuquantum/cutensornet/_internal/grad_torch.py new file mode 100644 index 0000000..d25d712 --- /dev/null +++ b/python/cuquantum/cutensornet/_internal/grad_torch.py @@ -0,0 +1,73 @@ +# Copyright (c) 2023, NVIDIA CORPORATION & AFFILIATES +# +# SPDX-License-Identifier: BSD-3-Clause + +import itertools + +# TODO: right now, we use try-except throughout the codebase to check the +# presence of PyTorch, so if it exists it'd get imported. We should switch +# to use importlib.util.find_spec('torch') so as to reduce the import time. +try: + import torch +except ImportError: + torch = None + + +if torch is not None: + + class _TorchContract(torch.autograd.Function): + + @staticmethod + def forward(context, network, optimize, stream, return_info, *operands): + + # Save objects needed in the backward pass. + context.network = network + context.stream = stream + + # Compute path. + opt_info = network.contract_path(optimize=optimize) + + # Skip autotuning since the network is contracted only once. + + # Contraction. + out = network.contract(stream=stream) + + if return_info: + return out, opt_info + else: + return out + + @staticmethod + def backward(context, *output_grad): + + try: + # Retrieve cached objects. + network = context.network + stream = context.stream + + # Regardless of return_info, we only care about the gradient of + # the first return value. + output_grad = output_grad[0] + + # Compute backprop. + input_grads = network.gradients(output_grad, stream=stream) + + # Rearrange return values based on the input format. + if network.is_interleaved: + out = [None, None, None, None, + *itertools.chain(*itertools.zip_longest(input_grads, [None]))] + if network.has_user_output: + out.append(None) + out = tuple(out) + else: + out = (None, None, None, None, + None, *input_grads) + finally: + # Free network resources explicitly. + network.free() + + return out + +else: + + _TorchContract = None diff --git a/python/cuquantum/cutensornet/_internal/optimizer_ifc.py b/python/cuquantum/cutensornet/_internal/optimizer_ifc.py index a92dc98..b0f583f 100644 --- a/python/cuquantum/cutensornet/_internal/optimizer_ifc.py +++ b/python/cuquantum/cutensornet/_internal/optimizer_ifc.py @@ -157,7 +157,8 @@ def path(self, path): if num_contraction != len(network.operands) - 1: raise ValueError(f"The length of the contraction path ({num_contraction}) must be one less than the number of operands ({len(network.operands)}).") - path = reduce(operator.concat, path) + if num_contraction > 0: + path = reduce(operator.concat, path) path_array = np.asarray(path, dtype=np.int32) # Construct the path type. @@ -235,7 +236,7 @@ def intermediate_modes(self): cutn.contraction_optimizer_info_get_attribute(network.handle, network.optimizer_info_ptr, InfoEnum.INTERMEDIATE_MODES, intermediate_modes.ctypes.data, size) count, out = 0, list() - mode_type = tuple if network.is_interleaved else ''.join + mode_type = tuple if (network.is_interleaved or network.has_ellipses) else ''.join for n in num_intermediate_modes: out.append(mode_type(map(lambda m: network.mode_map_ord_to_user[m], intermediate_modes[count:count+n]))) # Convert to user mode labels count += n diff --git a/python/cuquantum/cutensornet/_internal/package_ifc.py b/python/cuquantum/cutensornet/_internal/package_ifc.py index 6b94752..1309e5e 100644 --- a/python/cuquantum/cutensornet/_internal/package_ifc.py +++ b/python/cuquantum/cutensornet/_internal/package_ifc.py @@ -9,6 +9,9 @@ __all__ = ['Package'] from abc import ABC, abstractmethod +from dataclasses import dataclass +from contextlib import nullcontext +from typing import Any class Package(ABC): @@ -68,3 +71,21 @@ def create_stream(device_id): device_id: The id (ordinal) of the device. """ raise NotImplementedError + + +@dataclass +class StreamHolder: + """A data class for easing CUDA stream manipulation. + + Attributes: + ctx: A context manager for using the specified stream. + device_id (int): The device ID where the encapsulated stream locates. + obj: A foreign object that holds the stream alive. + package (str): + ptr (int): The address of the underlying ``cudaStream_t`` object. + """ + ctx: Any = nullcontext() + device_id: int = -2 + obj: Any = None + package: str = "" + ptr: int = 0 diff --git a/python/cuquantum/cutensornet/_internal/package_ifc_torch.py b/python/cuquantum/cutensornet/_internal/package_ifc_torch.py index 798a5f8..42fd66b 100644 --- a/python/cuquantum/cutensornet/_internal/package_ifc_torch.py +++ b/python/cuquantum/cutensornet/_internal/package_ifc_torch.py @@ -35,4 +35,3 @@ def create_external_stream(device_id, stream_ptr): def create_stream(device_id): stream = torch.cuda.Stream(device=device_id) return stream - diff --git a/python/cuquantum/cutensornet/_internal/tensor_ifc.py b/python/cuquantum/cutensornet/_internal/tensor_ifc.py index 921ae1d..bc6df7d 100644 --- a/python/cuquantum/cutensornet/_internal/tensor_ifc.py +++ b/python/cuquantum/cutensornet/_internal/tensor_ifc.py @@ -45,7 +45,7 @@ def empty(cls, shape, **context): raise NotImplementedError @abstractmethod - def numpy(self): + def numpy(self, stream_holder): raise NotImplementedError @property @@ -64,7 +64,11 @@ def empty(cls, shape, **context): raise NotImplementedError @abstractmethod - def to(self, device='cpu'): + def to(self, device='cpu', stream_holder=None): + raise NotImplementedError + + @abstractmethod + def copy_(self, src, stream_holder=None): raise NotImplementedError @staticmethod diff --git a/python/cuquantum/cutensornet/_internal/tensor_ifc_cupy.py b/python/cuquantum/cutensornet/_internal/tensor_ifc_cupy.py index efe69e0..6d99b3e 100644 --- a/python/cuquantum/cutensornet/_internal/tensor_ifc_cupy.py +++ b/python/cuquantum/cutensornet/_internal/tensor_ifc_cupy.py @@ -12,6 +12,7 @@ import numpy from . import utils +from .package_ifc import StreamHolder from .tensor_ifc import Tensor from .. import cutensornet as cutn @@ -52,8 +53,13 @@ def shape(self): def strides(self): return tuple(stride_in_bytes // self.tensor.itemsize for stride_in_bytes in self.tensor.strides) - def numpy(self): - return self.tensor.get() + def numpy(self, stream_holder=StreamHolder()): + stream = stream_holder.obj + out = self.tensor.get(stream=stream) + # cupy/cupy#7820 + if stream is not None: + stream.synchronize() + return out @classmethod def empty(cls, shape, **context): @@ -63,6 +69,7 @@ def empty(cls, shape, **context): name = context.get('dtype', 'float32') dtype = CupyTensor.name_to_dtype[name] device = context.get('device', None) + strides = context.get('strides', None) if isinstance(device, cupy.cuda.Device): device_id = device.id @@ -72,32 +79,42 @@ def empty(cls, shape, **context): raise ValueError(f"The device must be specified as an integer or cupy.cuda.Device instance, not '{device}'.") with utils.device_ctx(device_id): - tensor = cupy.empty(shape, dtype=dtype) + if strides: + # need an explicit allocation due to cupy/cupy#7818 + size = dtype.itemsize + for s in shape: + size = size * s + ptr = cupy.cuda.alloc(size) + # when strides is not None, it should be of unit counts not bytes + strides = tuple(s * dtype.itemsize for s in strides) + tensor = cupy.ndarray(shape, dtype=dtype, strides=strides, memptr=ptr) + else: + tensor = cupy.ndarray(shape, dtype=dtype) return tensor - def to(self, device='cpu'): + def to(self, device='cpu', stream_holder=StreamHolder()): """ Create a copy of the tensor on the specified device (integer or 'cpu'). Copy to Numpy ndarray if CPU, otherwise return Cupy type. """ if device == 'cpu': - return self.numpy() + return self.numpy(stream_holder=stream_holder) if not isinstance(device, int): raise ValueError(f"The device must be specified as an integer or 'cpu', not '{device}'.") - with utils.device_ctx(device): + with utils.device_ctx(device), stream_holder.ctx: tensor_device = cupy.asarray(self.tensor) return tensor_device - def copy_(self, src): + def copy_(self, src, stream_holder=StreamHolder()): """ Inplace copy of src (copy the data from src into self). """ - - cupy.copyto(self.tensor, src) + with stream_holder.ctx: + cupy.copyto(self.tensor, src) def istensor(self): """ @@ -110,4 +127,3 @@ def reshape_to_match_tensor_descriptor(self, handle, desc_tensor): if tuple(extents) != self.shape: strides = [i * self.tensor.itemsize for i in strides] self.tensor = cupy.ndarray(extents, dtype=self.tensor.dtype, memptr=self.tensor.data, strides=strides) - diff --git a/python/cuquantum/cutensornet/_internal/tensor_ifc_numpy.py b/python/cuquantum/cutensornet/_internal/tensor_ifc_numpy.py index ed6a0f2..c161a6b 100644 --- a/python/cuquantum/cutensornet/_internal/tensor_ifc_numpy.py +++ b/python/cuquantum/cutensornet/_internal/tensor_ifc_numpy.py @@ -12,6 +12,7 @@ import numpy from . import utils +from .package_ifc import StreamHolder from .tensor_ifc import Tensor @@ -51,7 +52,7 @@ def shape(self): def strides(self): return tuple(stride_in_bytes // self.tensor.itemsize for stride_in_bytes in self.tensor.strides) - def numpy(self): + def numpy(self, stream_holder=StreamHolder()): return self.tensor @classmethod @@ -61,9 +62,11 @@ def empty(cls, shape, **context): """ name = context.get('dtype', 'float32') dtype = NumpyTensor.name_to_dtype[name] - return cls(module.empty(shape, dtype=dtype)) + strides = context.get('strides', None) + # when strides is not None, it should be of unit counts not bytes + return cls(module.ndarray(shape, dtype=dtype, strides=(tuple(s * dtype.itemsize for s in strides) if strides else None))) - def to(self, device='cpu'): + def to(self, device='cpu', stream_holder=StreamHolder()): """ Create a copy of the tensor on the specified device (integer or 'cpu'). Copy to Cupy ndarray on the specified device if it @@ -75,11 +78,14 @@ def to(self, device='cpu'): if not isinstance(device, int): raise ValueError(f"The device must be specified as an integer or 'cpu', not '{device}'.") - with utils.device_ctx(device): + with utils.device_ctx(device), stream_holder.ctx: tensor_device = cupy.asarray(self.tensor) return tensor_device + def copy_(self, src, stream_holder=StreamHolder()): + raise NotImplementedError + def istensor(self): """ Check if the object is ndarray-like. @@ -88,4 +94,4 @@ def istensor(self): def reshape_to_match_tensor_descriptor(self, handle, desc_tensor): #NOTE: this method is only called for CupyTensor and TorchTensor - raise NotImplementedError \ No newline at end of file + raise NotImplementedError diff --git a/python/cuquantum/cutensornet/_internal/tensor_ifc_torch.py b/python/cuquantum/cutensornet/_internal/tensor_ifc_torch.py index a7e3a87..ece81e8 100644 --- a/python/cuquantum/cutensornet/_internal/tensor_ifc_torch.py +++ b/python/cuquantum/cutensornet/_internal/tensor_ifc_torch.py @@ -11,6 +11,7 @@ import torch from . import typemaps +from .package_ifc import StreamHolder from .tensor_ifc import Tensor from .. import cutensornet as cutn @@ -51,8 +52,9 @@ def shape(self): def strides(self): return self.tensor.stride() - def numpy(self): - return self.tensor.get() + def numpy(self, stream_holder=StreamHolder()): + # We currently do not use this. + raise NotImplementedError @classmethod def empty(cls, shape, **context): @@ -62,28 +64,36 @@ def empty(cls, shape, **context): name = context.get('dtype', 'float32') dtype = TorchTensor.name_to_dtype[name] device = context.get('device', None) - tensor = torch.empty(shape, dtype=dtype, device=device) + strides = context.get('strides', None) + if strides: + # note: torch strides is not scaled by bytes + tensor = torch.empty_strided(shape, strides, dtype=dtype, device=device) + else: + tensor = torch.empty(shape, dtype=dtype, device=device) return tensor - def to(self, device='cpu'): + def to(self, device='cpu', stream_holder=StreamHolder()): """ Create a copy of the tensor on the specified device (integer or 'cpu'). Copy to Numpy ndarray if CPU, otherwise return Cupy type. """ - if not(device == 'cpu' or isinstance(device, int)): + if not (device == 'cpu' or isinstance(device, int)): raise ValueError(f"The device must be specified as an integer or 'cpu', not '{device}'.") - tensor_device = self.tensor.to(device=device) + non_blocking = False if device == 'cpu' else True + + with stream_holder.ctx: + tensor_device = self.tensor.to(device=device, non_blocking=non_blocking) return tensor_device - def copy_(self, src): + def copy_(self, src, stream_holder=StreamHolder()): """ Inplace copy of src (copy the data from src into self). """ - - self.tensor.copy_(src) + with stream_holder.ctx: + self.tensor.copy_(src) def istensor(self): """ @@ -94,6 +104,5 @@ def istensor(self): def reshape_to_match_tensor_descriptor(self, handle, desc_tensor): _, _, extents, strides = cutn.get_tensor_details(handle, desc_tensor) if tuple(extents) != self.shape: - #note: torch strides is not scaled by bytes + # note: torch strides is not scaled by bytes self.tensor = torch.as_strided(self.tensor, tuple(extents), tuple(strides)) - diff --git a/python/cuquantum/cutensornet/_internal/tensor_wrapper.py b/python/cuquantum/cutensornet/_internal/tensor_wrapper.py index b1beeaf..10a7822 100644 --- a/python/cuquantum/cutensornet/_internal/tensor_wrapper.py +++ b/python/cuquantum/cutensornet/_internal/tensor_wrapper.py @@ -6,7 +6,7 @@ Entry point to using tensors from different libraries seamlessly. """ -__all__ = [ 'infer_tensor_package', 'wrap_operands', 'wrap_operands', 'to', 'copy_'] +__all__ = [ 'infer_tensor_package', 'wrap_operand', 'wrap_operands', 'to', 'copy_'] import functools @@ -103,23 +103,21 @@ def wrap_operands(native_operands): return wrapped_operands -def to(operands, device): +def to(operands, device, stream_holder): """ Copy the wrapped operands to the specified device ('cpu' or int) and return the wrapped operands on the device. """ - operands = tuple(o.to(device) for o in operands) + operands = tuple(o.to(device, stream_holder) for o in operands) return wrap_operands(operands) -def copy_(src, dest): +def copy_(src, dest, stream_holder): """ Copy the wrapped operands in dest to the corresponding wrapped operands in src. """ for s, d in zip(src, dest): if s.device_id is None: - s = wrap_operand(s.to(d.device_id)) - d.copy_(s.tensor) - - + s = wrap_operand(s.to(d.device_id, stream_holder=stream_holder)) + d.copy_(s.tensor, stream_holder=stream_holder) diff --git a/python/cuquantum/cutensornet/_internal/utils.py b/python/cuquantum/cutensornet/_internal/utils.py index 7e628c2..d9ea8b1 100644 --- a/python/cuquantum/cutensornet/_internal/utils.py +++ b/python/cuquantum/cutensornet/_internal/utils.py @@ -18,6 +18,7 @@ from . import mem_limit from . import package_wrapper from . import tensor_wrapper +from .package_ifc import StreamHolder def infer_object_package(obj): @@ -98,30 +99,34 @@ def get_or_create_stream(device_id, stream, op_package): op_package: The package the tensor network operands belong to. Returns: - tuple: CuPy stream object, package stream context, stream pointer. + StreamHolder: Hold a CuPy stream object, package stream context, stream pointer, ... """ op_package_ifc = package_wrapper.PACKAGE[op_package] if stream is None: stream = op_package_ifc.get_current_stream(device_id) - return _create_stream_ctx_ptr_cupy_stream(op_package_ifc, stream) + obj, ctx, ptr = _create_stream_ctx_ptr_cupy_stream(op_package_ifc, stream) + return StreamHolder( + **{'ctx': ctx, 'obj': obj, 'ptr': ptr, 'device_id': device_id, 'package': op_package}) if isinstance(stream, int): - stream_ptr = stream + ptr = stream if op_package == 'torch': message = "A stream object must be provided for PyTorch operands, not stream pointer." raise TypeError(message) - stream_ctx = op_package_ifc.to_stream_context(stream) - stream = cp.cuda.ExternalStream(stream_ptr) - - return stream, stream_ctx, stream_ptr + obj = cp.cuda.ExternalStream(ptr) + ctx = op_package_ifc.to_stream_context(obj) + return StreamHolder( + **{'ctx': ctx, 'obj': obj, 'ptr': ptr, 'device_id': device_id, 'package': op_package}) stream_package = infer_object_package(stream) if stream_package != op_package: message = "The stream object must belong to the same package as the tensor network operands." raise TypeError(message) - return _create_stream_ctx_ptr_cupy_stream(op_package_ifc, stream) + obj, ctx, ptr = _create_stream_ctx_ptr_cupy_stream(op_package_ifc, stream) + return StreamHolder( + **{'ctx': ctx, 'obj': obj, 'ptr': ptr, 'device_id': device_id, 'package': op_package}) def get_memory_limit(memory_limit, device): @@ -167,11 +172,11 @@ def get_operands_data(operands): """ Get the raw data pointer of the input operands for cuTensorNet. """ - op_data = tuple(o.data_ptr for o in operands) + op_data = tuple(o.data_ptr if o is not None else 0 for o in operands) return op_data -def create_empty_tensor(cls, extents, dtype, device_id, stream_ctx): +def create_empty_tensor(cls, extents, dtype, device_id, stream_holder, strides=None): """ Create a wrapped tensor of the same type as (the wrapped) cls on the specified device having the specified extents and dtype. @@ -179,13 +184,13 @@ def create_empty_tensor(cls, extents, dtype, device_id, stream_ctx): The tensor is created within a stream context to allow for asynchronous memory allocators like CuPy's MemoryAsyncPool. """ - with stream_ctx: - tensor = cls.empty(extents, dtype=dtype, device=device_id) + with stream_holder.ctx: + tensor = cls.empty(extents, dtype=dtype, device=device_id, strides=strides) tensor = tensor_wrapper.wrap_operand(tensor) return tensor -def create_output_tensor(cls, package, output, size_dict, device_id, stream, data_type): +def create_output_tensor(cls, output, size_dict, device_id, stream_holder, data_type): """ Create output tensor and associated data (modes, extents, strides). This operation is ordered through events and is safe to use with asynchronous memory pools. @@ -193,11 +198,9 @@ def create_output_tensor(cls, package, output, size_dict, device_id, stream, dat modes = tuple(m for m in output) extents = tuple(size_dict[m] for m in output) - stream, stream_ctx, _ = get_or_create_stream(device_id, stream, package) - with device_ctx(device_id): - output = create_empty_tensor(cls, extents, data_type, device_id, stream_ctx) - output_event = stream.record() + output = create_empty_tensor(cls, extents, data_type, device_id, stream_holder) + output_event = stream_holder.obj.record() strides = output.strides return output, output_event, modes, extents, strides @@ -375,7 +378,7 @@ def check_and_set_options(required: Mapping[str, Value], provided: Mapping[str, @contextlib.contextmanager -def cuda_call_ctx(stream, blocking=True, timing=True): +def cuda_call_ctx(stream_holder, blocking=True, timing=True): """ A simple context manager that provides (non-)blocking behavior depending on the `blocking` parameter for CUDA calls. The call is timed only for blocking behavior when timing is requested. @@ -384,6 +387,8 @@ def cuda_call_ctx(stream, blocking=True, timing=True): event is returned together with a `Value` object that stores the elapsed time if the call is blocking and timing is requested, or None otherwise. """ + stream = stream_holder.obj + if blocking: start = cp.cuda.Event(disable_timing = False if timing else True) stream.record(start) diff --git a/python/cuquantum/cutensornet/cutensornet.pxd b/python/cuquantum/cutensornet/cutensornet.pxd index 3a21b92..ff79bff 100644 --- a/python/cuquantum/cutensornet/cutensornet.pxd +++ b/python/cuquantum/cutensornet/cutensornet.pxd @@ -34,8 +34,11 @@ cdef extern from '' nogil: ctypedef void* _TensorSVDConfig 'cutensornetTensorSVDConfig_t' ctypedef void* _TensorSVDInfo 'cutensornetTensorSVDInfo_t' ctypedef void* _State 'cutensornetState_t' + ctypedef void* _StateAccessor 'cutensornetStateAccessor_t' + ctypedef void* _StateExpectation 'cutensornetStateExpectation_t' ctypedef void* _StateMarginal 'cutensornetStateMarginal_t' ctypedef void* _StateSampler 'cutensornetStateSampler_t' + ctypedef void* _NetworkOperator 'cutensornetNetworkOperator_t' # cuTensorNet structs ctypedef struct _NodePair 'cutensornetNodePair_t': @@ -175,6 +178,7 @@ cdef extern from '' nogil: CUTENSORNET_TENSOR_SVD_CONFIG_S_PARTITION CUTENSORNET_TENSOR_SVD_CONFIG_ALGO CUTENSORNET_TENSOR_SVD_CONFIG_ALGO_PARAMS + CUTENSORNET_TENSOR_SVD_CONFIG_DISCARDED_WEIGHT_CUTOFF ctypedef enum _TensorSVDAlgo 'cutensornetTensorSVDAlgo_t': CUTENSORNET_TENSOR_SVD_ALGO_GESVD @@ -208,6 +212,12 @@ cdef extern from '' nogil: ctypedef enum _StatePurity 'cutensornetStatePurity_t': CUTENSORNET_STATE_PURITY_PURE + ctypedef enum _ExpectationAttribute 'cutensornetExpectationAttributes_t': + CUTENSORNET_EXPECTATION_OPT_NUM_HYPER_SAMPLES + + ctypedef enum _AccessorAttribute 'cutensornetAccessorAttributes_t': + CUTENSORNET_ACCESSOR_OPT_NUM_HYPER_SAMPLES + ctypedef enum _MarginalAttribute 'cutensornetMarginalAttributes_t': CUTENSORNET_MARGINAL_OPT_NUM_HYPER_SAMPLES @@ -221,3 +231,16 @@ cdef extern from '' nogil: CUTENSORNET_NETWORK_INPUT_TENSORS_CONJUGATED CUTENSORNET_NETWORK_INPUT_TENSORS_NUM_REQUIRE_GRAD CUTENSORNET_NETWORK_INPUT_TENSORS_REQUIRE_GRAD + + ctypedef enum _BoundaryCondition 'cutensornetBoundaryCondition_t': + CUTENSORNET_BOUNDARY_CONDITION_OPEN + + ctypedef enum _StateAttribute 'cutensornetStateAttributes_t': + CUTENSORNET_STATE_MPS_CANONICAL_CENTER + CUTENSORNET_STATE_MPS_SVD_CONFIG_ABS_CUTOFF + CUTENSORNET_STATE_MPS_SVD_CONFIG_REL_CUTOFF + CUTENSORNET_STATE_MPS_SVD_CONFIG_S_NORMALIZATION + CUTENSORNET_STATE_MPS_SVD_CONFIG_ALGO + CUTENSORNET_STATE_MPS_SVD_CONFIG_ALGO_PARAMS + CUTENSORNET_STATE_MPS_SVD_CONFIG_DISCARDED_WEIGHT_CUTOFF + CUTENSORNET_STATE_NUM_HYPER_SAMPLES diff --git a/python/cuquantum/cutensornet/cutensornet.pyx b/python/cuquantum/cutensornet/cutensornet.pyx index a5d0c09..c83d197 100644 --- a/python/cuquantum/cutensornet/cutensornet.pyx +++ b/python/cuquantum/cutensornet/cutensornet.pyx @@ -15,6 +15,7 @@ from cuquantum.utils cimport cuqnt_alloc_wrapper from cuquantum.utils cimport cuqnt_free_wrapper from cuquantum.utils cimport get_buffer_pointer from cuquantum.utils cimport logger_callback_with_data +from cuquantum.utils cimport cuDoubleComplex from enum import IntEnum import warnings @@ -244,7 +245,37 @@ cdef extern from * nogil: const int64_t*, const int32_t, const int32_t, const int32_t, int64_t*) int cutensornetStateUpdateTensor( const _Handle, _State, int64_t, void*, int32_t) + int cutensornetStateConfigure( + const _Handle, _State, _StateAttribute, const void*, size_t) + int cutensornetStatePrepare( + const _Handle, _State, size_t, _WorkspaceDescriptor, Stream) + int cutensornetStateCompute( + const _Handle, _State, _WorkspaceDescriptor, int64_t*[], int64_t*[], void*[], Stream) + int cutensornetGetOutputStateDetails( + const _Handle, const _State, int32_t*, int32_t*, int64_t*[], int64_t*[]) + # expectation value + int cutensornetCreateExpectation( + const _Handle, _State, _NetworkOperator, _StateExpectation*) + int cutensornetExpectationConfigure( + const _Handle, _StateExpectation, _ExpectationAttribute, const void*, size_t) + int cutensornetExpectationPrepare( + const _Handle, _StateExpectation, size_t, _WorkspaceDescriptor, Stream) + int cutensornetExpectationCompute( + const _Handle, _StateExpectation, _WorkspaceDescriptor, void*, void*, Stream) + int cutensornetDestroyExpectation(_StateExpectation) + # accessor + int cutensornetCreateAccessor( + const _Handle, _State, int32_t, const int32_t*, + const int64_t*, _StateAccessor*) + int cutensornetAccessorConfigure( + const _Handle, _StateAccessor, _AccessorAttribute, const void*, size_t) + int cutensornetAccessorPrepare( + const _Handle, _StateAccessor, size_t, _WorkspaceDescriptor, Stream) + int cutensornetAccessorCompute( + const _Handle, _StateAccessor, const int64_t*, _WorkspaceDescriptor, void*, void*, Stream) + int cutensornetDestroyAccessor(_StateAccessor) + # marginals int cutensornetCreateMarginal( const _Handle, _State, int32_t, const int32_t*, @@ -269,6 +300,16 @@ cdef extern from * nogil: _WorkspaceDescriptor, int64_t*, Stream) int cutensornetDestroySampler(_StateSampler) + # mps-specific + int cutensornetStateFinalizeMPS( + const _Handle handle, _State, _BoundaryCondition, const int64_t* const[], const int64_t* const[]) + + # network operator + int cutensornetCreateNetworkOperator( + const _Handle, int32_t, const int64_t[], DataType, _NetworkOperator*) + int cutensornetNetworkOperatorAppendProduct( + const _Handle, _NetworkOperator, cuDoubleComplex, int32_t, const int32_t[], const int32_t* const[], const int64_t* const[], const void* const[], int64_t*) + int cutensornetDestroyNetworkOperator(_NetworkOperator) class cuTensorNetError(RuntimeError): def __init__(self, status): @@ -2213,6 +2254,7 @@ cdef dict tensor_svd_cfg_sizes = { CUTENSORNET_TENSOR_SVD_CONFIG_S_NORMALIZATION: _numpy.int32, # = sizeof(enum value) CUTENSORNET_TENSOR_SVD_CONFIG_S_PARTITION: _numpy.int32, # = sizeof(enum value) CUTENSORNET_TENSOR_SVD_CONFIG_ALGO: _numpy.int32, # = sizeof(enum value) + CUTENSORNET_TENSOR_SVD_CONFIG_DISCARDED_WEIGHT_CUTOFF: _numpy.float64, } cdef dict svd_algo_params_sizes = { @@ -3003,7 +3045,7 @@ cpdef intptr_t create_marginal( cdef dict marginal_attribute_sizes = { - CUTENSORNET_MARGINAL_OPT_NUM_HYPER_SAMPLES: _numpy.int64 + CUTENSORNET_MARGINAL_OPT_NUM_HYPER_SAMPLES: _numpy.int32 } @@ -3030,8 +3072,7 @@ cpdef marginal_configure(intptr_t handle, intptr_t marginal, int attr, intptr_t handle (intptr_t): The library handle. marginal (intptr_t): The tensor network marginal computation handle. attr (MarginalAttribute): The attribute to configure. - buf (intptr_t): The pointer address (as Python :class:`int`) for storing - the returned attribute value. + buf (intptr_t): The pointer address (as Python :class:`int`) of the attribute value. size (size_t): The size of ``buf`` (in bytes). .. note:: To compute ``size``, use the itemsize of the corresponding data @@ -3161,7 +3202,7 @@ cpdef intptr_t create_sampler( cdef dict sampler_attribute_sizes = { - CUTENSORNET_SAMPLER_OPT_NUM_HYPER_SAMPLES: _numpy.int64 + CUTENSORNET_SAMPLER_OPT_NUM_HYPER_SAMPLES: _numpy.int32 } @@ -3266,6 +3307,767 @@ cpdef destroy_sampler(intptr_t sampler): check_status(status) +cdef dict state_attribute_sizes = { + CUTENSORNET_STATE_MPS_CANONICAL_CENTER: _numpy.int32, + CUTENSORNET_STATE_MPS_SVD_CONFIG_ABS_CUTOFF: _numpy.float64, + CUTENSORNET_STATE_MPS_SVD_CONFIG_REL_CUTOFF: _numpy.float64, + CUTENSORNET_STATE_MPS_SVD_CONFIG_S_NORMALIZATION: _numpy.int32, # = sizeof(enum value) + CUTENSORNET_STATE_MPS_SVD_CONFIG_ALGO: _numpy.int32, # = sizeof(enum value) + CUTENSORNET_STATE_MPS_SVD_CONFIG_DISCARDED_WEIGHT_CUTOFF: _numpy.float64, + CUTENSORNET_STATE_NUM_HYPER_SAMPLES: _numpy.int32 +} + + +cpdef state_get_attribute_dtype(int attr): + """Get the Python data type of the corresponding state attribute. + + Args: + attr (StateAttribute): The attribute to query. The enum CUTENSORNET_STATE_MPS_SVD_CONFIG_ALGO is not supported, + the dtype of which can be queried by :func:`tensor_svd_algo_params_get_dtype`. + + Returns: + The data type of the queried attribute. The returned dtype is always + a valid NumPy dtype object. + + .. note:: This API has no C counterpart and is a convenient helper for + allocating memory for :func:`state_configure`. + """ + if attr == CUTENSORNET_STATE_MPS_SVD_CONFIG_ALGO_PARAMS: + raise ValueError("For CUTENSORNET_STATE_MPS_SVD_CONFIG_ALGO_PARAMS, use `tensor_svd_algo_params_get_dtype` to get the dtype") + dtype = state_attribute_sizes[attr] + if attr == CUTENSORNET_STATE_MPS_SVD_CONFIG_S_NORMALIZATION: + if _numpy.dtype(dtype).itemsize != sizeof(_TensorSVDNormalization): + warnings.warn("binary size may be incompatible") + elif attr == CUTENSORNET_STATE_MPS_SVD_CONFIG_ALGO: + if _numpy.dtype(dtype).itemsize != sizeof(_TensorSVDAlgo): + warnings.warn("binary size may be incompatible") + return dtype + + +cpdef state_configure(intptr_t handle, intptr_t state, int attr, intptr_t buf, size_t size): + """Configures computation of the tensor network state. + + Args: + handle (intptr_t): The library handle. + state (intptr_t): The tensor network state. + attr (StateAttribute): The attribute to configure. + buf (intptr_t): The pointer address (as Python :class:`int`) for storing + the returned attribute value. + size (size_t): The size of ``buf`` (in bytes). + + .. note:: To compute ``size``, use the itemsize of the corresponding data + type, which can be queried using :func:`state_get_attribute_dtype`. + + .. seealso:: `cutensornetStateConfigure` + """ + with nogil: + status = cutensornetStateConfigure( + <_Handle>handle, <_State>state, + <_StateAttribute>attr, + buf, size) + check_status(status) + + +cpdef state_prepare( + intptr_t handle, intptr_t state, + size_t max_workspace_size_device, intptr_t workspace, intptr_t stream): + """Prepares computation of the tensor network state representation. + + Args: + handle (intptr_t): The library handle. + state (intptr_t): The tensor network state handle. + max_workspace_size_device (size_t): The maximal device workspace size (in bytes) allowed + for the mariginal computation. + workspace (intptr_t): The workspace descriptor. + stream (intptr_t): The CUDA stream handle (``cudaStream_t`` as Python + :class:`int`). + + .. seealso:: `cutensornetStatePrepare` + """ + with nogil: + status = cutensornetStatePrepare( + <_Handle>handle, <_State>state, + max_workspace_size_device, <_WorkspaceDescriptor>workspace, stream) + check_status(status) + + +cpdef tuple state_compute( + intptr_t handle, intptr_t state, intptr_t workspace, + state_tensors_out, intptr_t stream): + """Computes the tensor network state representation. + + Args: + handle (intptr_t): The library handle. + state (intptr_t): The tensor network state. + workspace (intptr_t): The workspace descriptor. + state_tensors_out: A host array of pointer addresses (as Python :class:`int`) for + each output tensor (on device). It can be + + - an :class:`int` as the pointer address to the array + - a Python sequence of :class:`int` + + stream (intptr_t): The CUDA stream handle (``cudaStream_t`` as Python + :class:`int`). + + Returns: + tuple: + The metadata of the output tensors: ``(extents_out, strides_out)``. + + .. seealso:: `cutensornetStateCompute` + """ + cdef int32_t num_tensors = 0 + with nogil: + status = cutensornetGetOutputStateDetails( + <_Handle>handle, <_State>state, + &num_tensors, NULL, NULL, NULL) + check_status(status) + + num_modes = _numpy.empty(num_tensors, dtype=_numpy.int32) + cdef int32_t* numModesPtr = num_modes.ctypes.data + with nogil: + status = cutensornetGetOutputStateDetails( + <_Handle>handle, <_State>state, + &num_tensors, numModesPtr, NULL, NULL) + check_status(status) + + extents_out_py = [_numpy.empty(num_modes[i], dtype=_numpy.int64) for i in range(num_tensors)] + strides_out_py = [_numpy.empty(num_modes[i], dtype=_numpy.int64) for i in range(num_tensors)] + + cdef vector[intptr_t] extentsOut + cdef vector[intptr_t] stridesOut + for i in range(num_tensors): + extentsOut.push_back(extents_out_py[i].ctypes.data) + stridesOut.push_back(strides_out_py[i].ctypes.data) + + cdef int64_t** extentsOutPtr = (extentsOut.data()) + cdef int64_t** stridesOutPtr = (stridesOut.data()) + + cdef vector[intptr_t] stateTensorsOutData + cdef void** stateTensorsOutPtr + if cpython.PySequence_Check(state_tensors_out): + stateTensorsOutData = state_tensors_out + stateTensorsOutPtr = (stateTensorsOutData.data()) + else: # a pointer address + stateTensorsOutPtr = state_tensors_out + + with nogil: + status = cutensornetStateCompute( + <_Handle>handle, <_State>state, <_WorkspaceDescriptor>workspace, + extentsOutPtr, stridesOutPtr, stateTensorsOutPtr, stream) + check_status(status) + return (extents_out_py, strides_out_py) + + +cpdef tuple get_output_state_details(intptr_t handle, intptr_t state): + """Get the output state tensors' metadata. + + Args: + handle (intptr_t): The library handle. + state (intptr_t): The tensor network state. + + Returns: + tuple: + The metadata of the output tensor: ``(num_tensors, num_modes, extents, + strides)``. + + .. seealso:: `cutensornetGetOutputStateDetails` + """ + cdef int32_t num_tensors = 0 + with nogil: + status = cutensornetGetOutputStateDetails( + <_Handle>handle, <_State>state, + &num_tensors, NULL, NULL, NULL) + check_status(status) + + num_modes = _numpy.empty(num_tensors, dtype=_numpy.int32) + cdef int32_t* numModesPtr = num_modes.ctypes.data + with nogil: + status = cutensornetGetOutputStateDetails( + <_Handle>handle, <_State>state, + &num_tensors, numModesPtr, NULL, NULL) + check_status(status) + extents_out_py = [_numpy.empty(num_modes[i], dtype=_numpy.int64) for i in range(num_tensors)] + strides_out_py = [_numpy.empty(num_modes[i], dtype=_numpy.int64) for i in range(num_tensors)] + + cdef vector[intptr_t] extentsOut + cdef vector[intptr_t] stridesOut + for i in range(num_tensors): + extentsOut.push_back(extents_out_py[i].ctypes.data) + stridesOut.push_back(strides_out_py[i].ctypes.data) + + cdef int64_t** extentsOutPtr = (extentsOut.data()) + cdef int64_t** stridesOutPtr = (stridesOut.data()) + with nogil: + status = cutensornetGetOutputStateDetails( + <_Handle>handle, <_State>state, + &num_tensors, NULL, extentsOutPtr, stridesOutPtr) + check_status(status) + return (num_tensors, num_modes, extents_out_py, strides_out_py) + +cpdef state_finalize_mps( + intptr_t handle, intptr_t state, int boundary_condition, extents_out, strides_out): + """Set the target MPS representation. + + Args: + handle (intptr_t): The library handle. + state (intptr_t): The tensor network state. + boundary_condition (BoundaryCondition): The boundary condition of the initial MPS state. + extents_out: A host array of extents for all target MPS tensors. It can be + + - an :class:`int` as the pointer address to the nested sequence + - a Python sequence of :class:`int`, each of which is a pointer address + to the corresponding tensor's extents + - a nested Python sequence of :class:`int` + + strides_out: A host array of strides for all target MPS tensors. It can be + + - an :class:`int` as the pointer address to the nested sequence + - a Python sequence of :class:`int`, each of which is a pointer address + to the corresponding tensor's strides + - a nested Python sequence of :class:`int` + + + .. seealso:: `cutensornetStateFinalizeMPS` + """ + # extents_out can be: + # - a plain pointer address + # - a Python sequence (of pointer addresses) + # - a nested Python sequence (of int64_t) + # Note: it cannot be a mix of sequences and ints. + cdef vector[intptr_t] extentsOutCData + cdef int64_t** extentsOutPtr + if is_nested_sequence(extents_out): + # flatten the 2D sequence + extentsOutPyData = [] + for i in extents_out: + # too bad a Python list can't hold C++ vectors, so we use NumPy + # arrays as the container here to keep data alive + data = _numpy.asarray(i, dtype=_numpy.int64) + assert data.ndim == 1 + extentsOutPyData.append(data) + extentsOutCData.push_back(data.ctypes.data) + extentsOutPtr = (extentsOutCData.data()) + elif cpython.PySequence_Check(extents_out): + # handle 1D sequence + extentsOutCData = extents_out + extentsOutPtr = (extentsOutCData.data()) + else: + # a pointer address, take it as is + extentsOutPtr = extents_out + + # strides_out can be: + # - a plain pointer address + # - a Python sequence (of pointer addresses) + # - a nested Python sequence (of int64_t) + # Note: it cannot be a mix of sequences and ints. + cdef vector[intptr_t] stridesOutCData + cdef int64_t** stridesOutPtr + if is_nested_sequence(strides_out): + # flatten the 2D sequence + stridesOutPyData = [] + for i in strides_out: + # too bad a Python list can't hold C++ vectors, so we use NumPy + # arrays as the container here to keep data alive + data = _numpy.asarray(i, dtype=_numpy.int64) + assert data.ndim == 1 + stridesOutPyData.append(data) + stridesOutCData.push_back(data.ctypes.data) + stridesOutPtr = (stridesOutCData.data()) + elif cpython.PySequence_Check(strides_out): + # handle 1D sequence + stridesOutCData = strides_out + stridesOutPtr = (stridesOutCData.data()) + else: + # a pointer address, take it as is + stridesOutPtr = strides_out + + with nogil: + status = cutensornetStateFinalizeMPS( + <_Handle>handle, <_State>state, <_BoundaryCondition>boundary_condition, + extentsOutPtr, stridesOutPtr) + check_status(status) + + +cpdef intptr_t create_network_operator( + intptr_t handle, int32_t n_state_modes, state_mode_extents, int data_type) except*: + """Create a tensor network operator of a given shape. + + Args: + handle (intptr_t): The library handle. + n_state_modes (int32_t): The total number of state modes the operator will act on. + state_mode_extents: A host array of extents of each state mode. It can be + + - an :class:`int` as the pointer address to the array + - a Python sequence of :class:`int` + + data_type (cuquantum.cudaDataType): The data type of the operator. + Returns: + intptr_t: An opaque tensor network operator handle (as Python :class:`int`). + + .. seealso:: `cutensornetCreateNetworkOperator` + """ + # state_mode_extents can be a pointer address, or a Python sequence + cdef vector[int64_t] stateModeExtentsData + cdef int64_t* stateModeExtentsPtr + if cpython.PySequence_Check(state_mode_extents): + if len(state_mode_extents) != n_state_modes: + raise ValueError("size of state_mode_extents not matching num_state_modes") + stateModeExtentsData = state_mode_extents + stateModeExtentsPtr = stateModeExtentsData.data() + else: # a pointer address + stateModeExtentsPtr = state_mode_extents + + cdef _NetworkOperator operator + with nogil: + status = cutensornetCreateNetworkOperator( + <_Handle>handle, n_state_modes, stateModeExtentsPtr, data_type + , &operator) + check_status(status) + return operator + + +cpdef int64_t network_operator_append_product( + intptr_t handle, intptr_t network_operator, coefficient, + int32_t num_tensors, num_modes, state_modes, tensor_mode_strides, + tensor_data) except*: + """Appends a tensor product component to the tensor network operator. + + Args: + handle (intptr_t): The library handle. + network_operator (intptr_t): The tensor network operator the product will be appended to. + coefficient: Complex coefficient associated with the appended operator component. + num_tensors: Number of tensor factors in the tensor product. + num_modes: A host array of number of state modes each appended tensor factor acts on. It can be + + - an :class:`int` as the pointer address to the array + - a Python sequence of :class:`int` + + state_modes: A host array of modes each appended tensor factor acts on (length = nModes). It can be + + - an :class:`int` as the pointer address to the nested sequence + - a Python sequence of :class:`int`, each of which is a pointer address + to the corresponding tensor's modes + - a nested Python sequence of :class:`int` + + tensor_modes_strides: Tensor mode strides for each tensor factor (length = nModes * 2). It can be + + - an :class:`int` as the pointer address to the nested sequence + - a Python sequence of :class:`int`, each of which is a pointer address + to the corresponding tensor's strides + - a nested Python sequence of :class:`int` + + tensor_data: A host array of pointer addresses (as Python :class:`int`) for + each tensor data (on device). It can be + + - an :class:`int` as the pointer address to the array + - a Python sequence of :class:`int` + + Returns: + int64_t: A unique sequential integer identifier of the appended tensor network operator component. + + .. seealso:: `cutensornetNetworkOperatorAppendProduct` + """ + # num_modes can be a pointer address, or a Python sequence + cdef vector[int32_t] numModesData + cdef const int32_t* numModesPtr + if cpython.PySequence_Check(num_modes): + numModesData = num_modes + numModesPtr = numModesData.data() + else: # a pointer address + numModesPtr = num_modes + + # state_modes can be: + # - a plain pointer address + # - a Python sequence (of pointer addresses) + # - a nested Python sequence (of int32_t) + # Note: it cannot be a mix of sequences and ints. + cdef vector[intptr_t] stateModesCData + cdef const int32_t** stateModesPtr + if is_nested_sequence(state_modes): + # flatten the 2D sequence + stateModesPyData = [] + for i in state_modes: + # too bad a Python list can't hold C++ vectors, so we use NumPy + # arrays as the container here to keep data alive + data = _numpy.asarray(i, dtype=_numpy.int32) + assert data.ndim == 1 + stateModesPyData.append(data) + stateModesCData.push_back(data.ctypes.data) + stateModesPtr = (stateModesCData.data()) + elif cpython.PySequence_Check(state_modes): + # handle 1D sequence + stateModesCData = state_modes + stateModesPtr = (stateModesCData.data()) + else: + # a pointer address, take it as is + stateModesPtr = state_modes + + # tensor_mode_strides can be: + # - a plain pointer address + # - a Python sequence (of pointer addresses) + # - a nested Python sequence (of int64_t) + # Note: it cannot be a mix of sequences and ints. + cdef vector[intptr_t] tensorModeStridesCData + cdef const int64_t** tensorModeStridesPtr + if is_nested_sequence(tensor_mode_strides): + # flatten the 2D sequence + tensorModeStridesPyData = [] + for i in tensor_mode_strides: + # too bad a Python list can't hold C++ vectors, so we use NumPy + # arrays as the container here to keep data alive + data = _numpy.asarray(i, dtype=_numpy.int64) + assert data.ndim == 1 + tensorModeStridesPyData.append(data) + tensorModeStridesCData.push_back(data.ctypes.data) + tensorModeStridesPtr = (tensorModeStridesCData.data()) + elif cpython.PySequence_Check(tensor_mode_strides): + # handle 1D sequence + tensorModeStridesCData = tensor_mode_strides + tensorModeStridesPtr = (tensorModeStridesCData.data()) + else: + # a pointer address, take it as is + tensorModeStridesPtr = tensor_mode_strides + + # tensor_data can be a pointer address, or a Python sequence + cdef vector[intptr_t] tensorDataData + cdef const void** tensorDataPtr + if cpython.PySequence_Check(tensor_data): + tensorDataData = tensor_data + tensorDataPtr = (tensorDataData.data()) + else: # a pointer address + tensorDataPtr = tensor_data + + cdef cuDoubleComplex coeff + coeff.x = coefficient.real + coeff.y = coefficient.imag + + cdef int64_t componentId = 0 + with nogil: + status = cutensornetNetworkOperatorAppendProduct( + <_Handle>handle, <_NetworkOperator>network_operator, + coeff, num_tensors, numModesPtr, stateModesPtr, + tensorModeStridesPtr, tensorDataPtr + , &componentId) + check_status(status) + return componentId + + +cpdef intptr_t create_accessor( + intptr_t handle, intptr_t state, + int32_t n_projected_modes, projected_modes, amplitudes_tensor_strides) except*: + """Create a representation for the tensor network state accessor. + + Args: + handle (intptr_t): The library handle. + state (intptr_t): The tensor network state. + n_projected_modes (int32_t): The number of modes that are projected out for the state. + projected_modes: A host array of projected modes for the marginal. It can be + + - an :class:`int` as the pointer address to the array + - a Python sequence of :class:`int` + + amplitudes_tensor_strides: A host array of strides for the amplitudes tensor. It can be + + - an :class:`int` as the pointer address to the array + - a Python sequence of :class:`int` + + Returns: + intptr_t: An opaque tensor network state accessor handle (as Python :class:`int`). + + .. seealso:: `cutensornetCreateAccessor` + """ + + # projected_modes can be a pointer address, or a Python sequence + cdef vector[int32_t] projectedModesData + cdef int32_t* projectedModesPtr + if cpython.PySequence_Check(projected_modes): + if len(projected_modes) != n_projected_modes: + raise ValueError("size of projected_modes not matching n_projected_modes") + projectedModesData = projected_modes + projectedModesPtr = projectedModesData.data() + else: # a pointer address + projectedModesPtr = projected_modes + + # amplitudes_tensor_strides can be a pointer address, or a Python sequence + cdef vector[int64_t] amplitudesTensorStridesData + cdef int64_t* amplitudesTensorStridesPtr + if cpython.PySequence_Check(amplitudes_tensor_strides): + amplitudesTensorStridesData = amplitudes_tensor_strides + amplitudesTensorStridesPtr = amplitudesTensorStridesData.data() + else: # a pointer address + amplitudesTensorStridesPtr = amplitudes_tensor_strides + + cdef _StateAccessor accessor + with nogil: + status = cutensornetCreateAccessor( + <_Handle>handle, <_State>state, + n_projected_modes, projectedModesPtr, + amplitudesTensorStridesPtr, &accessor) + check_status(status) + return accessor + + +cdef dict accessor_attribute_sizes = { + CUTENSORNET_ACCESSOR_OPT_NUM_HYPER_SAMPLES: _numpy.int32 +} + + +cpdef accessor_get_attribute_dtype(int attr): + """Get the Python data type of the corresponding accessor attribute. + + Args: + attr (AccessorAttribute): The attribute to query. + + Returns: + The data type of the queried attribute. The returned dtype is always + a valid NumPy dtype object. + + .. note:: This API has no C counterpart and is a convenient helper for + allocating memory for :func:`accessor_configure`. + """ + return accessor_attribute_sizes[attr] + + +cpdef accessor_configure(intptr_t handle, intptr_t accessor, int attr, intptr_t buf, size_t size): + """Configures computation of the tensor network state accessor. + + Args: + handle (intptr_t): The library handle. + accessor (intptr_t): The tensor network state accessor computation handle. + attr (AccessorAttribute): The attribute to configure. + buf (intptr_t): The pointer address (as Python :class:`int`) for storing + the returned attribute value. + size (size_t): The size of ``buf`` (in bytes). + + .. note:: To compute ``size``, use the itemsize of the corresponding data + type, which can be queried using :func:`accessor_get_attribute_dtype`. + + .. seealso:: `cutensornetAccessorConfigure` + """ + with nogil: + status = cutensornetAccessorConfigure( + <_Handle>handle, <_StateAccessor>accessor, + <_AccessorAttribute>attr, + buf, size) + check_status(status) + + +cpdef accessor_prepare( + intptr_t handle, intptr_t accessor, + size_t max_workspace_size_device, intptr_t workspace, intptr_t stream): + """Prepares computation of the tensor network state accessor. + + Args: + handle (intptr_t): The library handle. + accessor (intptr_t): The tensor network state accessor handle. + max_workspace_size_device (size_t): The maximal device workspace size (in bytes) allowed + for the accessor computation. + workspace (intptr_t): The workspace descriptor. + stream (intptr_t): The CUDA stream handle (``cudaStream_t`` as Python + :class:`int`). + + .. seealso:: `cutensornetAccessorPrepare` + """ + with nogil: + status = cutensornetAccessorPrepare( + <_Handle>handle, <_StateAccessor>accessor, + max_workspace_size_device, <_WorkspaceDescriptor>workspace, stream) + check_status(status) + + +cpdef accessor_compute( + intptr_t handle, intptr_t accessor, projected_mode_values, + intptr_t workspace, intptr_t amplitudes_tensor, intptr_t state_norm, intptr_t stream): + """Computes the tensor network state amplitudes. + + Args: + handle (intptr_t): The library handle. + accessor (intptr_t): The tensor network state accessor handle. + projected_mode_values: A host array of values for the projected modes. It can be + + - an :class:`int` as the pointer address to the array + - a Python sequence of :class:`int` + + workspace (intptr_t): The workspace descriptor. + amplitudes_tensor (intptr_t): The pointer address (as Python :class:`int`) for storing + the computed amplitudes. + state_norm (intptr_t): The pointer address (as Python :class:`int`) for storing + the 2-norm of the underlying state. If set to 0 (`NULL` pointer), the norm calculation will be ignored. + stream (intptr_t): The CUDA stream handle (``cudaStream_t`` as Python + :class:`int`). + + .. seealso:: `cutensornetAccessorCompute` + """ + # projected_mode_values can be a pointer address, or a Python sequence + cdef vector[int64_t] projectedModeValuesData + cdef int64_t* projectedModeValuesPtr + if cpython.PySequence_Check(projected_mode_values): + projectedModeValuesData = projected_mode_values + projectedModeValuesPtr = projectedModeValuesData.data() + else: # a pointer address + projectedModeValuesPtr = projected_mode_values + + with nogil: + status = cutensornetAccessorCompute( + <_Handle>handle, <_StateAccessor>accessor, + projectedModeValuesPtr, <_WorkspaceDescriptor>workspace, + amplitudes_tensor, state_norm, stream) + check_status(status) + + +cpdef destroy_accessor(intptr_t accessor): + """Destroy a tensor network state accessor handle. + + Args: + marginal (intptr_t): The tensor network state accessor handle. + + .. seealso:: `cutensornetDestroyAccessor` + """ + with nogil: + status = cutensornetDestroyAccessor(<_StateAccessor>accessor) + check_status(status) + + +cpdef destroy_network_operator(intptr_t network_operator): + """Destroy a tensor network operator. + + Args: + network_operator (intptr_t): The tensor network operator. + + .. seealso:: `cutensornetDestroyNetworkOperator` + """ + with nogil: + status = cutensornetDestroyNetworkOperator(<_NetworkOperator>network_operator) + check_status(status) + + +cpdef intptr_t create_expectation( + intptr_t handle, intptr_t state, intptr_t operator) except*: + """Create a representation for the tensor network state expectation value. + + Args: + handle (intptr_t): The library handle. + state (intptr_t): The tensor network state. + + Returns: + intptr_t: An opaque tensor network state expectation handle (as Python :class:`int`). + + .. seealso:: `cutensornetCreateExpectation` + """ + cdef _StateExpectation expectation + with nogil: + status = cutensornetCreateExpectation( + <_Handle>handle, <_State>state, <_NetworkOperator>operator + , &expectation) + check_status(status) + return expectation + + +cdef dict expectation_attribute_sizes = { + CUTENSORNET_EXPECTATION_OPT_NUM_HYPER_SAMPLES: _numpy.int32 +} + + +cpdef expectation_get_attribute_dtype(int attr): + """Get the Python data type of the corresponding expectation attribute. + + Args: + attr (ExpectationAttribute): The attribute to query. + + Returns: + The data type of the queried attribute. The returned dtype is always + a valid NumPy dtype object. + + .. note:: This API has no C counterpart and is a convenient helper for + allocating memory for :func:`expectation_configure`. + """ + return expectation_attribute_sizes[attr] + + +cpdef expectation_configure(intptr_t handle, intptr_t expectation, int attr, intptr_t buf, size_t size): + """Configures computation of the tensor network state expectation value. + + Args: + handle (intptr_t): The library handle. + expectation (intptr_t): The tensor network expectation computation handle. + attr (ExpectationAttribute): The attribute to configure. + buf (intptr_t): The pointer address (as Python :class:`int`) of the attribute value. + size (size_t): The size of ``buf`` (in bytes). + + .. note:: To compute ``size``, use the itemsize of the corresponding data + type, which can be queried using :func:`expectation_get_attribute_dtype`. + + .. seealso:: `cutensornetExpectationConfigure` + """ + with nogil: + status = cutensornetExpectationConfigure( + <_Handle>handle, <_StateExpectation>expectation, + <_ExpectationAttribute>attr, + buf, size) + check_status(status) + + +cpdef expectation_prepare( + intptr_t handle, intptr_t expectation, + size_t max_workspace_size_device, intptr_t workspace, intptr_t stream): + """Prepares computation of the tensor network state expectation. + + Args: + handle (intptr_t): The library handle. + expectation (intptr_t): The tensor network expectation computation handle. + max_workspace_size_device (size_t): The maximal device workspace size (in bytes) allowed + for the expectation value computation. + workspace (intptr_t): The workspace descriptor. + stream (intptr_t): The CUDA stream handle (``cudaStream_t`` as Python + :class:`int`). + + .. seealso:: `cutensornetExpectationPrepare` + """ + with nogil: + status = cutensornetExpectationPrepare( + <_Handle>handle, <_StateExpectation>expectation, + max_workspace_size_device, <_WorkspaceDescriptor>workspace, stream) + check_status(status) + + +cpdef expectation_compute( + intptr_t handle, intptr_t expectation, + intptr_t workspace, intptr_t expectation_value, intptr_t state_norm, intptr_t stream): + """Computes the tensor network state expectation value. + + Args: + handle (intptr_t): The library handle. + expectation (intptr_t): The tensor network expectation computation handle. + workspace (intptr_t): The workspace descriptor. + expectation_value (intptr_t): The pointer address (as Python :class:`int`) for storing + the computed expectation_value (stored on host). + state_norm (intptr_t): The pointer address (as Python :class:`int`) for storing + the 2-norm of the underlying state. If set to 0 (`NULL` pointer), the norm calculation will be ignored. + stream (intptr_t): The CUDA stream handle (``cudaStream_t`` as Python + :class:`int`). + + .. seealso:: `cutensornetExpectationCompute` + """ + with nogil: + status = cutensornetExpectationCompute( + <_Handle>handle, <_StateExpectation>expectation, + <_WorkspaceDescriptor>workspace, expectation_value, state_norm, stream) + check_status(status) + + +cpdef destroy_expectation(intptr_t expectation): + """Destroy a tensor network expectation value representation. + + Args: + expectation (intptr_t): The tensor network expectation value representation. + + .. seealso:: `cutensornetDestroyExpectation` + """ + with nogil: + status = cutensornetDestroyExpectation(<_StateExpectation>expectation) + check_status(status) + + class NetworkAttribute(IntEnum): """See `cutensornetNetworkAttributes_t`.""" INPUT_TENSORS_NUM_CONSTANT = CUTENSORNET_NETWORK_INPUT_TENSORS_NUM_CONSTANT @@ -3360,6 +4162,7 @@ class TensorSVDConfigAttribute(IntEnum): S_PARTITION = CUTENSORNET_TENSOR_SVD_CONFIG_S_PARTITION ALGO = CUTENSORNET_TENSOR_SVD_CONFIG_ALGO ALGO_PARAMS = CUTENSORNET_TENSOR_SVD_CONFIG_ALGO_PARAMS + DISCARDED_WEIGHT_CUTOFF = CUTENSORNET_TENSOR_SVD_CONFIG_DISCARDED_WEIGHT_CUTOFF class TensorSVDNormalization(IntEnum): """See `cutensornetTensorSVDNormalization_t`.""" @@ -3407,6 +4210,29 @@ class SamplerAttribute(IntEnum): """See `cutensornetSamplerAttributes_t`.""" OPT_NUM_HYPER_SAMPLES = CUTENSORNET_SAMPLER_OPT_NUM_HYPER_SAMPLES +class AccessorAttribute(IntEnum): + """See `cutensornetAccessorAttributes_t`.""" + OPT_NUM_HYPER_SAMPLES = CUTENSORNET_ACCESSOR_OPT_NUM_HYPER_SAMPLES + +class ExpectationAttribute(IntEnum): + """See `cutensornetExpectationAttributes_t`.""" + OPT_NUM_HYPER_SAMPLES = CUTENSORNET_EXPECTATION_OPT_NUM_HYPER_SAMPLES + +class BoundaryCondition(IntEnum): + """See `cutensornetBoundaryCondition_t`.""" + OPEN = CUTENSORNET_BOUNDARY_CONDITION_OPEN + +class StateAttribute(IntEnum): + """See `cutensornetStateAttributes_t`.""" + MPS_CANONICAL_CENTER = CUTENSORNET_STATE_MPS_CANONICAL_CENTER + MPS_SVD_CONFIG_ABS_CUTOFF = CUTENSORNET_STATE_MPS_SVD_CONFIG_ABS_CUTOFF + MPS_SVD_CONFIG_REL_CUTOFF = CUTENSORNET_STATE_MPS_SVD_CONFIG_REL_CUTOFF + MPS_SVD_CONFIG_S_NORMALIZATION = CUTENSORNET_STATE_MPS_SVD_CONFIG_S_NORMALIZATION + MPS_SVD_CONFIG_ALGO = CUTENSORNET_STATE_MPS_SVD_CONFIG_ALGO + MPS_SVD_CONFIG_ALGO_PARAMS = CUTENSORNET_STATE_MPS_SVD_CONFIG_ALGO_PARAMS + MPS_SVD_CONFIG_DISCARDED_WEIGHT_CUTOFF = CUTENSORNET_STATE_MPS_SVD_CONFIG_DISCARDED_WEIGHT_CUTOFF + NUM_HYPER_SAMPLES = CUTENSORNET_STATE_NUM_HYPER_SAMPLES + del IntEnum diff --git a/python/cuquantum/cutensornet/experimental/tensor_network.py b/python/cuquantum/cutensornet/experimental/tensor_network.py index 9c8c450..00fae61 100644 --- a/python/cuquantum/cutensornet/experimental/tensor_network.py +++ b/python/cuquantum/cutensornet/experimental/tensor_network.py @@ -44,24 +44,24 @@ def _gate_split(wrapped_operands, inputs, outputs, size_dict, max_mid_extent, al # placeholder to help avoid resource leak handle = workspace_desc = svd_config = svd_info = None input_tensor_descriptors = output_tensor_descriptors = [] + workspaces = dict() + own_handle = False try: # Options converted to an internal option - wrapped_operands, options, own_handle, operands_location = decomposition_utils.parse_decompose_operands_options( - options, wrapped_operands, allowed_dtype_names=decomposition_utils.DECOMPOSITION_DTYPE_NAMES) + wrapped_operands, options, own_handle, operands_location, stream_holder = decomposition_utils.parse_decompose_operands_options( + options, wrapped_operands, stream, allowed_dtype_names=decomposition_utils.DECOMPOSITION_DTYPE_NAMES) mid_extent = max_mid_extent if algorithm.svd_method.max_extent is None else min(max_mid_extent, algorithm.svd_method.max_extent) handle = options.handle - - package = utils.infer_object_package(wrapped_operands[0].tensor) - stream, stream_ctx, stream_ptr = utils.get_or_create_stream(options.device_id, stream, package) + stream_ptr = stream_holder.ptr # this exists as we always use the ExternalStream from CuPy internally... options.logger.info("Calling specicialized kernel `cutensornetGateSplit` for contraction and decomposition.") # Create input/output tensor descriptors and empty output operands input_tensor_descriptors, output_operands, output_tensor_descriptors, s, s_ptr = decomposition_utils.create_operands_and_descriptors( handle, wrapped_operands, size_dict, inputs, outputs, - mid_extent, algorithm.svd_method, options.device_id, stream_ctx, options.logger) + mid_extent, algorithm.svd_method, options.device_id, stream_holder, options.logger) # Parse SVDConfig svd_config = cutn.create_tensor_svd_config(handle) @@ -75,17 +75,16 @@ def _gate_split(wrapped_operands, inputs, outputs, size_dict, max_mid_extent, al workspace_ptr = None options.logger.debug("Querying workspace size...") - + cutn.workspace_compute_gate_split_sizes(handle, *input_tensor_descriptors, *output_tensor_descriptors, gate_algorithm, svd_config, options.compute_type, workspace_desc) - + # Allocate and set workspace - workspaces = dict() for mem_space in (cutn.Memspace.DEVICE, cutn.Memspace.HOST): workspaces[mem_space] = decomposition_utils.allocate_and_set_workspace(handle, options.allocator, workspace_desc, cutn.WorksizePref.MIN, mem_space, cutn.WorkspaceKind.SCRATCH, options.device_id, - stream, stream_ctx, options.logger, task_name='contract decomposition') + stream_holder, options.logger, task_name='contract decomposition') options.logger.info("Starting contract-decompose (gate split)...") timing = bool(options.logger and options.logger.handlers) @@ -96,7 +95,7 @@ def _gate_split(wrapped_operands, inputs, outputs, size_dict, max_mid_extent, al options.logger.info("This call is non-blocking and will return immediately after the operation is launched on the device.") svd_info = cutn.create_tensor_svd_info(handle) - with utils.device_ctx(options.device_id), utils.cuda_call_ctx(stream, blocking, timing) as (last_compute_event, elapsed): + with utils.device_ctx(options.device_id), utils.cuda_call_ctx(stream_holder, blocking, timing) as (last_compute_event, elapsed): cutn.gate_split(handle, input_tensor_descriptors[0], wrapped_operands[0].data_ptr, input_tensor_descriptors[1], wrapped_operands[1].data_ptr, @@ -126,8 +125,8 @@ def _gate_split(wrapped_operands, inputs, outputs, size_dict, max_mid_extent, al s.tensor = s.tensor[:reduced_extent] finally: # when host workspace is allocated, synchronize stream before return - if workspaces[cutn.Memspace.HOST] is not None: - stream.synchronize() + if workspaces.get(cutn.Memspace.HOST) is not None: + stream_holder.obj.synchronize() # Free resources decomposition_utils._destroy_tensor_descriptors(input_tensor_descriptors) decomposition_utils._destroy_tensor_descriptors(output_tensor_descriptors) @@ -141,7 +140,7 @@ def _gate_split(wrapped_operands, inputs, outputs, size_dict, max_mid_extent, al if own_handle and handle is not None: cutn.destroy(handle) - u, v, s = [decomposition_utils.get_return_operand_data(t, operands_location) for t in output_operands + [s, ]] + u, v, s = [decomposition_utils.get_return_operand_data(t, operands_location, stream_holder) for t in output_operands + [s, ]] if return_info: info = ContractDecomposeInfo(qr_method=algorithm.qr_method, @@ -288,9 +287,15 @@ def contract_decompose(subscripts, *operands, algorithm=None, options=None, opti algorithm = utils.check_or_create_options(ContractDecomposeAlgorithm, algorithm, "Contract Decompose Algorithm") options = utils.check_or_create_options(NetworkOptions, options, "Network Options") + # Get cuTensorNet version (as seen at run-time) + cutn_ver = cutn.get_version() + cutn_major = cutn_ver // 10000 + cutn_minor = (cutn_ver % 10000) // 100 + cutn_patch = cutn_ver % 100 + + # Logger logger = logging.getLogger() if options.logger is None else options.logger - logger.info(f"CUDA runtime version = {cutn.get_cudart_version()}") - logger.info(f"cuTensorNet version = {cutn.MAJOR_VER}.{cutn.MINOR_VER}.{cutn.PATCH_VER}") + logger.info(f"cuTensorNet version = {cutn_major}.{cutn_minor}.{cutn_patch}") logger.info("Beginning operands parsing...") # Parse subscipts and operands @@ -299,11 +304,12 @@ def contract_decompose(subscripts, *operands, algorithm=None, options=None, opti if is_gate_split(inputs, outputs, algorithm): # dedicated kernel for GateSplit problem return _gate_split(wrapped_operands, inputs, outputs, size_dict, max_mid_extent, algorithm, options, stream, return_info) - + + own_handle = False try: # contraction followed by decomposition - wrapped_operands, options, own_handle, operands_location = decomposition_utils.parse_decompose_operands_options( - options, wrapped_operands, allowed_dtype_names=decomposition_utils.DECOMPOSITION_DTYPE_NAMES) + wrapped_operands, options, own_handle, operands_location, stream_holder = decomposition_utils.parse_decompose_operands_options( + options, wrapped_operands, stream, allowed_dtype_names=decomposition_utils.DECOMPOSITION_DTYPE_NAMES) intermediate_modes = einsum_parser.infer_output_mode_labels(outputs) @@ -348,17 +354,21 @@ def contract_decompose(subscripts, *operands, algorithm=None, options=None, opti logger.info("Beginning decomposition of the intermediate tensor...") if algorithm.qr_method and algorithm.svd_method is False: # contract and QR decompose - results = decompose(decompose_subscripts, intm_output, method=algorithm.qr_method, options=dataclasses.asdict(options), stream=stream, return_info=False) + results = decompose( + decompose_subscripts, intm_output, method=algorithm.qr_method, options=dataclasses.asdict(options), + stream=stream, return_info=False) results = maybe_truncate_qr_output_operands(results, outputs, max_mid_extent) if operands_location == 'cpu': - results = [tensor_wrapper.wrap_operand(o).to('cpu') for o in results] + results = [tensor_wrapper.wrap_operand(o).to('cpu', stream_holder=stream_holder) for o in results] elif algorithm.svd_method and algorithm.qr_method is False: # contract and SVD decompose use_max_mid_extent = algorithm.svd_method.max_extent is None if use_max_mid_extent: algorithm.svd_method.max_extent = max_mid_extent - results = decompose(decompose_subscripts, intm_output, method=algorithm.svd_method, options=dataclasses.asdict(options), stream=stream, return_info=return_info) + results = decompose( + decompose_subscripts, intm_output, method=algorithm.svd_method, options=dataclasses.asdict(options), + stream=stream, return_info=return_info) if use_max_mid_extent: # revert back algorithm.svd_method.max_extent = None @@ -366,7 +376,9 @@ def contract_decompose(subscripts, *operands, algorithm=None, options=None, opti if return_info: results, info_dict['svd_info'] = results[:-1], results[-1] if operands_location == 'cpu': - results = [o if o is None else tensor_wrapper.wrap_operand(o).to('cpu') for o in results] + results = [o if o is None else tensor_wrapper.wrap_operand(o).to( + 'cpu', stream_holder=stream_holder) + for o in results] else: raise NotImplementedError("contract_decompose currently doesn't support QR assisted SVD contract decomposition for more than 3 operands") logger.info("Decomposition of the intermediate tensor is completed.") diff --git a/python/cuquantum/cutensornet/tensor.py b/python/cuquantum/cutensornet/tensor.py index 52f13d3..b6f5fc0 100644 --- a/python/cuquantum/cutensornet/tensor.py +++ b/python/cuquantum/cutensornet/tensor.py @@ -230,13 +230,15 @@ def decompose( # placeholder to help avoid resource leak handle = workspace_desc = svd_config = svd_info = None input_descriptors = output_descriptors = [] - + workspaces = dict() + own_handle = False try: # wrap operands to be consistent with options. # options is a new instance of DecompositionOptions with all entries initialized - wrapped_operands, options, own_handle, operands_location = decomposition_utils.parse_decompose_operands_options(options, - wrapped_operands, allowed_dtype_names=decomposition_utils.DECOMPOSITION_DTYPE_NAMES) + wrapped_operands, options, own_handle, operands_location, stream_holder = decomposition_utils.parse_decompose_operands_options( + options, wrapped_operands, stream, allowed_dtype_names=decomposition_utils.DECOMPOSITION_DTYPE_NAMES) handle = options.handle + stream_ptr = stream_holder.ptr # this exists as we always use the ExternalStream from CuPy internally... if isinstance(method, QRMethod): mid_extent = max_mid_extent @@ -248,10 +250,8 @@ def decompose( raise ValueError("method must be either SVDMethod or QRMethod") # # Create input/output tensor descriptors and empty output operands - package = utils.infer_object_package(wrapped_operands[0].tensor) - stream, stream_ctx, stream_ptr = utils.get_or_create_stream(options.device_id, stream, package) - input_descriptors, output_operands, output_descriptors, s, s_ptr = decomposition_utils.create_operands_and_descriptors(options.handle, - wrapped_operands, size_dict, inputs, outputs, mid_extent, method, options.device_id, stream_ctx, options.logger) + input_descriptors, output_operands, output_descriptors, s, s_ptr = decomposition_utils.create_operands_and_descriptors( + options.handle, wrapped_operands, size_dict, inputs, outputs, mid_extent, method, options.device_id, stream_holder, options.logger) # Create workspace descriptor workspace_desc = cutn.create_workspace_descriptor(handle) @@ -270,11 +270,10 @@ def decompose( ValueError("method must be either a QRMethod/SVDMethod object or a dict that can be used to construct QRMethod/SVDMethod") # Allocate and set workspace - workspaces = dict() for mem_space in (cutn.Memspace.DEVICE, cutn.Memspace.HOST): workspaces[mem_space] = decomposition_utils.allocate_and_set_workspace(handle, options.allocator, workspace_desc, - cutn.WorksizePref.MIN, mem_space, cutn.WorkspaceKind.SCRATCH, options.device_id, - stream, stream_ctx, options.logger, task_name='tensor decomposition') + cutn.WorksizePref.MIN, mem_space, cutn.WorkspaceKind.SCRATCH, options.device_id, + stream_holder, options.logger, task_name='tensor decomposition') svd_info_obj = None @@ -287,7 +286,7 @@ def decompose( logger.info("This call is non-blocking and will return immediately after the operation is launched on the device.") timing = bool(logger and logger.handlers) if isinstance(method, QRMethod): - with utils.device_ctx(options.device_id), utils.cuda_call_ctx(stream, blocking, timing) as (last_compute_event, elapsed): + with utils.device_ctx(options.device_id), utils.cuda_call_ctx(stream_holder, blocking, timing) as (last_compute_event, elapsed): cutn.tensor_qr(handle, *input_descriptors, wrapped_operands[0].data_ptr, output_descriptors[0], output_operands[0].data_ptr, @@ -298,7 +297,7 @@ def decompose( logger.info(f"The QR decomposition took {elapsed.data:.3f} ms to complete.") elif isinstance(method, SVDMethod): svd_info = cutn.create_tensor_svd_info(handle) - with utils.device_ctx(options.device_id), utils.cuda_call_ctx(stream, blocking, timing) as (last_compute_event, elapsed): + with utils.device_ctx(options.device_id), utils.cuda_call_ctx(stream_holder, blocking, timing) as (last_compute_event, elapsed): cutn.tensor_svd(handle, *input_descriptors, wrapped_operands[0].data_ptr, output_descriptors[0], output_operands[0].data_ptr, @@ -318,8 +317,8 @@ def decompose( s.tensor = s.tensor[:reduced_extent] finally: # when host workspace is allocated, synchronize stream before return - if workspaces[cutn.Memspace.HOST] is not None: - stream.synchronize() + if workspaces.get(cutn.Memspace.HOST) is not None: + stream_holder.obj.synchronize() # Free resources if svd_config is not None: cutn.destroy_tensor_svd_config(svd_config) @@ -335,7 +334,9 @@ def decompose( cutn.destroy(handle) logger.info(f"All resources for the decomposition are freed.") - left_output, right_output, s = [decomposition_utils.get_return_operand_data(o, operands_location) for o in output_operands + [s, ]] + left_output, right_output, s = [decomposition_utils.get_return_operand_data( + o, operands_location, stream_holder) + for o in output_operands + [s, ]] if isinstance(method, QRMethod): return left_output, right_output @@ -407,6 +408,7 @@ class SVDMethod: max_extent: Keep no more than the largest ``max_extent`` singular values in the output operands (the rest will be truncated). abs_cutoff: Singular values below this value will be trimmed in the output operands. rel_cutoff: Singular values below the product of this value and the largest singular value will be trimmed in the output operands. + discarded_weight_cutoff: Singular values with discarded weight (square sum dividied by total square sum) below this value will be trimmed. partition: Singular values S will be explictly returned by default (``partition=None``). Alternatively, singular values may be factorized onto output tensor U (``partition="U"``), output tensor V (``partition="V"``) or equally onto output tensor U and output tensor V (``partition="UV"``). When any of these three partition schemes is selected, @@ -420,6 +422,10 @@ class SVDMethod: gesvdr_oversampling: The size of oversampling when ``algorithm`` is set to ``"gesvdr"``. Default 0 denotes the lower of 4 times ``max_extent`` and the difference between full rank and ``max_extent``. gesvdr_niters: The number of iteration of power method when ``algorithm`` is set to ``"gesvdr"`` and the default (0) is 10. + .. note:: + + If multiple truncation paramters are set, e.g, ``max_extent`` and ``discarded_weight_cutoff``, the truncated extent will be determined as the lowest among all. + .. note:: For detailed explanation on the different SVD algorithms and the corresponding parameters, @@ -434,6 +440,7 @@ class SVDMethod: max_extent: Optional[int] = None abs_cutoff: Optional[float] = 0.0 rel_cutoff: Optional[float] = 0.0 + discarded_weight_cutoff: Optional[float] = 0.0 partition: Optional[str] = None normalization: Optional[str] = None algorithm: Optional[str] = 'gesvd' @@ -459,6 +466,7 @@ def __str__(self): Maxmial number of singular values = {self.max_extent} Absolute value cutoff = {self.abs_cutoff} Relative value cutoff = {self.rel_cutoff} + Discarded weight cutoff = {self.discarded_weight_cutoff} Singular values partition = {self.partition} Singular values normalization = {self.normalization}""" @@ -473,6 +481,9 @@ def __post_init__(self): if (self.gesvdr_oversampling !=0 or self.gesvdr_niters !=0) and self.algorithm != 'gesvdr': raise ValueError(f"gesvdr_oversample and gesvdr_niters can only be set when algorithm is set to gesvdr, found algorithm {self.algorithm}") + + if self.algorithm == 'gesvdr' and self.discarded_weight_cutoff != 0 and self.max_extent is not None: + raise ValueError("Discarded weight truncation is not supported for gesvdr algorithm with fixed extent truncation") def _get_algo_params(self): initialized = False diff --git a/python/cuquantum/cutensornet/tensor_network.py b/python/cuquantum/cutensornet/tensor_network.py index c6f4528..19613dc 100644 --- a/python/cuquantum/cutensornet/tensor_network.py +++ b/python/cuquantum/cutensornet/tensor_network.py @@ -11,6 +11,7 @@ import collections import dataclasses import logging +import warnings import cupy as cp import numpy as np @@ -20,6 +21,7 @@ from . import memory from ._internal import einsum_parser from ._internal import formatters +from ._internal import grad_torch from ._internal import optimizer_ifc from ._internal import tensor_wrapper from ._internal import typemaps @@ -33,7 +35,7 @@ class InvalidNetworkState(Exception): class Network: """ - Network(subscripts, *operands, options=None) + Network(subscripts, *operands, qualifiers=None, options=None, stream=None) Create a tensor network object specified as an Einstein summation expression. @@ -78,6 +80,9 @@ class Network: options: Specify options for the tensor network as a :class:`~cuquantum.NetworkOptions` object. Alternatively, a `dict` containing the parameters for the ``NetworkOptions`` constructor can also be provided. If not specified, the value will be set to the default-constructed ``NetworkOptions`` object. + stream: Provide the CUDA stream to use for network construction, which is needed for stream-ordered operations such as allocating memory. Acceptable inputs include ``cudaStream_t`` (as + Python :class:`int`), :class:`cupy.cuda.Stream`, and :class:`torch.cuda.Stream`. If a stream is not provided, the + current stream will be used. See Also: :meth:`~Network.contract_path`, :meth:`autotune`, :meth:`~Network.contract`, :meth:`reset_operands` @@ -98,28 +103,28 @@ class Network: Create a :class:`Network` object: - >>> n = Network(expr, *operands) + >>> tn = Network(expr, *operands) Find the best contraction order: - >>> path, info = n.contract_path({'samples': 500}) + >>> path, info = tn.contract_path({'samples': 500}) Autotune the network: - >>> n.autotune(iterations=5) + >>> tn.autotune(iterations=5) Perform the contraction. The result is of the same type and on the same device as the operands: - >>> r1 = n.contract() + >>> r1 = tn.contract() Reset operands to new values: >>> operands = [i*operand for i, operand in enumerate(operands, start=1)] - >>> n.reset_operands(*operands) + >>> tn.reset_operands(*operands) Get the result of the new contraction: - >>> r2 = n.contract() + >>> r2 = tn.contract() >>> from math import factorial >>> np.allclose(r2, factorial(len(operands))*r1) True @@ -128,7 +133,7 @@ class Network: network is large) since the memory will be released only when the object goes out of scope. (*To avoid having to explicitly make this call, it is recommended to use the* :class:`Network` *object as a context manager*.) - >>> n.free() + >>> tn.free() If the operands are on the GPU, they can also be updated using in-place operations. In this case, the call to :meth:`reset_operands` can be skipped -- subsequent :meth:`~Network.contract` calls will use the same @@ -140,19 +145,19 @@ class Network: >>> shapes = [(8, 2, 5), (5, 7), (8, 8, 2, 5), (8, 6, 3), (8,), (6,), (5,), (6, 5, 5, 7), (6, 3), (3,)] >>> operands = [cp.random.rand(*shape) for shape in shapes] >>> - >>> with Network(expr, *operands) as n: - ... path, info = n.contract_path({'samples': 500}) - ... n.autotune(iterations=5) + >>> with Network(expr, *operands) as tn: + ... path, info = tn.contract_path({'samples': 500}) + ... tn.autotune(iterations=5) ... ... # Perform the contraction - ... r1 = n.contract() + ... r1 = tn.contract() ... ... # Update the operands in place ... for i, operand in enumerate(operands, start=1): ... operand *= i ... ... # Perform the contraction with the updated operand values - ... r2 = n.contract() + ... r2 = tn.contract() ... ... # The resources used by the network are automatically released when the context ends. >>> @@ -162,34 +167,83 @@ class Network: PyTorch CPU and GPU tensors can be passed as input operands in the same fashion. + To compute the gradients of the network w.r.t. the input operands (NumPy/CuPy/PyTorch), the :meth:`gradients` method can + be used. To enable the gradient computation, one should + + 1. create the network with the ``qualifiers`` argument + 2. call the :meth:`contract` method prior to the :meth:`gradients` method + 3. seed the :meth:`gradients` method with the output gradient (see the docs for the requirements) + + Below is a minimal example: + + >>> from cuquantum import cutensornet as cutn + >>> expr = "ijk,jkl,klm,lmn" + >>> shapes = ((3, 4, 5), (4, 5, 3), (5, 3, 2), (3, 2, 6)) + >>> operands = [cp.random.rand(*shape) for shape in shapes] + >>> qualifiers = np.zeros(len(shapes), dtype=cutn.tensor_qualifiers_dtype) + >>> qualifiers[:]["requires_gradient"] = 1 # request gradients for all input tensors + >>> + >>> with Network(expr, *operands, qualifiers=qualifiers) as tn: + ... path, info = tn.contract_path() + ... + ... # Perform the contraction + ... r = tn.contract() + ... + ... # Perform the backprop + ... input_grads = tn.gradients(cp.ones_like(r)) + ... + >>> + + For PyTorch CPU/GPU tensors with the ``requires_grad`` attribute set up, one does not need to pass the ``qualifiers`` + argument. Note that this :class:`Network` class and its methods are **not** PyTorch operators and do **not** add any + node to PyTorch's autograd graph. For a native, differentiable PyTorch operator, use the :func:`cuquantum.contract` + function. + See :func:`contract` for more examples on specifying the Einstein summation expression as well as specifying options for the tensor network and the optimizer. """ - def __init__(self, *operands, qualifiers=None, options=None): + def __init__(self, *operands, qualifiers=None, options=None, stream=None): """ - __init__(subscripts, *operands, options=None) + __init__(subscripts, *operands, qualifiers=None, options=None, stream=None) """ options = utils.check_or_create_options(configuration.NetworkOptions, options, "network options") self.options = options + # Get cuTensorNet version (as seen at run-time). + cutn_ver = cutn.get_version() + cutn_major = cutn_ver // 10000 + cutn_minor = (cutn_ver % 10000) // 100 + cutn_patch = cutn_ver % 100 + # Logger. self.logger = options.logger if options.logger is not None else logging.getLogger() - self.logger.info(f"CUDA runtime version = {cutn.get_cudart_version()}") - self.logger.info(f"cuTensorNet version = {cutn.MAJOR_VER}.{cutn.MINOR_VER}.{cutn.PATCH_VER}") + self.logger.info(f"cuTensorNet version = {cutn_major}.{cutn_minor}.{cutn_patch}") self.logger.info("Beginning network creation...") # Parse Einsum expression. - self.operands, self.inputs, self.output, self.size_dict, self.mode_map_user_to_ord, self.mode_map_ord_to_user, self.is_interleaved = einsum_parser.parse_einsum(*operands) + self.operands, self.inputs, self.output, self.has_user_output, \ + self.size_dict, self.mode_map_user_to_ord, self.mode_map_ord_to_user, \ + self.is_interleaved, self.has_ellipses = einsum_parser.parse_einsum(*operands) - # Copy operands to device if needed. + # Infer the library package & device ID the operands belong to. + self.package = utils.get_operands_package(self.operands) self.network_location = 'cuda' self.device_id = utils.get_network_device_id(self.operands) if self.device_id is None: + self.package = self.operands[0].name + if self.package == 'numpy': + self.package = 'cupy' self.network_location = 'cpu' self.device_id = options.device_id - self.operands = tensor_wrapper.to(self.operands, self.device_id) + + # Allocate device memory (in stream context) if needed. + stream_holder = utils.get_or_create_stream(self.device_id, stream, self.package) + + # Copy operands to device if needed. + if self.network_location == 'cpu': + self.operands = tensor_wrapper.to(self.operands, self.device_id, stream_holder) # Set blocking or non-blocking behavior. self.blocking = self.options.blocking is True or self.network_location == 'cpu' @@ -198,9 +252,6 @@ def __init__(self, *operands, qualifiers=None, options=None): else: self.call_prologue = "This call is non-blocking and will return immediately after the operation is launched on the device." - # Infer the library package the operands belong to. - self.package = utils.get_operands_package(self.operands) - # The output class is that of the first wrapped device operand. self.output_class = self.operands[0].__class__ @@ -226,18 +277,32 @@ def __init__(self, *operands, qualifiers=None, options=None): num_inputs = len(self.inputs) num_modes_out = len(self.output) - extents_in = tuple(o.shape for o in self.operands) - strides_in = tuple(o.strides for o in self.operands) + extents_in = self.extents_in = tuple(o.shape for o in self.operands) + strides_in = self.strides_in = tuple(o.strides for o in self.operands) self.operands_data = utils.get_operands_data(self.operands) modes_in = tuple(tuple(m for m in _input) for _input in self.inputs) num_modes_in = tuple(len(m) for m in modes_in) self.qualifiers_in = utils.check_tensor_qualifiers(qualifiers, cutn.tensor_qualifiers_dtype, num_inputs) + # For torch tensors, if qualifiers are explicitly passed, we ignore the tensor attrs. + # Otherwise, we look up the tensor attrs and populate qualifiers. + if self.package == 'torch' and isinstance(self.qualifiers_in, int): # = 0 + self.qualifiers_in = np.zeros(num_inputs, dtype=cutn.tensor_qualifiers_dtype) + self.logger.debug("Checking input tensors' requires_grad attribute") + for i, t in enumerate(self.operands): + self.qualifiers_in[i]['requires_gradient'] = self.operands[i].tensor.requires_grad + self.qualifiers_in[i]['is_conjugate'] = self.operands[i].tensor.is_conj() + + # Check if gradient computation is required + if isinstance(self.qualifiers_in, np.ndarray): + self.require_grad = any(self.qualifiers_in['requires_gradient']) + else: + self.require_grad = False + # Create the output in the context of the current stream to work around a performance issue with CuPy's memory pool. - stream = None self.logger.debug("Beginning output tensor creation...") self.contraction, self.contraction_output_event, modes_out, extents_out, strides_out = utils.create_output_tensor( - self.output_class, self.package, self.output, self.size_dict, self.device_id, stream, self.data_type) + self.output_class, self.output, self.size_dict, self.device_id, stream_holder, self.data_type) self.logger.debug("The output tensor has been created.") # Create/set handle. @@ -257,6 +322,7 @@ def __init__(self, *operands, qualifiers=None, options=None): # Keep output extents for creating new tensors, if needed. self.extents_out = extents_out + self.strides_out = strides_out # Path optimization attributes. self.optimizer_config_ptr, self.optimizer_info_ptr = None, None @@ -264,7 +330,10 @@ def __init__(self, *operands, qualifiers=None, options=None): # Workspace attributes. self.workspace_desc = cutn.create_workspace_descriptor(self.handle) - self.workspace_ptr, self.workspace_size = None, None + self.workspace_scratch_ptr, self.workspace_scratch_size = None, None + self.workspace_cache_ptr, self.workspace_cache_size = None, None + self.workspace_h_scratch_ptr, self.workspace_h_scratch_size = None, None + self.workspace_h_cache_ptr, self.workspace_h_cache_size = None, None # Contraction plan attributes. self.plan = None @@ -279,6 +348,7 @@ def __init__(self, *operands, qualifiers=None, options=None): self.last_compute_event = None self.valid_state = True + self.contracted = False self.logger.info("The network has been created.") @@ -308,6 +378,21 @@ def _check_planned(self, *args, **kwargs): if not self.planned: raise RuntimeError(f"Internal Error: {what} cannot be performed before planning has been done.") + def _check_contracted(self, *args, **kwargs): + """ + """ + what = kwargs['what'] + if not self.contracted: + raise RuntimeError(f"{what} cannot be performed before contraction has been done.") + + def _check_qualifiers(self, *args, **kwargs): + """ + """ + what = kwargs['what'] + # cannot perform equality check (a == 0) if a is a numpy ndarray + if isinstance(self.qualifiers_in, int): + raise RuntimeError(f"{what} cannot be performed without creating the Network object with tensor qualifiers") + def _free_plan_resources(self, exception=None): """ Free resources allocated in network contraction planning. @@ -323,7 +408,10 @@ def _free_workspace_memory(self, exception=None): """ Free workspace by releasing the MemoryPointer object. """ - self.workspace_ptr = None + self.workspace_scratch_ptr = None + self.workspace_cache_ptr = None + self.workspace_h_scratch_ptr = None + self.workspace_h_cache_ptr = None return True @@ -341,7 +429,10 @@ def _free_path_resources(self, exception=None): self.optimizer_info_ptr = None self._free_workspace_memory() - self.workspace_size = None + self.workspace_scratch_size = None + self.workspace_cache_size = None + self.workspace_h_scratch_size = None + self.workspace_h_cache_size = None self._free_plan_resources() @@ -350,26 +441,50 @@ def _free_path_resources(self, exception=None): @utils.precondition(_check_valid_network) @utils.precondition(_check_optimized, "Workspace memory allocation") @utils.atomic(_free_workspace_memory, method=True) - def _allocate_workspace_memory_perhaps(self, stream, stream_ctx): - if self.workspace_ptr is not None: + def _allocate_workspace_memory_perhaps(self, stream_holder, kind): + assert kind == "scratch" or kind == "cache", "Internal Error." + + if getattr(self, f"workspace_{kind}_ptr") is not None and getattr(self, f"workspace_h_{kind}_ptr") is not None: return - assert self.workspace_size is not None, "Internal Error." + assert getattr(self, f"workspace_{kind}_size") is not None, "Internal Error." + assert getattr(self, f"workspace_h_{kind}_size") is not None, "Internal Error." + + self.logger.debug(f"Allocating {kind} workspace for contracting the tensor network...") - self.logger.debug("Allocating memory for contracting the tensor network...") - with utils.device_ctx(self.device_id), stream_ctx: + # Allocate device workspace. + device_size = getattr(self, f"workspace_{kind}_size") + with utils.device_ctx(self.device_id), stream_holder.ctx: try: - self.workspace_ptr = self.allocator.memalloc(self.workspace_size) + setattr(self, f"workspace_{kind}_ptr", self.allocator.memalloc(device_size)) except TypeError as e: message = "The method 'memalloc' in the allocator object must conform to the interface in the "\ "'BaseCUDAMemoryManager' protocol." raise TypeError(message) from e - self.workspace_stream = stream - self.logger.debug(f"Finished allocating memory of size {formatters.MemoryStr(self.workspace_size)} for contraction in the context of stream {self.workspace_stream}.") - - device_ptr = utils.get_ptr_from_memory_pointer(self.workspace_ptr) - cutn.workspace_set_memory(self.handle, self.workspace_desc, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH, device_ptr, self.workspace_size) - self.logger.debug(f"The workspace memory (device pointer = {device_ptr}) has been set in the workspace descriptor.") + self.workspace_stream = stream_holder.obj + # Allocate host workspace. + # TODO: ideally we should use a memory manager, as we did for device memory, but... + host_size = getattr(self, f"workspace_h_{kind}_size") + setattr(self, f"workspace_h_{kind}_ptr", np.empty(host_size, dtype=np.int8)) + self.logger.debug("Finished allocating " + f"device memory of size {formatters.MemoryStr(device_size)} and " + f"host memory of size {formatters.MemoryStr(host_size)} " + f"for contraction in the context of stream {self.workspace_stream}.") + + # Set device workspace. + device_ptr = utils.get_ptr_from_memory_pointer(getattr(self, f"workspace_{kind}_ptr")) + cutn.workspace_set_memory(self.handle, self.workspace_desc, cutn.Memspace.DEVICE, + cutn.WorkspaceKind.SCRATCH if kind == "scratch" else cutn.WorkspaceKind.CACHE, + device_ptr, device_size) + # Set host workspace. + # TODO: ideally we should be manipulating a MemoryPointer object here, but ... + host_ptr = getattr(self, f"workspace_h_{kind}_ptr").ctypes.data + cutn.workspace_set_memory(self.handle, self.workspace_desc, cutn.Memspace.HOST, + cutn.WorkspaceKind.SCRATCH if kind == "scratch" else cutn.WorkspaceKind.CACHE, + # WAR: empty numpy arrays still have nonzero ptr addresses + host_ptr if host_size > 0 else 0, host_size) + self.logger.debug(f"The {kind} workspace memory (device pointer = {device_ptr}, " + f"host pointer = {host_ptr}) has been set in the workspace descriptor.") @utils.precondition(_check_valid_network) @utils.precondition(_check_optimized, "Workspace size calculation") @@ -379,26 +494,59 @@ def _calculate_workspace_size(self): """ # Release workspace already allocated, if any, because the new requirements are likely different. - self.workspace_ptr = None + self.workspace_scratch_ptr = None + self.workspace_cache_ptr = None + self.workspace_h_scratch_ptr = None + self.workspace_h_cache_ptr = None cutn.workspace_compute_contraction_sizes(self.handle, self.network, self.optimizer_info_ptr, self.workspace_desc) - min_size = cutn.workspace_get_memory_size(self.handle, self.workspace_desc, cutn.WorksizePref.MIN, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH) - max_size = cutn.workspace_get_memory_size(self.handle, self.workspace_desc, cutn.WorksizePref.MAX, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH) - - if self.memory_limit < min_size: + # Deal with device workspaces. + min_scratch_size = cutn.workspace_get_memory_size( + self.handle, self.workspace_desc, cutn.WorksizePref.MIN, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH) + max_scratch_size = cutn.workspace_get_memory_size( + self.handle, self.workspace_desc, cutn.WorksizePref.MAX, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH) + min_cache_size = cutn.workspace_get_memory_size( + self.handle, self.workspace_desc, cutn.WorksizePref.MIN, cutn.Memspace.DEVICE, cutn.WorkspaceKind.CACHE) + max_cache_size = cutn.workspace_get_memory_size( + self.handle, self.workspace_desc, cutn.WorksizePref.MAX, cutn.Memspace.DEVICE, cutn.WorkspaceKind.CACHE) + + if self.memory_limit < min_scratch_size + min_cache_size: message = f"""Insufficient memory. -The memory limit specified is {self.memory_limit}, while the minimum workspace size needed is {min_size}. +The memory limit specified is {self.memory_limit}, while the minimum workspace size needed is {min_scratch_size + min_cache_size}. """ raise RuntimeError(message) - self.workspace_size = max_size if max_size < self.memory_limit else self.memory_limit - self.logger.info(f"The workspace size requirements range from {formatters.MemoryStr(min_size)} to "\ - f"{formatters.MemoryStr(max_size)}.") - self.logger.info(f"The workspace size has been set to {formatters.MemoryStr(self.workspace_size)}.") + if self.memory_limit - max_scratch_size >= min_cache_size: + self.workspace_scratch_size = max_scratch_size + else: + self.workspace_scratch_size = min_scratch_size + if min_cache_size > 0 and self.require_grad: + self.workspace_cache_size = min(max_cache_size, self.memory_limit - self.workspace_scratch_size) + else: + self.workspace_cache_size = 0 + self.logger.info(f"The workspace size requirements range from {formatters.MemoryStr(min_scratch_size + min_cache_size)} to "\ + f"{formatters.MemoryStr(max_scratch_size + max_cache_size)}.") + self.logger.info(f"The scratch workspace size has been set to {formatters.MemoryStr(self.workspace_scratch_size)}.") + self.logger.info(f"The cache workspace size has been set to {formatters.MemoryStr(self.workspace_cache_size)}.") # Set workspace size to enable contraction planning. The device pointer will be set later during allocation. - cutn.workspace_set_memory(self.handle, self.workspace_desc, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH, 0, self.workspace_size) + cutn.workspace_set_memory( + self.handle, self.workspace_desc, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH, 0, self.workspace_scratch_size) + cutn.workspace_set_memory( + self.handle, self.workspace_desc, cutn.Memspace.DEVICE, cutn.WorkspaceKind.CACHE, 0, self.workspace_cache_size) + + # Deal with device workspaces. For now we don't care how much host memory is used. + self.workspace_h_scratch_size = cutn.workspace_get_memory_size( + self.handle, self.workspace_desc, cutn.WorksizePref.RECOMMENDED, cutn.Memspace.HOST, cutn.WorkspaceKind.SCRATCH) + self.workspace_h_cache_size = cutn.workspace_get_memory_size( + self.handle, self.workspace_desc, cutn.WorksizePref.RECOMMENDED, cutn.Memspace.HOST, cutn.WorkspaceKind.CACHE) + + # Set workspace size to enable contraction planning. The host pointer will be set later during allocation. + cutn.workspace_set_memory( + self.handle, self.workspace_desc, cutn.Memspace.HOST, cutn.WorkspaceKind.SCRATCH, 0, self.workspace_h_scratch_size) + cutn.workspace_set_memory( + self.handle, self.workspace_desc, cutn.Memspace.HOST, cutn.WorkspaceKind.CACHE, 0, self.workspace_h_cache_size) @utils.precondition(_check_valid_network) @utils.precondition(_check_optimized, "Planning") @@ -637,34 +785,35 @@ def autotune(self, *, iterations=3, stream=None): self._set_autotune_options(options) # Allocate device memory (in stream context) if needed. - stream, stream_ctx, stream_ptr = utils.get_or_create_stream(self.device_id, stream, self.package) - self._allocate_workspace_memory_perhaps(stream, stream_ctx) + stream_holder = utils.get_or_create_stream(self.device_id, stream, self.package) + self._allocate_workspace_memory_perhaps(stream_holder, "scratch") + self._allocate_workspace_memory_perhaps(stream_holder, "cache") # Check if we still hold an output tensor; if not, create a new one. if self.contraction is None: self.logger.debug("Beginning output (empty) tensor creation...") - self.contraction = utils.create_empty_tensor(self.output_class, self.extents_out, self.data_type, self.device_id, stream_ctx) + self.contraction = utils.create_empty_tensor(self.output_class, self.extents_out, self.data_type, self.device_id, stream_holder) self.logger.debug("The output (empty) tensor has been created.") elif self.contraction_output_event is not None: - stream.wait_event(self.contraction_output_event) + stream_holder.obj.wait_event(self.contraction_output_event) self.contraction_output_event = None self.logger.debug("Established ordering with output tensor creation event.") timing = bool(self.logger and self.logger.handlers) self.logger.info(f"Starting autotuning...") self.logger.info(f"{self.call_prologue}") - with utils.device_ctx(self.device_id), utils.cuda_call_ctx(stream, self.blocking, timing) as (self.last_compute_event, elapsed): - cutn.contraction_autotune(self.handle, self.plan, self.operands_data, self.contraction.data_ptr, - self.workspace_desc, self.autotune_pref_ptr, stream_ptr) + with utils.device_ctx(self.device_id), utils.cuda_call_ctx(stream_holder, self.blocking, timing) as (self.last_compute_event, elapsed): + cutn.contraction_autotune( + self.handle, self.plan, self.operands_data, self.contraction.data_ptr, + self.workspace_desc, self.autotune_pref_ptr, stream_holder.ptr) if elapsed.data is not None: self.logger.info(f"The autotuning took {elapsed.data:.3f} ms to complete.") self.autotuned = True - @utils.precondition(_check_valid_network) - def reset_operands(self, *operands): + def reset_operands(self, *operands, stream=None): """Reset the operands held by this :class:`Network` instance. This method is not needed when the operands @@ -678,12 +827,20 @@ def reset_operands(self, *operands): Args: operands: See :class:`Network`'s documentation. + stream: Provide the CUDA stream to use for resetting operands (this is used to copy the operands to the GPU if they are provided on the CPU). Acceptable inputs include ``cudaStream_t`` + (as Python :class:`int`), :class:`cupy.cuda.Stream`, and :class:`torch.cuda.Stream`. If a stream is not provided, + the current stream will be used. """ if len(operands) != len(self.operands): message = f"Mismatch in the number of operands ({len(operands)} provided, need {len(self.operands)})." raise ValueError(message) + # Future operations on the workspace stream should be ordered after the computation. + # Also, we should ensure self.operands is overwritten only after work using them is done. + if self.last_compute_event is not None: + self.workspace_stream.wait_event(self.last_compute_event) + self.logger.info("Resetting operands...") # First wrap operands. operands = tensor_wrapper.wrap_operands(operands) @@ -691,10 +848,12 @@ def reset_operands(self, *operands): utils.check_operands_match(self.operands, operands, 'dtype', "data type") utils.check_operands_match(self.operands, operands, 'shape', 'shape') + stream_holder = utils.get_or_create_stream(self.device_id, stream, self.package) + device_id = utils.get_network_device_id(operands) if device_id is None: - # Copy to existing device pointers because the new operands are on the CPU. - tensor_wrapper.copy_(operands, self.operands) + # In-place copy to existing device pointers because the new operands are on the CPU. + tensor_wrapper.copy_(operands, self.operands, stream_holder) else: utils.check_operands_match(self.operands, operands, 'strides', 'strides') package = utils.get_operands_package(operands) @@ -708,8 +867,17 @@ def reset_operands(self, *operands): # Finally, replace the original data pointers by the new ones. self.operands_data = utils.get_operands_data(operands) + self.operands = operands self.logger.info("The operands have been reset.") + self.contracted = False + if not self.require_grad: + return + + # The cache workspace is invalidated. + cutn.workspace_purge_cache(self.handle, self.workspace_desc, cutn.Memspace.DEVICE) + cutn.workspace_purge_cache(self.handle, self.workspace_desc, cutn.Memspace.HOST) + @utils.precondition(_check_valid_network) @utils.precondition(_check_optimized, "Contraction") @utils.precondition(_check_planned, "Contraction") @@ -728,16 +896,18 @@ def contract(self, *, slices=None, stream=None): """ # Allocate device memory (in stream context) if needed. - stream, stream_ctx, stream_ptr = utils.get_or_create_stream(self.device_id, stream, self.package) - self._allocate_workspace_memory_perhaps(stream, stream_ctx) + stream_holder = utils.get_or_create_stream(self.device_id, stream, self.package) + self._allocate_workspace_memory_perhaps(stream_holder, "scratch") + self._allocate_workspace_memory_perhaps(stream_holder, "cache") # Check if we still hold an output tensor; if not, create a new one. if self.contraction is None: self.logger.debug("Beginning output (empty) tensor creation...") - self.contraction = utils.create_empty_tensor(self.output_class, self.extents_out, self.data_type, self.device_id, stream_ctx) + self.contraction = utils.create_empty_tensor( + self.output_class, self.extents_out, self.data_type, self.device_id, stream_holder) self.logger.debug("The output (empty) tensor has been created.") elif self.contraction_output_event is not None: - stream.wait_event(self.contraction_output_event) + stream_holder.obj.wait_event(self.contraction_output_event) self.contraction_output_event = None self.logger.debug("Established ordering with output tensor creation event.") @@ -759,9 +929,10 @@ def contract(self, *, slices=None, stream=None): timing = bool(self.logger and self.logger.handlers) self.logger.info("Starting network contraction...") self.logger.info(f"{self.call_prologue}") - with utils.device_ctx(self.device_id), utils.cuda_call_ctx(stream, self.blocking, timing) as (self.last_compute_event, elapsed): - cutn.contract_slices(self.handle, self.plan, self.operands_data, self.contraction.data_ptr, False, - self.workspace_desc, slice_group, stream_ptr) + with utils.device_ctx(self.device_id), utils.cuda_call_ctx(stream_holder, self.blocking, timing) as (self.last_compute_event, elapsed): + cutn.contract_slices( + self.handle, self.plan, self.operands_data, self.contraction.data_ptr, False, + self.workspace_desc, slice_group, stream_holder.ptr) if elapsed.data is not None: self.logger.info(f"The contraction took {elapsed.data:.3f} ms to complete.") @@ -772,13 +943,102 @@ def contract(self, *, slices=None, stream=None): self.logger.debug(f"Slice group ({slice_group}) has been destroyed.") if self.network_location == 'cpu': - out = self.contraction.to('cpu') + out = self.contraction.to('cpu', stream_holder=stream_holder) else: out = self.contraction.tensor self.contraction = None # We cannot overwrite what we've already handed to users. + self.contracted = True return out + @utils.precondition(_check_valid_network) + @utils.precondition(_check_optimized, "Gradient") + @utils.precondition(_check_planned, "Gradient") + @utils.precondition(_check_contracted, "Gradient") + @utils.precondition(_check_qualifiers, "Gradient") + def gradients(self, output_gradient, *, stream=None): + """Compute the gradients of the network (w.r.t. the input operands whose gradients are required). + + Before calling this method, a full contraction must have been performed (by calling :meth:`contract`), otherwise an + error is raised. + + Args: + output_gradient: A tensor of the same package (NumPy/CuPy/PyTorch), shape, dtype, strides, and location (CPU/GPU) + as the contraction output (as returned by :meth:`contract`), which in turn shares the same properties with the + input operands. In a chain-rule setting, ``output_gradient`` is the gradient w.r.t. the output tensor. + stream: Provide the CUDA stream to use for the gradient computation. Acceptable inputs include ``cudaStream_t`` + (as Python :class:`int`), :class:`cupy.cuda.Stream`, and :class:`torch.cuda.Stream`. If a stream is not provided, + the current stream will be used. + + Returns: + A sequence of gradient tensors. The result is of the same length and type and on the same device as the input operands. + For the gradient components that are not requested, ``None`` is returned. + + .. note:: For PyTorch operands, calling this method is **not** tracked by the autograd graph. + + .. warning:: This API is experimental and subject to future changes. + """ + warnings.warn("Network.gradients() is an experimental API and subject to future changes", + stacklevel=2) + + # At this point, both scratch and cache workspaces are allocated/populated. + assert self.workspace_scratch_ptr is not None and self.workspace_cache_ptr is not None, "Internal error." + assert self.workspace_h_scratch_ptr is not None and self.workspace_h_cache_ptr is not None, "Internal error." + + stream_holder = utils.get_or_create_stream(self.device_id, stream, self.package) + # Future operations on the workspace stream should be ordered after the computation. + if self.last_compute_event is not None: + self.workspace_stream.wait_event(self.last_compute_event) + stream_holder.obj.wait_event(self.last_compute_event) + + # Wrap output_gradient + output_grad = tensor_wrapper.wrap_operand(output_gradient) + if output_grad.device_id != self.device_id: + output_grad = tensor_wrapper.to([output_grad], self.device_id, stream_holder)[0] + if output_grad.shape != self.extents_out: + raise ValueError(f"output_gradient shape incorrect (given {output_grad.shape}, expected {self.extents_out})") + if output_grad.dtype != self.data_type: + raise ValueError(f"output_gradient dtype incorrect (given {output_grad.dtype}, expected {self.data_type}") + if output_grad.strides != self.strides_out: + # output_gradient could be a view, but we need a full buffer for now + if any(s == 0 for s in output_grad.strides): + buf = utils.create_empty_tensor( + self.output_class, self.extents_out, self.data_type, self.device_id, stream_holder, strides=self.strides_out) + buf.copy_(output_grad.tensor, stream_holder=stream_holder) + output_grad = buf + else: + raise ValueError(f"output_gradient strides incorrect (given {output_grad.strides}, expected {self.strides_out}") + + # Allocate grad tensors, as needed + input_grads = [] + for i, extents, strides, requires_grad in zip( + range(len(self.inputs)), self.extents_in, self.strides_in, self.qualifiers_in['requires_gradient']): + if requires_grad: + input_grads.append( + utils.create_empty_tensor(self.output_class, extents, self.data_type, self.device_id, stream_holder, strides=strides) + ) + else: + input_grads.append(None) + input_grads_data = utils.get_operands_data(input_grads) + + timing = bool(self.logger and self.logger.handlers) + self.logger.info("Starting gradient computation...") + self.logger.info(f"{self.call_prologue}") + with utils.device_ctx(self.device_id), utils.cuda_call_ctx(stream_holder, self.blocking, timing) as (self.last_compute_event, elapsed): + cutn.compute_gradients_backward( + self.handle, self.plan, self.operands_data, output_grad.data_ptr, + input_grads_data, False, self.workspace_desc, stream_holder.ptr) + + if elapsed.data is not None: + self.logger.info(f"The backprop took {elapsed.data:.3f} ms to complete.") + + if self.network_location == 'cpu': + op = lambda t: t.to('cpu', stream_holder=stream_holder) if t is not None else None + else: + op = lambda t: t.tensor if t is not None else None + + return tuple(map(op, input_grads)) + def free(self): """Free network resources. @@ -950,7 +1210,11 @@ def contract(*operands, qualifiers=None, options=None, optimize=None, stream=Non ... b = cupy.arange(6.).reshape(2, 3) >>> r = contract('ij,jk', a, b) - Use PyTorch operands. The result ``r`` is a PyTorch tensor on the same device (``dev``) as the operands: + For PyTorch operands, this function **works like a native PyTorch operator** out of box that will be tracked by the + autograd graph so as to enable backpropagation. The result ``r`` is a PyTorch tensor on the same device (``dev``) as + the operands. To enable gradient computation, just set the target operands' ``requires_grad`` attribute to ``True``, + as usual. If ``stream`` is explicitly passed, the user must establish the stream ordering following the requirements + outlined in PyTorch's `CUDA Semantics `_. .. doctest:: :skipif: torch is None @@ -958,12 +1222,28 @@ def contract(*operands, qualifiers=None, options=None, optimize=None, stream=Non >>> import torch >>> dev = 0 >>> a = torch.arange(6., device=f'cuda:{dev}').reshape(3, 2) + >>> a.requires_grad_(True) >>> b = torch.arange(6., device=f'cuda:{dev}').reshape(2, 3) + >>> b.requires_grad_(True) >>> r = contract('ij,jk', a, b) + >>> r.backward(torch.ones_like(r)) # gradient w.r.t self is 1 + >>> a.grad + tensor([[ 3., 12.], + [ 3., 12.], + [ 3., 12.]], device='cuda:0') + >>> b.grad + tensor([[6., 6., 6.], + [9., 9., 9.]], device='cuda:0') """ # Create network. - with Network(*operands, qualifiers=qualifiers, options=options) as network: + network = Network(*operands, qualifiers=qualifiers, options=options, stream=stream) + + # For PyTorch tensors, we ensure contract() is differentiable. + if network.package == "torch": + return grad_torch._TorchContract.apply(network, optimize, stream, return_info, *operands) + + with network: # Compute path. opt_info = network.contract_path(optimize=optimize) @@ -1094,6 +1374,8 @@ def einsum(*operands, out=None, dtype=None, order='K', casting='safe', optimize= output: A tensor (ndarray-like object) of the same type and on the same device as the operands containing the result of the contraction. + + .. note:: For PyTorch operands, calling this method is **not** tracked by the autograd graph. """ _check_einsum_options(out, dtype, order, casting, optimize) diff --git a/python/cuquantum/utils.pxd b/python/cuquantum/utils.pxd index 9cfa200..a5a3d57 100644 --- a/python/cuquantum/utils.pxd +++ b/python/cuquantum/utils.pxd @@ -20,6 +20,11 @@ cdef extern from "vector_types.h" nogil: ctypedef struct int2 'int2': pass +cdef extern from "cuComplex.h" nogil: + ctypedef struct cuDoubleComplex: + double x + double y + # Cython limitation: need standalone typedef if we wanna use it for casting ctypedef int (*DeviceAllocType)(void*, void**, size_t, Stream) diff --git a/python/samples/custatevec/subsv_migration.py b/python/samples/custatevec/subsv_migration.py new file mode 100644 index 0000000..69801b6 --- /dev/null +++ b/python/samples/custatevec/subsv_migration.py @@ -0,0 +1,63 @@ +# Copyright (c) 2021-2023, NVIDIA CORPORATION & AFFILIATES +# +# SPDX-License-Identifier: BSD-3-Clause + +import cupy as cp +import cupyx as cpx +import numpy as np + +from cuquantum import custatevec as cusv +from cuquantum import cudaDataType + +dtype = np.complex128 +sv_data_type = cudaDataType.CUDA_C_64F; + +n_local_index_bits = 3; + +sub_sv_size = 2 ** n_local_index_bits + +# allocate host sub state vectors +n_sub_svs = 2 +sub_svs = [None] * n_sub_svs +sub_svs[0] = cpx.empty_pinned(sub_sv_size, dtype=dtype) +sub_svs[0][:] = 0.25 + 0.j +sub_svs[1] = cpx.zeros_pinned(sub_sv_size, dtype=dtype) + +# allocate device slots + +n_device_slots = 1; +device_slots_size = sub_sv_size * n_device_slots +device_slots = cp.zeros([device_slots_size], dtype=dtype) + +# initialize custatevec handle +handle = cusv.create() + +# create migrator +migrator = cusv.sub_sv_migrator_create(handle, device_slots.data.ptr, sv_data_type, + n_device_slots, n_local_index_bits) + +device_slot_index = 0 +src_sub_sv = sub_svs[0] +dst_sub_sv = sub_svs[1] + +# migrate sub_svs[0] into device_slots +cusv.sub_sv_migrator_migrate(handle, migrator, device_slot_index, + src_sub_sv.ctypes.data, 0, 0, sub_sv_size) + +# migrate device_slots into sub_svs[1] +cusv.sub_sv_migrator_migrate(handle, migrator, device_slot_index, + 0, dst_sub_sv.ctypes.data, 0, sub_sv_size) + +# destroy migrator +cusv.sub_sv_migrator_destroy(handle, migrator) + +# destroy custatevec handle +cusv.destroy(handle) + +# check if sub_svs[1] has expected values +correct = np.all(sub_svs[1] == 0.25 + 0.j) + +if correct: + print('subsv_migration example PASSED') +else: + raise RuntimeError('subsv_migration example FAILED: wrong result') diff --git a/python/samples/cutensornet/circuit_converter/qiskit_advanced.ipynb b/python/samples/cutensornet/circuit_converter/qiskit_advanced.ipynb index 18856e7..bd6b60d 100644 --- a/python/samples/cutensornet/circuit_converter/qiskit_advanced.ipynb +++ b/python/samples/cutensornet/circuit_converter/qiskit_advanced.ipynb @@ -57,9 +57,9 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAArQAAAGwCAYAAABYR/ZRAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAABtTElEQVR4nO3deXgUVdbH8W9nDwlbCBJIkD2ENWEXQSCKgwjogAsioIkMoIjogMC4MOqoKMsI4goqbvMqUUFlGQRkUQQUEAgIyBIIGJKAgbAkQNZ+/8gYjARMh+ruVNfv8zw8kOqq4ty6p26fdN+qstntdjsiIiIiIibl5e4ARERERESuhApaERERETE1FbQiIiIiYmoqaEVERETE1FTQioiIiIipqaAVEREREVNTQSsiIiIipqaCVkRERERMTQWtiIiIiJiaCloRERERMTUVtCIiIiJiaipoRURERMTUVNCKiIiIiKmpoBURERERU1NBKyIiIiKmpoJWRERERExNBa2IiIiImJoKWhERERExNRW0IiIiImJqKmhFRERExNRU0IqIiIiIqamgFRERERFTU0ErIiIiIqamglZERERETE0FrYiIiIiYmgpaERERETE1FbQiIiIiYmoqaEVERETE1FTQioiIiIipqaAVEREREVNTQSsiIiIipqaCVkRERERMTQWtiIiIiJiaCloRERERMTUVtCIiIiJiaipoRURERMTUVNCKiIiIiKmpoBURERERU/NxdwBSOrsdCvPcHUXZefmCzebuKDyH2fofjM8BHQOxOqufA1ZvvzhGBW0FVZgHq2e5O4qyix0D3n7ujsJzmK3/wfgc0DEQq7P6OWD19otjNOVARERERExNBa2IiIiImJoKWhERERExNRW0IiIiImJqKmhFRERExNRU0IqIiIiIqem2XR4kMWkNj74ZW2JZgF8QETUj6dl2KH/t8hDe3upyT2b1HLB6+0V0DugYWJV61APFxgyiY9TN2LGTeSadFT9+wJuLxnL42G7+fvscd4cnLmD1HLB6+0V0DugYWI0KWg/UJLwtPdsNKf6537WjGDY1iqUb3yb+puepFlzTjdGJK1g9B6zefhGdAzoGVqM5tBYQ6BdEVL1rsNvtpB5Pcnc44gZWzwGrt19E54COgadTQWsRaf87eatUCnFzJOIuVs8Bq7dfROeAjoEn05QDD3Q+7yyn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", "text/plain": [ - "
" + "
" ] }, "execution_count": 2, @@ -86,9 +86,9 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAArQAAAGwCAYAAABYR/ZRAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAAB1uklEQVR4nO3dd3hUZdrH8e+kF0IJiSYkSIDQS+hKWSCCCiIgoKKLKOiKCogKgq7dXUVBdxFEJaiIrCvEFfRVWFQQUEBYQXqREowQSMRIKAmkTGbeP0aCkTYTZjJz5vw+18XFldNyP+e5z3PumZxisdvtdkREREREDCrA2wGIiIiIiFwKFbQiIiIiYmgqaEVERETE0FTQioiIiIihqaAVEREREUNTQSsiIiIihqaCVkREREQMTQWtiIiIiBiaCloRERERMTQVtCIiIiJiaCpoRURERMTQVNCKiIiIiKGpoBURERERQ1NBKyIiIiKGpoJWRERERAxNBa2IiIiIGJoKWhERERExNBW0IiIiImJoKmhFRERExNBU0IqIiIiIoamgFRERERFDU0ErIiIiIoamglZEREREDE0FrYiIiIgYmgpaERERETE0FbQiIiIiYmgqaEVERETE0FTQioiIiIihqaAVEREREUNTQSsiIiIihqaCVkREREQMTQWtiIiIiBiaCloRERERMTQVtCIiIiJiaCpoRURERMTQVNCKiIiIiKGpoBURERERQ1NBKyIiIiKGFuTtAOTc7HawlXg7CucFBIPF4u0o/IfR+h/cnwPaB2J2Zj8GzN5+cY0KWh9lK4Hl07wdhfNSx0BgiLej8B9G639wfw5oH4jZmf0YMHv7xTW65EBEREREDE0FrYiIiIgYmgpaERERETE0FbQiIiIiYmgqaEVERETE0FTQioiIiIih6bFdfmRzxgoemZFablpYSCSJsQ3p2WYoN3Z+gMBAdbk/M3sOmL39IjoGtA/MSj3qh1Jb3UaHxtdjx07eiRyWfD+HGZ+NZf/hnTx800xvhyeVwOw5YPb2i+gY0D4wGxW0fqhBQht6tr297Oe+nUZy9+TGLP7ubYb3eoHqVWK9GJ1UBrPngNnbL6JjQPvAbHQNrQmEh0TSuM5V2O12Dv2a4e1wxAvMngNmb7+IjgHtA3+ngtYksn87eKtGRHs5EvEWs+eA2dsvomNA+8Cf6ZIDP1RYcpJjBbnY7Y7rhj5bM4O9BzfSuHYHEmMbejs8qQRmzwGzt19Ex4D2gdmYoqDNzc1l8uTJLFiwgKysLGJjYxk4cCATJ05kzJgxzJo1i9dee43Ro0d7O1S3mPPlM8z58ply07o0H8gDA173UkRS2cyeA2Zvv4iOAe0Ds/H7gnbTpk307t2bnJwcIiMjadq0KYcOHWLatGlkZGRw5MgRAFq1auXdQN2oz5Uj6NryZqy2En7M3kr6iknkHssiJDisbJliaxEjX21Daus/M6THE2XTJ88bxtH8n5n4l8XeCN0jiq2w4SfYkAnHT0GABWKi4Kr60LiW42d/Y/YcMHv75WzZR2H1bvjpV8eYEBYCjeOhYzJUj/B2dO6nY0D7wGz8+hra3Nxc+vbtS05ODuPGjSM7O5sNGzaQk5PDpEmTWLRoEevWrcNisdCyZUtvh+s2CTENaNOwJx0a92Zw6gT+PvwzdmWtY+r8+8qWCQkKZcKtc5j31UQyDm0GYPW2T1i78zPG3vyOt0J3u2/3wDMfw7y1sDsHco7BoaOw5QDMXAETP4V9h70dpfuZPQfM3n4540QhvLkMJi2CVXvgwBH4+Tj8lAtfbIW/feIYH6yl3o7UvXQMaB+YjV8XtGPGjCErK4vRo0fzyiuvEBUVVTZvwoQJpKSkYLVaSUpKomrVql6M1LOaJXWiZ5uhrNiczvbMb8umN0xsy03dHmHyvDv45WgWr340ggcGvE5MtVpejNZ9lmyDD7+DU8XnXyY3H974ylHs+jOz5sBpZm+/WZ04BVO/gF3Z51/GZoe1GfDWCv8ran9Px4D2gb/z24J2586dpKenExMTw4svvnjOZdq2bQtASkpKuek//vgj/fr1Iyoqiho1anDHHXfw66+/ejxmTxrS8ykCAgJ574un/zD9SQIDgrj/1dakJKeS2upWL0XoXjsPwaLNzi1rtcGsbxyXI/gzs+XAH5m9/WY0e5XjQ6szduXAZ5s8Go7X6RjQPvBnflvQzp07F5vNxpAhQ6hSpco5lwkPDwfKF7QnTpwgNTWVrKws5s6dy8yZM1m5ciU33HADNputUmL3hISYZFJTbmXj3q/Yum9l2fSgwGCaJnXiWEEu17Ub7sUI3WvFTteWLyyBtXs9E4uvMFsO/JHZ2282+3+FDBcvJ1qz98J/0TE6HQPaB/7MbwvaZcuWAZCamnreZbKysoDyBe3MmTM5ePAgn3zyCTfccAM333wzH3zwAWvXruXTTz/1bNAedluPJwiwBPDel2c+mW7dt5Iv18+mf+fRvPHpgxSVGP9rysPHHd+2uOrbvVBq3M8sTjFLDpyP2dtvJqt2u75OsRXW/ej+WHyJjgHtA39lsdvtdm8H4Qm1a9cmKyuLjRs3nvMJBlarlfj4eHJzc8nIyKBevXrAmQJ4+fLl5ZavX78+3bt35513XL9IvF27duTkuFZhhQSFM3P0Hpd/lytOFeVz7z9TGNR1LH073s+4Gd1omNiO+/tNcXlbI6Y3oNjqGwNAnXa30P7mf1Zo3cWTOlNw5Cc3R+S6yuh/8O0cMNoxAL51HJhdr/GrqBKT5PJ6BzZ/yv8+GOn+gCrA7MeAxkHziYuLY/369RVa128f21VQUADAqVPnTqz09HRyc3OJioqibt26ZdN37NjBzTfffNbyzZo1Y8eOHRWKJScnh4MHD7q0Tliw558jk/bZOOKi69Kv00gsFgvjb5nNfa+2onPzAbSs19WlbWUfOkRhyUkPReqamKYlFV73yLECcl3sK0+ojP4H384Box0D4FvHgdlZgsIuvtA5WO2BLo/XnmL2Y0DjoLjCbwvauLg48vLy2LBhAx07diw3Lzs7m/HjxwPQsmVLLJYzDyLNy8ujevXqZ20vOjqaXbt2VTgWV4UEhVfodznrux8Ws2JzOjPHbilrf62Y+tzd+yVeSR9O2rgthIdEOr29+Fq1fOZTaWRYYIXXja4WQWhCghujqRhP9z/4fg4Y7RgA3zoOzM5WwaIiyGIlwQfGANAxoHHQfCpSL53mt5ccjBkzhtdee43atWuzdOlSGjZ0vOZu3bp1DB06lH379lFSUsKoUaOYPn162XohISFMmDCB559/vtz2hg0bxpo1aypc1LqqtBiWT6uUX+UWqWMgMMTbUTj8mg/P/x+4mtg1q8AT/XzjRQtG639wfw5oH8il+PB/juviXTX4SsfLFnyB2Y8Bs7dfXOO3N4VNmDCBmjVrcuDAAZo1a0aLFi1o0KABHTp0oF69elx99dXA2Y/sqlGjBkePHj1re0eOHCE6OroyQpdLVLMKNK3AFyydG/hGMSsil65zQ9fXCQuGNkluD0VEKoHfFrSJiYmsXLmSPn36EBYWRmZmJtHR0aSlpbFo0SJ273bcAvvHgrZJkybnvFZ2x44dNGnSpFJil0uX2gRcqU0jQ6FDPY+FIyKVLKGG49W2rvhTQwj12wvxRPyb3xa04ChOFy5cyIkTJzhx4gT/+9//GDFiBAUFBWRmZhIQEEDz5s3LrXPDDTewatWqskd6Afzvf/8jIyODvn37VnYTpIKSL4dB7Z1bNjQI/tINqlTsHhIR8VF3dIb46s4t27I29PafN6CLmI5fF7Tns337dux2Ow0aNCAiovxdlCNGjCA+Pp7+/fuzcOFCPvroI2677TY6dOhA//79vRSxVESXho4TWrUL3FeQWAMeuAbqxlZeXCJSOSJCHcd3qyvAcp4/2QQHOv6ic2cXCDDlGVHEP5jyjytbt24Fzr7cAKBq1aosW7aMBx98kFtvvZWgoCBuuOEGpkyZQoBGO8NpkwQpV8D2g/D9j7DtoOPlCcGBMKon1Kl5/hOdiBhfRAgM+xPkFcC3e2D5TsfrroMCoG9raF/PsYyIGJsK2nOoX78+CxcurMyQXJb1yx5eTr+TYwW5RIZVY/zg2STFNSu3jM1mY+bCR1i363MCA4KoGlmTh296i4QYxy28H654mSXr38Nmt1E7thGPDH6XKuHVOVVcwIQZV1NsLQQgOiqeBwfNIC46qbKb6RaBAY4/J7asDc8sgGOnHCewpBhvR+ZezuTE5owVPP52bxJjG5VNm/bAGkKDw9mRuYapC+4HoNRWQvOkLoy8cRohQaGV2g5XXGqbc45k8nL6MPYe2khcjbqkjd1UtszFjh8xlhqR0KcVfLfPMQZEhkK3xt6O6tJd6rlg3a4veHvRo2XLHi04THRUHG8+tAGAEyfzmP7JaHYdWEdQYDBXNe3LX65/qVLbeCHOtP/zde/y8cqpZT/nHsuiRb2uPHvnAgDW7ljIzIWPUGovpW5cC8YPnk1kWFXg/OdJ8T0qaA1q6vx7uf7KEVzXfhjfbPmIl9OH8fqD68ots2bHp2zPXE3a2M0EBQbz76XPM2vx4zw19EO+372EL9a9y2sP/I+IsKjf5j3BmIGvExoUzqQRS4kIiwJg/jdTeOP/HuRvw//PG00VJzmTEwCJsY3KFW6n1auVwusPOk5aNpuNv80ZxGffvsGgrg9XQvQVc6ltjgiryvBez1NQeIxZi58oN+9Cx4+Ir7jUc0H7RtfRvtF1Zcs+OesGUuqfeWX8Pz68i2ZJnfnrn/8NwJHjFXivuAc50/5e7YfTq/3wsp/veaU5PVoPARxvCfvHf+7mH/d/zRWXNea1j0fz76V/Z8QNL1/wPCm+x5R/Q1+2bBl2u50+ffp4O5QKycs/zO6s9fRsczsAf2oxiF+OHuBgbvmHLlqwUGwtorikELvdzsnC48RWSwRg36HNNK/bpaxo7dD4er7a8C8AAgICyqafXs+iv8v7NGdz4kLCQiIICgwGwFpaTFHJKZ/ud3e0uWpENM3rdiHsHA9Ov9DxI+IL3HEu+L3cY4fYuOcrerYdCsDB3L3szlrPoK5jy5aJrlrxB9+7W0XGgJ37/8fR/MN0bNYPcLxYIblWa664zPF1fb9OI1m+aS5w4fOk+B5TfkNrdL8cPUB01XgCAx3dZ7FYuKzGFRw+ur/cn0OvatqXTRnLGfy3OMJDo4iplsA/7v8agAaJbfl0zRscOZ5DjajL+WrjvzlZdILjJ49QNcLxvN0JaT35MWcr1SNjefGeLyq/oeI0Z3MCIPtIBve/2oYASyDXtR9Ov05n3lufcySTZ2b359CvGVzZpA99O/rGO+3PxV1tPp8LHT8ivsAd54Lf+3L9bDo0vp4aVS4D4KefdxBTLZFpC+5nd9Z6oiJqck+fSSQntK6cBl6EK2PAaZ9/9w492g4t+/B++Oh+Lq9Rp2z+5TWSOHI8m9JSq1PnSfEdpvyG1ix2Z60nM2cbc586yLynDtE6uQdT598HQKvkVG7u9ghPvnsDY167iuqRjtv8AwPOfMaZfO9S0p/KplvKYD746gWvtEHcKzmhDXOfyOLNhzbw7J0fs3DNDL7efOZP6HHRSaSN3cyHT+dQYi1i1bYFXozWPS7W5vO50PEjYiTO5LLdbueLdbPo1eHusmmlNiu7DnxH91a38sZD3zOo68M8OesGrKUlld0EtzhVXMCKTfPo/bs2Xogz50nxHSpoDSi2eu2yT5DgGIgO5+3nsupXlFtuyfdzaJV8NVXCqxMQEMA17e5kU8bysvn9Oo3kjQfX89qY/9GyfndiqyWWXQh/WkBAANdfeQ9L9WcWn+ZsTkSGVSUyvNpv6ySS2vo2tv648qzthYdWoXurW1m24d+eD76C3N3mP7rY8SPibe46FwBs2fc1xdZC2v3uetrLql9BzWoJtEp2XFPboXFvrKXF/Jz3k4db5hxn23/aN5v/Q53Lm1Hn8qZl0y6rfkW59vycl1nuW19nzpPiG1TQGlCNKpeRnNCGpRveB2Dl1vnEVE88608s8dH12LR3GSXWYgDW7lxIUtyZF0n8ejwbgMLik7z3xdPc0n0C4Ljo/8TJvLLlVmxOp26cnjjuy5zNiV+PZ2Oz2QA4WXiCtTsWklzL8efDg7l7y755KbEWs3rbx9SN991+d0ebL+Rix4+It7nrXACw+Lt3uLbdMAIDAsumNUxsS2RoVfYd2gLAD/u/w263E1u9tieb5TRn23/a5+veKfcNNED7Rr3Ye3AD+w//AMCn375B95Rby+af7zwpvkffmxvUQ4PSeDl9GHOXTSQirCrjb3kXgH/85y90bNqPTs360a/zKPYf3sm9U1IICgimRlQcDw2aUbaNx966FrvdRklpMT3bDKV/59GA45qiV+ffi81Wih07tWrW57E/v++VdorznMmJlVvns3DNmwQGBFFqs9K15c1c99vdv5v2LuOTVdMICAik1GaldXIPbu/5lDebdFGX2ubC4pMMn9yQEmsRBYXHuO35RHq2Gcrd17940eNHxBe441xQcOoYq7cuYOa4reW2bbFYGH/re0z56B6KSk4RHBTK03fM96lH+TnTfoADh3eRcWgTL9z133LrR4RF8fDNb/Ps7BsptVlJimvOhMHvlc0/33lSfI/FbrfbvR2EnK20GJZP83YUzksdA4EGeDj56efQVguH5wZ6O5rzM1r/g/tzQPtAPMEoYwDoGDB7+8U1uuRARERERAxNBa2IiIiIGJoKWhERERExNBW0IiIiImJoKmhFRERExND02C4fFRDsuFvSKAKCvR2BfzFa/4P7c0D7QMzO7MeA2dsvrlFB66MsFj36w8zU/9oHImY/BszefnGNLjkQEREREUNTQSsiIiIihqaCVkREREQMTQWtiIiIiBiaCloRERERMTQVtCIiIiJiaCpoRURERMTQVNCKiIiIiKGpoBURERERQ1NBKyIiIiKGpoJWRERERAxNBa2IiIiIGJoKWhERERExNBW0IiIiImJoKmhFRERExNBU0IqIiIiIoamgFRERERFDU0ErIiIiIoYW5O0A5NzsdrCVeDsK5wUEg8Xivu3Z7VBc6r7t/X67p/8vsrp32yGB7tsHRut/cH8OmJ3Zc8DsYwAoB8R4OeDN/rfY7acPb/ElpcWwfJq3o3Be6hgIDHHf9oqs8Gi6+7ZXGSYNhlA3fUQ0Wv+D+3PA7MyeA2YfA0A5IMbLAW/2vy45EBERERFDU0ErIiIiIoamglZEREREDE0FrYiIiIgYmgpaERERETE0FbQiIiIiYmgqaEVERETE0PRiBT+yOWMFj8xILTctLCSSxNiG9GwzlBs7P0BgoLrcnykHRDlgbup/MWsO+F+LhNRWt9Gh8fXYsZN3Iocl389hxmdj2X94Jw/fNNPb4UklUA6IcsDc1P9ithxQQeuHGiS0oWfb28t+7ttpJHdPbszi795meK8XqF4l1ovRSWVQDohywNzU/2K2HNA1tCYQHhJJ4zpXYbfbOfRrhrfDES9QDohywNzU/+LvOaCC1iSyf0veqhHRXo5EvEU5IMoBc1P/iz/ngCkK2tzcXCZMmEBycjJhYWHUrl2bBx98kIKCAu6++24sFgvTp0/3dphuU1hykmMFuRzN/4Ufs7cybcEo9h7cSOPaHUiMbejt8KQSKAdEOWBu6n8xWw74/TW0mzZtonfv3uTk5BAZGUnTpk05dOgQ06ZNIyMjgyNHjgDQqlUr7wbqRnO+fIY5Xz5TblqX5gN5YMDrXorIexZNvYmM9R8z6MmvSWjU5az5B3etYv7z3ajfbgB9HvzICxF6hnLgbHkF8EM2nCqG4ECoXRPq1ASLxduReYZy4AwzjgPq/7PZbLArBw4fB5sdqoRCs0SICPF2ZJ5hthzw64I2NzeXvn37kpOTw7hx43jmmWeIiooCYPLkyTz66KMEBQVhsVho2bKll6N1nz5XjqBry5ux2kr4MXsr6SsmkXssi5DgsLJliq1FjHy1Damt/8yQHk+UTZ88bxhH839m4l8WeyN0t0sd/iYHd61kSdowhkzcTHBYZNm8kqKTLEkbRlhUDFffNcOLUbqfcuCMzFxYuh22HwS7vfy8xBrQtTG0r+t/ha1y4AwzjgPq/zOspbDiB1i9x/HB9veCA6FtEvRsBjFRXgnPY8yWA359ycGYMWPIyspi9OjRvPLKK2XFLMCECRNISUnBarWSlJRE1apVvRipeyXENKBNw550aNybwakT+Pvwz9iVtY6p8+8rWyYkKJQJt85h3lcTyTi0GYDV2z5h7c7PGHvzO94K3e0iqsbS4640jh3OYNW8CeXmrZ73KMcOZ9Dj7pmER8V4KULPUA44fP8jTPsStmWdXcwCZOXBB2sg/X+Ob2z8iXLgDDOOA+p/h8ISeHMZLNx0djELUFIKazNgyhfwU26lh+dRZssBvy1od+7cSXp6OjExMbz44ovnXKZt27YApKSklE07XQB36NCB0NBQLH7wtU2zpE70bDOUFZvT2Z75bdn0holtuanbI0yedwe/HM3i1Y9G8MCA14mpVsuL0bpf/XY30rjLULZ89Sb7t30FQNaOFWxe+jpNutxB/bb9vRyh55kxB3Zlw7/XOFeors1wnPD8mRlz4PfMPg6Ysf9tdnh3JWQcvviyBUUwcwXknvB4WF7j7zngtwXt3LlzsdlsDBkyhCpVqpxzmfDwcKB8Qbt3717mz59PXFwc7du3r5RYK8OQnk8REBDIe188/YfpTxIYEMT9r7YmJTmV1Fa3eilCz+o+dBpVaiSw9K27yM87xJK37qJKjQS63THN26FVGjPlgN0On2507VvXFTvh6EnPxeQLzJQD52L2ccBs/f/DIccHW2cVFDkuT/Jn/pwDflvQLlu2DIDU1NTzLpOVlQWUL2i7du1KdnY2n376KT179vRskJUoISaZ1JRb2bj3K7buW1k2PSgwmKZJnThWkMt17YZ7MULPCo2sTs973uHEr/v54PEUjudm0vOeWYRGVPN2aJXGTDmQmQsH81xbx2aHNXs9E4+vMFMOnIvZxwGz9f/qPa6v830mnCxyeyg+w59zwG8L2p9++gmAOnXqnHO+1Wpl9erVQPmCNiDAb3cJt/V4ggBLAO99eeaT2dZ9K/ly/Wz6dx7NG58+SFHJKS9G6Fl1WlxL89QRnDqRS/Pu91CnxTXeDqnSmSUHNvxUwfUy3RqGTzJLDpyP2ccBs/T/qWLYcdD19UpKYVsF1jMSf80Bi91+rlsljC86Opq8vDy+/fZbOnbseNb8f//739x+++1ERUVx7Nixc14r++yzz/Lcc89xqbuoXbt25OTkuLROSFA4M0dX4OOlC04V5XPvP1MY1HUsfTvez7gZ3WiY2I77+01xeVsjpjeg2Oq+AyAwOJwBz7u//Tu+mc2SmcO5ZsS7NO06zK3b/vjJBpS6aRCojP4H386BS3Hln1+ndorr10SWFB7n/55p6oGIXGf2HPDUGACeGwfcOQaA8c4D4DvjQGT0FfR+9NuLL3gOW/77Aru/ftPNEVWM0XLgUvs/Li6O9evXV2hdv31sV1xcHHl5eWzYsOGsgjY7O5vx48cD0LJlS4/f+JWTk8PBg6595AsLjvBQNGekfTaOuOi69Os0EovFwvhbZnPfq63o3HwALet1dWlb2YcOUVjivgsQg0I93353O5R9CGuRe/ZBZfQ/+HYOXIqC/Ird2VFSXOTyseopZs8Bs48BYLzzAPjOOBBVWPG/th49kmuqccBXxwBX+W1B27NnT3bu3MmkSZO45ppraNjQ8VaMdevWMXToUHJzHc/nqIwXKsTFxbm8TkhQuAciOeO7HxazYnM6M8duKSvoa8XU5+7eL/FK+nDSxm0hPCTyIls5I75WLbd/Q2s0teJrufUbWk/z9Ry4FKUnf67QeqfyfiIhIcHN0VSM2XPA7GMAGO88AL4zDgQEBlFSeILgMNcfLhtYcsQ044CvjQEVqZdO89tLDrKysmjVqhW//vorQUFBNG7cmMLCQvbu3Uvv3r2x2Wx88cUXzJw5k3vuueec23DXJQcVUVoMyw10423qGAh049tWiqzwaLr7tneaJy85mDQYQt30EdFo/Q/uz4FLkVcAf/u/cz979kJuuwqurO+ZmFxl9hzw1BgAnhsH3DkGgHLgUs1fByt3u7ZO9Qh4qj8E+sjtNEbLAW/2v490mfslJiaycuVK+vTpQ1hYGJmZmURHR5OWlsaiRYvYvduR5b+/IUxE/EONSGju4hcsESHQ+tz3kIqIAXVu6Po6nZJ9p5gV1/jtJQcATZo0YeHChWdNz8/PJzMzk4CAAJo3b+6FyETE0wa1h/2/wjEn/vplAYZ0hBC/HhFFzCWuGvRJgUWbnVu+bgyk+sY9oVIBphy+t2/fjt1up2HDhkREnH3B9UcffQTAjh07yv2clJREu3btKi9QcbumXYe5/VID8U3VI2D0NZC2/MJv/wkKhDs6Q7PEyotNvEvjgHn0bOb4/2JFbYPLYXhXCA70fEziGaYsaLdu3Qqc/3KDm2+++Zw/33nnncyePdujsYmI+8RGwYTrHc+lXbUbso6cmWcBrm0BHZMdxa+I+B+LBa5pDs0THS9aWLfPcX32aY3joUtDaFoL/Pgx9KaggvYcjHCfXNYve3g5/U6OFeQSGVaN8YNnkxTXrNwyOzLXMHXB/QCU2kpontSFkTdOIyQo9ILzNu5dxjv/fYxTRflYLBaubNyHu69/ya9fOmFEzuTAaXa7nQlpPdhzcAOf/P0o4Hj24HNzBrEn63tKbday6ad9uOJllqx/D5vdRu3YRjwy+F2qhFf3bKM8ICQIrqoPV9aD/EKYtAjyiyAqDHq39HZ0l8aZHNicsYLH3+5NYmyjsmnTHlhDaHA4n697l49XTi2bnnssixb1uvLsnQsA/8kBf+ZMDthsNmYufIR1uz4nMCCIqpE1efimt0iISebH7K289vEojuYfJjAgiEZXdOCBAa8T+oenTLz3xTO8v/RvvPnQRpITWlViC90jvjrc1B76tYa/fwIniqBqGNx3tbcjuzSXOgacVpFzhK8xZYVysYLWCKbOv5frrxzB7Ed3Mzj1UV5OH3bWMvVqpfD6g+tIG7uJmWO3cjT/MJ99+8ZF50WF1+CJIfN4Z/wO3njwe7b/9C1Lvp9Tmc0TJziTA6fN/2YK8TXL374fGBjM4NRHmTRi6VnLf797CV+se5epo9fwzvgdNEhsy6zFT7i7CZXKYoGo8DM3fHj48dOVwtkcSIxtRNrYTWX/Tp/IerUfXm56dFQcPVoPAfwzB/yRMzmwZsenbM9cTdrYzcwct4XWyT2YtfhxAIKDwhh943RmTfiBGWM3U1hcQPrySeXW/2H/d+zKWsflNYx/12RI0JlvYjUGnOHqOcIXmbKgXbZsGXa7nT59+ng7lArJyz/M7qz19GxzOwB/ajGIX44e4GBu+RfRh4VEEBQYDIC1tJiiklNlz5m70LzkhNbE16wHQEhwGPVrteLnvMzKaJo4ydkcAMjM2c632z/h1tTHyk0PCQqldfLV5/zGbd+hzTSv24WI357h2KHx9Xy14V/ub4hUmCs54Iyd+//H0fzDdGzWD1AOGIGzOWDBQrG1iOKSQux2OycLjxNbzXHReGJsA+rVcvypIjAgkEaJ7ck5klm2bmHxSaZ/MpqHBqVVTqPEae4aAypyjvBFpixoje6XoweIrhpPYKDjihGLxcJlNa7g8NH9Zy2bcyTT8Uq7Z2OIDK9G344jnZp32pHjOazc8hFXNrnBcw0SlzmbA9bSEqZ8dA8PDkojIMD5ux0aJLZlw56lHDmeg91u56uN/+Zk0QmOnzxy8ZWlUrgyDmQfyeD+V9swamp7Pv3tLzF/9Pl379Cj7dCyD7rKAd/nbA5c1bQvKfW7M/hvcQz+Wzwb937Fndf97aztnSouYPF3b9Op2ZnXRr+1aAI3dLyfy6rX9mxjxGXuGAMqeo7wRSpo/VxcdBJpYzfz4dM5lFiLWLVtgVPzAAoKj/PUu325pfsEGtXW0x2M6F9LnqNL84HUubyJS+u1Sk7l5m6P8OS7NzDmtauoHhkLQGCAKS+7N7TkhDbMfSKLNx/awLN3fszCNTP4evOH5ZY5VVzAik3z6N3h7rJpygH/sTtrPZk525j71EHmPXWI1sk9mDr/vnLLlFiLeeH9wbRteC1dWgwAHJedHM77iV7th3sjbHGTC40BFT1H+CKNTAYUW702R45nU1pqJTAwCLvdzuG8/VxW/YrzrhMeWoXurW5l2YZ/k9rq1ovOO1l4gsff7kWnZv25qdtYj7ZHXOdsDmzZ9zWH8/bzf99Op9Rm5WTRcW6fmMT0MeuoXiX2gr+jX6eR9Ovk+NZ+x09ria2WSGRYVY+1SVzjbA78vs9iqyeS2vo2tv64km4pt5RN/2bzf6hzeTPqXF7+IZzKAd/mbA4s+X4OrX73p+Nr2t3JY29dWzbfWlrCC+8PJjoqnpH9z9wkuGnvMvYc3MDtE5MA+OVYFk/Mup6HBqXRsWlfj7dPLswdY8ClnCN8jb6hNaAaVS4jOaENSze8D8DKrfOJqZ5IQkxyueUO5u7FWloCOD59r972MXXjW1503qmifP76di/aNerFkJ5PVlazxAXO5sCUkSv59xM/8f7jmUwZuYqI0Kq8/3imUwPVr8ezAcc1dO998TS3dJ/g/oZIhTmbA78ez8ZmswGOD6prdywkuVbrcst8vu4dev3u29nfrwvKAV/lbA7ER9dj095llFiLAVi7cyFJcY6XCpWWWnnh/VuJiojm4Ztmlt1LAXD39S8y76mDvP94Ju8/nklstUReuOu/KmZ9hDvGgEs5R/gafUNrUA8NSuPl9GHMXTaRiLCqjL/lXQD+8Z+/0LFpPzo168emvcv4ZNU0AgICKbVZaZ3cg9t7PgVwwXkLVk1l14HvKCwuYNVWx2UIXVNuZkgP3eHsS5zJgYsZ8Y+WHCv4hZNFx7nt+URS6qfy2G2OG38ee+ta7HYbJaXF9GwzlP6dR3u0PeI6Z3Jg5db5LFzzJoEBQZTarHRteTPX/e5PyAcO7yLj0CZeuOu/Z21fOeD7nMmBfp1Hsf/wTu6dkkJQQDA1ouJ4aNAMAFZsTmfVtgXUi2/JfVMcRU6zpM6MGfi619okznPHGHAhFzpH+BqL3QgPXTWh0mJYPs3bUTgvdQwEhrhve0VWeDTdfdurDJMGQ6ibPiIarf/B/TngKc8scLwOt1o4PDfQ29Gcn9lzwOxjACgHPMUoYwAYLwe82f+65EBEREREDE0FrYiIiIgYmgpaERERETE0FbQiIiIiYmgqaEVERETE0FTQioiIiIih6bFdPspuB1uJt6NwXkAw/O553JfMbofiUvdtrzKEBLpvHxit/8H9OeApRnlkj9lzwOxjACgHPMUoYwAYLwe82f96sYKPslh8/1l+nmSxuPd5jkZj9v4X5YDZxwBQDohywBW65EBEREREDE0FrYiIiIgYmgpaERERETE0FbQiIiIiYmgqaEVERETE0FTQioiIiIihqaAVEREREUNTQSsiIiIihqaCVkREREQMTQWtiIiIiBiaCloRERERMTQVtCIiIiJiaCpoRURERMTQVNCKiIiIiKGpoBURERERQ1NBKyIiIiKGpoJWRERERAwtyNsByLnZ7WAr8XYUzgsIBovF21H4D6P1P7g/B+x2KC513/Z+v93T/xdZ3bvtkED37QPlgJg9B8w+BoDxcsCbY4DFbj/dteJLSoth+TRvR+G81DEQGOLtKPyH0fof3J8DRVZ4NN1926sMkwZDqJu+JlAOiNlzwOxjABgvB7w5BuiSAxERERExNBW0IiIiImJoKmhFRERExNBU0IqIiIiIoamgFRERERFDU0ErIiIiIoamglZEREREDE0vVvAjmzNW8MiM1HLTwkIiSYxtSM82Q7mx8wMEBqrL/ZlyQJQD5qb+F7PmgP+1SEhtdRsdGl+PHTt5J3JY8v0cZnw2lv2Hd/LwTTO9HZ5UAuWAKAfMTf0vZssBFbR+qEFCG3q2vb3s576dRnL35MYs/u5thvd6gepVYr0YnVQG5YAoB8xN/S9mywFdQ2sC4SGRNK5zFXa7nUO/Zng7HPEC5YAoB8xN/S/+ngMqaE0i+7fkrRoR7eVIxFuUA6IcMDf1v/hzDuiSAz9UWHKSYwW52O2O62Y+WzODvQc30rh2BxJjG3o7PKkEygFRDpib+l/MlgOmKGhzc3OZPHkyCxYsICsri9jYWAYOHMjEiRMZM2YMs2bN4rXXXmP06NHeDtUt5nz5DHO+fKbctC7NB/LAgNe9FJFvsNmgsAQCAiA0CCwWb0fkOcqBMxZNvYmM9R8z6MmvSWjU5az5B3etYv7z3ajfbgB9HvzICxF6hnLg3IqtUFIKYcEQ6Md/o1T/l2fGccBsOeD3Be2mTZvo3bs3OTk5REZG0rRpUw4dOsS0adPIyMjgyJEjALRq1cq7gbpRnytH0LXlzVhtJfyYvZX0FZPIPZZFSHBY2TLF1iJGvtqG1NZ/ZkiPJ8qmT543jKP5PzPxL4u9EbpH7P8VVu2GjT85TmQAVULhqmTo3ABqRHo3Pk9QDpyROvxNDu5ayZK0YQyZuJngsDMdXlJ0kiVpwwiLiuHqu2Z4MUr3Uw6cUVQC63+EVXsg++iZ6Q0uhy4NoXmi/xW36v/yzDgOmC0H/OwQLi83N5e+ffuSk5PDuHHjyM7OZsOGDeTk5DBp0iQWLVrEunXrsFgstGzZ0tvhuk1CTAPaNOxJh8a9GZw6gb8P/4xdWeuYOv++smVCgkKZcOsc5n01kYxDmwFYve0T1u78jLE3v+Ot0N3KWgrvr4Z/fg7f7TtTzALkF8HS7fC3/4Ovf/BejJ6iHDgjomosPe5K49jhDFbNm1Bu3up5j3LscAY97p5JeFSMlyL0DOWAw4+/OI7z/6wrX8wC7PkZ3l0JL/8X8gq8Ep7HqP/LM+M4YLYc8OuCdsyYMWRlZTF69GheeeUVoqKiyuZNmDCBlJQUrFYrSUlJVK1a1YuRelazpE70bDOUFZvT2Z75bdn0holtuanbI0yedwe/HM3i1Y9G8MCA14mpVsuL0bqHzQZzVsP6zAsvZ7fDx9/D8p2VEpbXmDEHfq9+uxtp3GUoW756k/3bvgIga8cKNi99nSZd7qB+2/5ejtDzzJgDP+XCG19BQdGFl8s5BtOWwLFTlROXN5ix///I7OOAv+eA3xa0O3fuJD09nZiYGF588cVzLtO2bVsAUlJSyqZ99NFHDBo0iDp16hAREUHjxo154oknyM/Pr5S4PWVIz6cICAjkvS+e/sP0JwkMCOL+V1uTkpxKaqtbvRShe63ZC1sOOL/8/204+9sbf2O2HPij7kOnUaVGAkvfuov8vEMseesuqtRIoNsd07wdWqUxUw7YbDB7Vfm/zFxIXgH85zvPxuRtZur/8zH7OODPOeC3Be3cuXOx2WwMGTKEKlWqnHOZ8PBwoHxB+8orrxAYGMjEiRNZvHgx999/P2+++Sa9evXCZrNVSuyekBCTTGrKrWzc+xVb960smx4UGEzTpE4cK8jlunbDvRih+9jtjmtmXbW6AusYiZly4FxCI6vT8553OPHrfj54PIXjuZn0vGcWoRHVvB1apTFTDuw45PplBNsPwhFjf3dxQWbq//Mx+zjgzzngtwXtsmXLAEhNTT3vMllZWUD5gvazzz7jww8/ZMiQIXTr1o0HH3yQ6dOns3r1alatWuXZoD3sth5PEGAJ4L0vz3wy27pvJV+un03/zqN549MHKSox/t/cMnMh+5jr6637EYqs7o/Hl5glB86nTotraZ46glMncmne/R7qtLjG2yFVOrPkwLd7XF/Hboe1/ve8+XLM0v8XYvZxwF9zwGK32+3eDsITateuTVZWFhs3bjznEwysVivx8fHk5uaSkZFBvXr1zrut3bt306hRIz744ANuu+02l2Np164dOTk5Lq0TEhTOzNEVGJFdcKoon3v/mcKgrmPp2/F+xs3oRsPEdtzfb4rL2xoxvQHFVt84AOp2+DNtB02u0Lqfv9yV/Nx9bo7IdZXR/+DbORAYHM6A592/D3Z8M5slM4dzzYh3adp1mFu3/fGTDSh104lAOXBpej+6hsjo2i6vl7X1v6x9f4QHInKd0c4D4N4c8NQYAJ4bB9w5BoDxcuBS+z8uLo7169dXaF2/fWxXQYHjb02nTp17x6anp5Obm0tUVBR169a94LaWL18OQJMmTSoUS05ODgcPHnRpnbDgiAr9LlekfTaOuOi69Os0EovFwvhbZnPfq63o3HwALet1dWlb2YcOUVhy0kORuqZmfsXj+DXvGLku9pUnVEb/g2/nQFBo5ewDdzqUfQhrkXv2gXLgEgVU7PRWYrW5PF57itHOA+DeHDD7GADGywFvjgF+W9DGxcWRl5fHhg0b6NixY7l52dnZjB8/HoCWLVtiucAT9g8ePMhTTz1Fr169Kvys2ri4OJfXCQkKr9DvctZ3PyxmxeZ0Zo7dUtb+WjH1ubv3S7ySPpy0cVsID3H+Aa3xtWr5zDcz4cGuX+tst9uxWCxUiwgiNCHBA1G5xtP9D76fA4HBnt8H7lYrvpZbv6H1NF/PgUthPXUMqse7vJ6l9CQJPjAGgPHOA+DeHDD7GADGy4FL7f+K1Eun+e0lB2PGjOG1116jdu3aLF26lIYNHa95W7duHUOHDmXfvn2UlJQwatQopk+ffs5t5Ofn0717d3Jycli3bh3x8a4PjhVVWgzLDXTTZeoYCAzxdhQOJ4vh2QVQ7OTdzaclxcBD13kmJlcZrf/B/TlQZIVH0923vdM8ecnBpMGOt9C5g3Lg0ny+BT7f6vp693SDZonuj6cizJ4DnhoDwHPjgDvHADBeDnhzDPDbm8ImTJhAzZo1OXDgAM2aNaNFixY0aNCADh06UK9ePa6++mqg/A1hv3fq1Cn69u3Ljz/+yJdfflmpxaxcmogQaHvhq0jOqYv/vdpaxLQ6JkOAi6+3rhEJTYz16E0R+Y3fFrSJiYmsXLmSPn36EBYWRmZmJtHR0aSlpbFo0SJ273Y8o+lcBW1JSQk33XQT69evZ/HixTRt2rSyw5dLdHVTCA92fvnEGpByhefiEZHKVS0CujV2bZ0+KRDgt2dFEf/mt9fQguMmroULF541PT8/n8zMTAICAmjevHm5eaefXfvVV1/x3//+lw4dOlRWuOJGsVFwT3eYuQIKSy68bFw1uCcVggMrIzLxtqZdh7n9UgPxTX1bwYlTF39jIEC/1tCuAn/ZEWPSOOB//LqgPZ/t27djt9tp2LAhERHl7yAcNWoU//nPf3jssceIiIhg7dq1ZfPq169PbGxsZYcrFVTvMsc1sZ9vcbw1zPaHq8XDgqFDPejV0nGZgoj4l4AA+HMnqBMDX/8Aued4aUKdmtCzGbRw/QlfIuJDTFnQbt3quFPgXJcbLF68GICXXnqJl156qdy8d999l2HDhnk8PnGfuGow7E9w7CRs2g//3ey40SA8GJ4d6N6L90XE9wRY4E+NoHND2JUNs1c6xoDQIBjdE2rX9HaEIuIOpjydX6igzczMrORoKibrlz28nH4nxwpyiQyrxvjBs0mKa1ZuGZvNxsyFj7Bu1+cEBgRRNbImD9/0FgkxyfyYvZXXPh7F0fzDBAYE0eiKDjww4HVCf3tMyt/m3MT2n77lyPFsPv5bHlXCq3uhle5z+nq6ZTscJ7OQIOMXs87kwI7MNUxdcD8ApbYSmid1YeSN0wgJCr3gvM0ZK3j87d4kxjYq29a0B9aU5Yf4BmdyAODH7K1M/+QB8vJ/BmB4rxf4U4uBZfPtdjsT0nqw5+AGPvn7UQByjmRy50v1SYprUbbcM3fMp1ZMfc82ykMCLI4bvsKCHWNAWLB/FLPO5sDi795h3vKXsNtstEq+mjED3yAoMNipY/1c+SG+wZn+zzmSycvpw9h7aCNxNeqSNnZT2byNe5fxzn8f41RRPhaLhSsb9+Hu618iICDAcGOAwU/pFXOhgtYops6/l+uvHMF17YfxzZaPeDl9GK8/uK7cMmt2fMr2zNWkjd1MUGAw/176PLMWP85TQz8kOCiM0TdOp16tlpTaSnnxgz+TvnwSd1z7LAA3XHUfDwx8g1ueu9wLrRNnOJMD9Wql8PqD6wgKDMZms/G3OYP47Ns3GNT14QvOA0iMbVRu4BPf40wOFBaf5OnZ/Xn01jk0r9uFUlspJ04eKbfM/G+mEF+zPnsObig3PTw0Sjng45zJgewjPzL7i6d488EN1Ii6nKdn92fR2pn07zwKuPixfr78EO9zpv8jwqoyvNfzFBQeY9biJ8rNiwqvwRND5hFfsx7FJYVMmNmTJd/P4br2wwBjjQGmvJ9z2bJl2O12+vTp4+1QKiQv/zC7s9bTs83tAPypxSB+OXqAg7l7yy1nwUKxtYjikkLsdjsnC48TW83xgMXE2AbUq9USgMCAQBoltifnSGbZum0a9qRGlcsqp0HiMmdzICwkgqBAx+MerKXFFJWcKnt49oXmie9zNgeWbfyAJldcRfO6XQDH8V69ypl7ATJztvPt9k+4NfWxygte3MLZHFi55SM6Nu1HdNU4LBYLN1x1H8s3zXXqdyg/fJez/V81IprmdbsQdo4XJCQntCa+Zj0AQoLDqF+rFT/nZXo8dk8wZUFrdL8cPUB01XgCAx1fsFssFi6rcQWHj+4vt9xVTfuSUr87g/8Wx+C/xbNx71fced3fztreqeICFn/3Np2a9a+U+OXSOZsD4Phz073/TGHQszFEhlejb8eRTs3LPpLB/a+2YdTU9nz67Rueb5S4xNkc2P/zDoKDQnly1g3c+89WTJp7B0fzfwHAWlrClI/u4cFBaQQEnP2Yj8LiAkZNbc/9r7bhX0v+RqnNxbeViEc5mwOHj+7n8hp1yn6Oi04qt8z5jvWL5Yd4lyvnAWccOZ7Dyi0fcWWTG8qmGWkMUEHrx3ZnrSczZxtznzrIvKcO0Tq5B1Pn31dumRJrMS+8P5i2Da+lS4sBXopUPCkuOom0sZv58OkcSqxFrNq24KLzkhPaMPeJLN58aAPP3vkxC9fM4OvNH3qrCXIJSm1WNu5ZykOD0pjx8EZiqiUw7bdrp/+15Dm6NB9IncubnLVedNV45j51kNcfXMekEUvZ9uNKPvr6H5UdvnjYhY71C+WH+JeCwuM89W5fbuk+gUa12wHGGwNU0BpQbPXaHDmeTWmpFXBcsH84bz+XVS//ZoAl38+hVfLVVAmvTkBAANe0u5NNGcvL5ltLS3jh/cFER8Uzsv/USm2DXBpnc+D3wkOr0L3VrSzb8O+LzosMq0pkeLXfflciqa1vY+uPKz3QEqkoZ3PgsupXkFI/lZhqCVgsFnq0uZ2d+x2PI9yy72s+Wf0at09M4uE3unCy6Di3T0ziaP4vhASFll12VDUimuva38U25YBPcSUHfs77qeznnCOZZctc6Fi/UH6I91XkPHAuJwtP8PjbvejUrD83dRtbNt1oY4AKWgOqUeUykhPasHTD+wCs3DqfmOqJJMQkl1suProem/Yuo8RaDMDanQtJinO8SKK01MoL799KVEQ0D980U9dOGoyzOXAwdy/WUsebJUqsxaze9jF141tedN6vx7Ox2WyAY7Bbu2MhybVaV0rbxDnO5kC3lFvYlbWOgsLjAHz3w3+pF++4IXbKyJX8+4mfeP/xTKaMXEVEaFXefzyT6lViycs/XJYfxb99e18/QTngS5zNgT+1GMSaHZ9y5HgOdrudhWtn0L3VrcCFj/UL5Yd4n7P9fyGnivL569u9aNeoF0N6PlluntHGAFM+5cAfPDQojZfThzF32UQiwqoy/pZ3AfjHf/5Cx6b96NSsH/06j2L/4Z3cOyWFoIBgakTF8dCgGQCs2JzOqm0LqBffkvumOBK0WVJnxgx8HYAn3unDvuzNAPzllWYkxDTgH/evqPyGynk5kwOb9i7jk1XTCAgIpNRmpXVyD27v+RTABeet3DqfhWveJDAgiFKbla4tb+a69sO91lY5N2dy4LIaV3Db1Y/z0PROWCwBxFRL4KGbZl5029t+XMWcL54uy49WyVfz5x5PXHQ9qVzO5EB8zXrcee1zPPR6ZwBS6nfnhqvuBXSsG50z/V9YfJLhkxtSYi2ioPAYtz2fSM82Q7n7+hdZsGoquw58R2FxAau2Oi4565pyM0N6PGG4McBit9vtF19MKltpMSyf5u0onJc6BgIN8LatZxbAsVNQLRyeG3jx5b3FaP0P7s+BIis8mu6+7VWGSYPd93xj5YBnGGUMAOWA2ccAMF4OeHMM0CUHIiIiImJoKmhFRERExNBU0IqIiIiIoamgFRERERFDU0ErIiIiIoampxz4KLsdbCXejsJ5AcFghEfZGuUOZ6P1P7g/B+x2KPbdtyyeU0ig+/aBcsAzjDIGgHLA7GMAGC8HvDkG6Dm0Pspi8f3H34jnqP8d+8Cdj78xGuWAmD0HzD4GgHLAFbrkQEREREQMTQWtiIiIiBiaCloRERERMTQVtCIiIiJiaCpoRURERMTQVNCKiIiIiKGpoBURERERQ1NBKyIiIiKGpoJWRERERAxNBa2IiIiIGJoKWhERERExNBW0IiIiImJoKmhFRERExNBU0IqIiIiIoamgFRERERFDU0ErIiIiIoamglZEREREDE0FrYiIiIgYWpC3A5Bzs9vBVuLtKJwXEAwWi/u2Z7dDcan7tvf77Z7+v8jq3m2HBLpvHxit/8H9OWB2Zs8Bs48BoBwQ4+WAN/vfYrefPrzFl5QWw/Jp3o7CealjIDDEfdsrssKj6e7bXmWYNBhC3fQR0Wj9D+7PAbMzew6YfQwA5YAYLwe82f+65EBEREREDE0FrYiIiIgYmgpaERERETE0FbQiIiIiYmgqaEVERETE0FTQioiIiIih6Tm0fmRzxgoemZFablpYSCSJsQ3p2WYoN3Z+gMBAdbk/Uw6IcsDc1P9i1hzwvxYJqa1uo0Pj67FjJ+9EDku+n8OMz8ay//BOHr5pprfDk0qgHBDlgLmp/8VsOaCC1g81SGhDz7a3l/3ct9NI7p7cmMXfvc3wXi9QvUqsF6OTyqAcEOWAuan/xWw5oGtoTSA8JJLGda7Cbrdz6NcMb4cjXqAcEOWAuan/xd9zQAWtSWT/lrxVI6K9HIl4i3JAlAPmpv4Xf84BXXLghwpLTnKsIBe73XHdzGdrZrD34EYa1+5AYmxDb4cnlUA5IMoBc1P/i9lywBQFbW5uLpMnT2bBggVkZWURGxvLwIEDmThxImPGjGHWrFm89tprjB492tuhusWcL59hzpfPlJvWpflAHhjwupciksqmHBDlgLmp/8VsOeD3Be2mTZvo3bs3OTk5REZG0rRpUw4dOsS0adPIyMjgyJEjALRq1cq7gbpRnytH0LXlzVhtJfyYvZX0FZPIPZZFSHBY2TLF1iJGvtqG1NZ/ZkiPJ8qmT543jKP5PzPxL4u9EbrbLZp6ExnrP2bQk1+T0KjLWfMP7lrF/Oe7Ub/dAPo8+JEXIvQM5UB51lLYcgB2HIRTJRASBLWjoUM9qBJ28fWNSDlwhhnHAfX/2XJPwNoM+OU4lNohKgxa14EGl4PF4u3o3M9sOeDX19Dm5ubSt29fcnJyGDduHNnZ2WzYsIGcnBwmTZrEokWLWLduHRaLhZYtW3o7XLdJiGlAm4Y96dC4N4NTJ/D34Z+xK2sdU+ffV7ZMSFAoE26dw7yvJpJxaDMAq7d9wtqdnzH25ne8FbrbpQ5/k7CoGJakDaOksKDcvJKikyxJG0ZYVAxX3zXDSxF6hnLAwW6Hr3+A5z6BOathfSZsPwgbf4JPN8KzH8O8tVBU4u1I3U85cIYZxwH1/xlH8mHmcnjhU1i6HTYfgG1ZsGYvvPEVvLjQ8bO/MVsO+HVBO2bMGLKyshg9ejSvvPIKUVFRZfMmTJhASkoKVquVpKQkqlat6sVIPatZUid6thnKis3pbM/8tmx6w8S23NTtESbPu4Nfjmbx6kcjeGDA68RUq+XFaN0romosPe5K49jhDFbNm1Bu3up5j3LscAY97p5JeFSMlyKsHGbMAbsdFnwPH38PJwrPvYzV5vjGZvpSOFVcufFVNjPmwGkaB8zb/4ePw6tfwI5DYL/AMu98DWv3Vmpolc7fc8BvC9qdO3eSnp5OTEwML7744jmXadu2LQApKSll01auXEnPnj2Jj48nNDSUxMREBg8ezM6dOyslbk8Z0vMpAgICee+Lp/8w/UkCA4K4/9XWpCSnktrqVi9F6Dn1291I4y5D2fLVm+zf9hUAWTtWsHnp6zTpcgf12/b3coSVw2w5sGo3rNzl3LIHjsC/Vns2Hl9gthz4PY0D5uv/YiukLYfj5/lA+3t24MPvIONnj4flVf6cA35b0M6dOxebzcaQIUOoUqXKOZcJDw8Hyhe0eXl5tGjRgmnTpvHll18yadIktm/fTseOHcnKMu7fJBJikklNuZWNe79i676VZdODAoNpmtSJYwW5XNduuBcj9KzuQ6dRpUYCS9+6i/y8Qyx56y6q1Eig2x3TvB1apTFTDpTaHH9adMWOQ5B1xDPx+Aoz5cC5mH0cMFv/b8iEX/OdX95mh6U7PBaOT/DnHPDbgnbZsmUApKamnneZ0wXq7wvafv36MWXKFG6++Wa6devGkCFDWLBgAceOHWP+/PmeDdrDbuvxBAGWAN778swns637VvLl+tn07zyaNz59kKKSU16M0HNCI6vT8553OPHrfj54PIXjuZn0vGcWoRHVvB1apTJLDmzLgmMVaMbqPe6PxdeYJQfOReOAefrfbnf8lcZVPxxy3Dzmz/w1B/y2oP3pp58AqFOnzjnnW61WVq92/I3x9wXtudSsWROAoCDffihESv3uLHnZzs3dHznn/DqXN+GLyaW8ct9yAE4V5fNy+jDu7v0SI/tNpXqVy5i1+PHKDLlS1WlxLc1TR3DqRC7Nu99DnRbXeDskt1MOOOw8VLnr+RLlwIX5+zig/ncoKIKsPNfXswM/ZLs9nEpl1hzw7QrtEhQUOO5kPXXq3J8y0tPTyc3NJSoqirp16541v7S0FJvNxk8//cRf//pX4uLiuOWWWyoUS7t27cjJyXFpnZCgcGaO9uzXRWmfjSMuui79Oo3EYrEw/pbZ3PdqKzo3H0DLel1d2laDhg0otrrvE11gcDgDnnd/++MbdGTb8pnEN+jo9m03bNCAUjd9qq2M/gffzoFLceWQN6ndsq/L6/2Sl09iYmMPROQ6s+eAp8YA8Nw44M4xAIx3HgDfGQciaybRe8KqCq37zN9eZNcK33hWq9Fy4FL7Py4ujvXr11doXb8taOPi4sjLy2PDhg107Fh+0MrOzmb8+PEAtGzZEss5HkDXrVu3sm9wk5OTWbZsGbGxsRWKJScnh4MHD7q0TlhwRIV+l7O++2ExKzanM3PslrL214qpz929X+KV9OGkjdtCeEik09vLPnSIwpKTbosvKNSz7feEQ9mHsBa5Zx94uv/B93PgUpw4+muF1is+dcLlY9VTzJ4DZh8DwHjnAfCdcSCywFbhdX89fMg044AvjwGu8tuCtmfPnuzcuZNJkyZxzTXX0LCh4zVv69atY+jQoeTm5gLnf6HCO++8w9GjR/nxxx95+eWXufbaa1m9ejVXXHGFy7HExcW5vE5IULjL67iiQ+PefPL3o2dN7995FP07j3J5e/G1arn9G1qjqRVfy63f0Hqar+fApSj8tQIXzwHHDm4hISHBzdFUjNlzwOxjABjvPAA+NA5YAijIyyKyRqLTq9jtdiwWC7YTmaYZB3xtDKhIvXSa3xa0EyZM4IMPPuDAgQM0a9aMxo0bU1hYyN69e+nduzdJSUl88cUX571+tlGjRgBceeWV9OrVi6SkJCZPnsz06dNdjqUiX5+XFsNyA914u2f3HgJD3Le9Iis8mu6+7VWG3Xv2EOqmI8po/Q/uz4FLUVgCzyxw5JErnht9HU0m+sbTTMyeA2YfA0A5cKmWbINFm51f3mKxkBgNm7/9zGfeHGa0HPBm//vtTWGJiYmsXLmSPn36EBYWRmZmJtHR0aSlpbFo0SJ273Z8g3OxG8IAqlevTnJyMnv3+vlTl0X8RFgwXFnftXUurwqN4j0Tj4hUvqvq4/IHjG6N/PM1uGbgt9/QAjRp0oSFCxeeNT0/P5/MzEwCAgJo3rz5Rbdz+PBhdu3axZVXXumJMEXEA/q2hoN5kHH44stGhsLd3SBAJzIRvxEVDnd2gbe/djxj9mI6N4B2Z98jLgbh1wXt+Wzfvh273U7Dhg2JiCh/wfXtt99OcnIyrVq1onr16uzZs4cpU6YQFBTEww8/7KWIxV2adh1G067DvB2GVILgQLg3FeauhY0/nX+5uGpwV1e4zH/ffi1/oHHAPJomwH1XO94EeL5XYAcGQI+m0Kulvp01MlMWtFu3bgXOfbnBVVddxZw5c5g6dSqFhYXUrl2b1NRUHn/88fM+01ZEfFNIkOMbml4t4ds9sOOg46HpdiAoAEakQoPLdRIT8WcN4+CZG2HLAVizF/Yedrx4IcACvVs6Lk2IMt49iPIHKmj/YPTo0YwePbqyQ3JZ1i97eDn9To4V5BIZVo3xg2eTFNfsrOUWf/cO85a/hN1mo1Xy1YwZ+AZBgcFl8+12OxPSerDn4IayOx1/zN7Kax+P4mj+YQIDgmh0RQceGPA6oQa869ifOZsDP2ZvZfonD5CX73hJ+fBeL/CnFgPZuHcZ7/z3MU4V5WOxWLiycR/uvv4lAgICyD7yI3+fcxOltlJsNiu1L2/Cw4NmEhVRo7Kb6RaXV4UBbR3/nlngeItYZKjjROcvnMkHm83GW4smsH7X55TarDRL6syYgW8SHOS4i+PDFS+zZP172Ow2asc24pHB71IlvLoXWiPOcqbfP1/3Lh+vnFr2c+6xLFrU68qzdy7gVFE+z80ZxJ6s7ym1Wc+6491fciIoENokOf6dHgOiwuCai1916NMu9bjPOZLJnS/VJymuRdnyz9wxn1oxjpsQ5i2fxJL17xEUGEJIcBij+k+j8RUdKrWNzvLbm8Iu5EIFrVFMnX8v1185gtmP7mZw6qO8nD7srGWyj/zI7C+eYsr9K3nvsb3k5f/MorUzyy0z/5spxNcsf/dMcFAYo2+czqwJPzBj7GYKiwtIXz7Jk82RCnAmBwqLT/L07P4M7/U8s8bv5K1x22hR908ARIXX4Ikh83hn/A7eePB7tv/0LUu+nwNAzaq1mDJqFWljN/HWI9uoWbUWc5Y8W4mtE1c5kw+fr3uHvQc38MZDG3hn/E4slgA+XuUodL7fvYQv1r3L1NFreGf8DhoktmXW4icquRXiKmf6vVf74aSN3VT2Lzoqjh6thwAQGBjM4NRHmTRi6VnrKSd836Ue9wDhoVHl8uN0Mbv34CY++/YNpo/5jrSxm+jfaTTTP/HdL/xMWdAuW7YMu91Onz59vB1KheTlH2Z31np6trkdgD+1GMQvRw9wMLf8UxhWbvmIjk37EV01DovFwg1X3cfyTXPL5mfmbOfb7Z9wa+pj5dZLjG1AvVotAQgMCKRRYntyjmR6tlHiEmdzYNnGD2hyxVU0r9sFcPRn9SqOF4QkJ7QmvmY9AEKCw6hfqxU/52U6fg4KLftGvtRWSmFxARb0d3lf5Ww+ZBzaTOsGPQkOCsFisdC+cW+Wfv8vAPYd2kzzul2ICIsCoEPj6/lqw78qtyHiEmf7/fd27v8fR/MP07FZP8BxrLdOvvqc37oqJ3ybO477C7FYLFhtJRQWO968ml94lJhqzj/Xt7KZsqA1ul+OHiC6ajyBgY4rRiwWC5fVuILDR/eXW+7w0f1cXuPMdb9x0Ully1hLS5jy0T08OCiNgIDA8/6uU8UFLP7ubTo16++BlkhFOZsD+3/eQXBQKE/OuoF7/9mKSXPv4Gj+L2dt78jxHFZu+Ygrm9xQNq3EWsy9/2zFTc/GcDB3D3de+5xnGyUV5mw+NEhsy5odn1JQeBxraQnfbP6w7ENMg8S2bNizlCPHc7Db7Xy18d+cLDrB8ZNHKrs54iRn+/33Pv/uHXq0HVru0rPzUU74Nncc9wCFxQWMmtqe+19tw7+W/I1SWykA9WulMOhPDzP0xbrc9nwiC76ZwugbX6u09rlKBa1J/WvJc3RpPpA6lzc57zIl1mJeeH8wbRteS5cWAyoxOnGXUpuVjXuW8tCgNGY8vJGYaglMW3B/uWUKCo/z1Lt9uaX7BBrVblc2PTgohLSxm/jw6Z+5IrYxC9emVXb44mbXtRtG+0a9GPdmN8a92Y2E2IYEBjhOhq2SU7m52yM8+e4NjHntKqpHOr7JPz1fjO9UcQErNs2jd4e7nVpeOeEfLnTcR1eNZ+5TB3n9wXVMGrGUbT+u5KOv/wE4LltctXUBsx/dy9wnsxjY9WGef3+wN5tyQcpKA4qtXpsjx7MpLbUSGBiE3W7ncN5+Lqte/rW8l1W/gkO/ZpT9nHMks2yZLfu+5nDefv7v2+mU2qycLDrO7ROTmD5mHdWrxGItLeGF9wcTHRXPyP5TEd/iSg6k1E8lpprjNY492tzOX9++rmz+ycITPP52Lzo1689N3cae83cFB4VwbfvhTPnoHganTvBco6TCnM0Hi8XCHdc+yx3XPgvA8k3zqPO7G0j6dRpJv04jAdjx01piqyUSGabnmfkqZ/v9tG82/4c6lzejzuVNnf4dygnf5Y7jPiQolJAqlwFQNSKa69rfxfKNHzA4dQKrtsynbnwLYqrVAuC69sN5/ZMHKLEWl91I6kv0Da0B1ahyGckJbVi64X0AVm6dT0z1RBJiksst96cWg1iz49OyPxctXDuD7q1uBWDKyJX8+4mfeP/xTKaMXEVEaFXefzyT6lViKS218sL7txIVEc3DN83Eomca+Rxnc6Bbyi3sylpHQeFxAL774b/Ui3fcDHmqKJ+/vt2Ldo16MaTnk+XW+znvJwqLTwKOO2S/2fIf6sW39HSzpIKczYfikkJOnMwD4FhBLvOWvcQt3c98SPn1eDbguJnwvS+eLjdPfI+z/X7a5+veoZeT386eppzwXe447vPyD2MtLXEsZy1i1bYF1E9oDUBczXpsz1zNqaJ8AP63YyGJsQ19spgFfUNrWA8NSuPl9GHMXTaRiLCqjL/lXQD+8Z+/0LFpPzo160d8zXrcee1zPPR6ZwBS6nfnhqvuvei2V2xOZ9W2BdSLb8l9UxyJ7XjMx+uea5C4zJkcuKzGFdx29eM8NL0TFksAMdUSeOgmx5MuFqyayq4D31FYXMCqrQsA6JpyM0N6PMG+7C28+9vdzHa7jeSENozqb6AXipuQM/lQUHiMcTO6E2AJwGa3MaDLg3Rs2rdsG4+9dS12u42S0mJ6thlK/86+e0ezODjT7wAHDu8i49AmXrjrv2dtY8Q/WnKs4BdOFh3ntucTSamfymO3OW4aUk74tks97rf9uIo5XzxNQEAgpTYrrZKv5s89HGN/l+YD2H1gHaOmtiM4KJSwkEj++ucPvNbWi7HY7XYnXggnla20GJYbqH5IHQOBbvzQVmSFR9Pdt73KMGmw6+8NPx+j9T+4Pwc85fQzKKuFw3MDvR3N+Zk9B8w+BoBywFOMMgaA8XLAm/2vSw5ERERExNBU0IqIiIiIoamgFRERERFDU0ErIiIiIoamglZEREREDE0FrYiIiIgYmh7b5aPsdrCVeDsK5wUEgzvfv2C3Q3Gp+7ZXGUIC3bcPjNb/4P4c8BSjPLLH7Dlg9jEAlAOeYpQxAIyXA97sf71YwUdZLL7/LD9Psljc+zxHozF7/4tywOxjACgHRDngCl1yICIiIiKGpoJWRERERAxNBa2IiIiIGJoKWhERERExNBW0IiIiImJoKmhFRERExNBU0IqIiIiIoamgFRERERFDU0ErIiIiIoamglZEREREDE0FrYiIiIgYmgpaERERETE0FbQiIiIiYmgqaEVERETE0FTQioiIiIihqaAVEREREUNTQSsiIiIihhbk7QDk3Ox2sJV4OwrnBQSDxeLtKPyH0fof3J8DdjsUl7pve7/f7un/i6zu3XZIoPv2gXJAzJ4DZh8DwHg54M0xwGK3n+5a8SWlxbB8mrejcF7qGAgM8XYU/sNo/Q/uz4EiKzya7r7tVYZJgyHUTV8TKAfE7Dlg9jEAjJcD3hwDdMmBiIiIiBiaCloRERERMTQVtCIiIiJiaCpoRURERMTQVNCKiIiIiKGpoBURERERQ1NBKyIiIiKGphcr+JHNGSt4ZEZquWlhIZEkxjakZ5uh3Nj5AQID1eX+TDkgygFzU/+LWXPA/1okpLa6jQ6Nr8eOnbwTOSz5fg4zPhvL/sM7efimmd4OTyqBckCUA+am/hez5YAKWj/UIKENPdveXvZz304juXtyYxZ/9zbDe71A9SqxXoxOKoNyQJQD5qb+F7PlgK6hNYHwkEga17kKu93OoV8zvB2OeIFyQJQD5qb+F3/PARW0JpH9W/JWjYj2ciTiLcoBUQ6Ym/pf/DkHdMmBHyosOcmxglzsdsd1M5+tmcHegxtpXLsDibENvR2eVALlgCgHzE39L2bLAb8vaHNzc5k8eTILFiwgKyuL2NhYBg4cyMSJExkzZgyzZs3itddeY/To0d4O1W3mfPkMc758pty0Ls0H8sCA170UkffZ7HDgVzhRCBYLxFSBy6t5OyrPUQ6csWjqTWSs/5hBT35NQqMuZ80/uGsV85/vRv12A+jz4EdeiNAzlANnO1EIB/Og2ArhwXBFDIT66VlQ/V+eGccBs+WAnx7KDps2baJ3797k5OQQGRlJ06ZNOXToENOmTSMjI4MjR44A0KpVK+8G6mZ9rhxB15Y3Y7WV8GP2VtJXTCL3WBYhwWFlyxRbixj5ahtSW/+ZIT2eKJs+ed4wjub/zMS/LPZG6G5XWAJr9sLqPZB7ovy8erHQuSG0rgMBFu/E5ynKgTNSh7/JwV0rWZI2jCETNxMcFlk2r6ToJEvShhEWFcPVd83wYpTupxw4IzMXVu6CTfuh1HZmelgwdKgHXRtBTJT34vME9X95ZhwHzJYDfnsNbW5uLn379iUnJ4dx48aRnZ3Nhg0byMnJYdKkSSxatIh169ZhsVho2bKlt8N1q4SYBrRp2JMOjXszOHUCfx/+Gbuy1jF1/n1ly4QEhTLh1jnM+2oiGYc2A7B62yes3fkZY29+x1uhu1VeAbz6BfzfhrOLWYB9v8C/VsN7K6GktPLj8yTlwBkRVWPpcVcaxw5nsGrehHLzVs97lGOHM+hx90zCo2K8FKFnKAccvtkFU7+A7zPLF7Pg+MD7zS54+b/wQ7ZXwvMY9X95ZhwHzJYDflvQjhkzhqysLEaPHs0rr7xCVNSZj98TJkwgJSUFq9VKUlISVatW9WKkntcsqRM92wxlxeZ0tmd+Wza9YWJbbur2CJPn3cEvR7N49aMRPDDgdWKq1fJitO5xsghmLIOcYxdfdvMB+GCN47IEf2XGHPi9+u1upHGXoWz56k32b/sKgKwdK9i89HWadLmD+m37ezlCzzNjDvwvAxash4sd2kVWePtrxze5/sqM/f9HZh8H/D0H/LKg3blzJ+np6cTExPDiiy+ec5m2bdsCkJKSct7t9O7dG4vFwrPPPuuJMCvVkJ5PERAQyHtfPP2H6U8SGBDE/a+2JiU5ldRWt3opQvda8QP8fNz55Tf+BHtyPBePLzBbDvxR96HTqFIjgaVv3UV+3iGWvHUXVWok0O2Oad4OrdKYKQeKShzFrLOspTB/nefi8QVm6v/zMfs44M854JcF7dy5c7HZbAwZMoQqVaqcc5nw8HDg/AXthx9+yKZNmzwVYqVLiEkmNeVWNu79iq37VpZNDwoMpmlSJ44V5HJdu+FejNB9rKWO62ZdtWq3+2PxJWbKgXMJjaxOz3ve4cSv+/ng8RSO52bS855ZhEb48d2Bf2CmHFif6fjm1RUHjsD+Xz0Sjk8wU/+fj9nHAX/OAb8saJctWwZAamrqeZfJysoCzl3QHj9+nIceeohXXnnFMwF6yW09niDAEsB7X575ZLZ130q+XD+b/p1H88anD1JUcsqLEbrHrmzH3cyu2nYQCorcH48vMUsOnE+dFtfSPHUEp07k0rz7PdRpcY23Q6p0ZsmB7/ZV7npGYZb+vxCzjwP+mgMWu93ud1cO1q5dm6ysLDZu3HjOJxhYrVbi4+PJzc0lIyODevXqlZv/wAMPsHXrVlasWIHFYuGZZ565pMsO2rVrR06Oa3/PDgkKZ+boPRX+nc44VZTPvf9MYVDXsfTteD/jZnSjYWI77u83xeVtjZjegGKrbxwA9a4aSpsB577U5GK+/GcPjv+8y80Rua4y+h98OwcCg8MZ8Lz798GOb2azZOZwrhnxLk27DnPrtj9+sgGlbjoRKAcuzfWPryOiWrzL6x3a/gXfzrnbAxG5zmjnAXBvDnhqDADPjQPuHAPAeDlwqf0fFxfH+vUuXCv0O3752K6CggIATp06905NT08nNzeXqKgo6tatW27e+vXreeutt/j+++/dFk9OTg4HDx50aZ2w4Ai3/f7zSftsHHHRdenXaSQWi4Xxt8zmvldb0bn5AFrW6+rStrIPHaKw5KSHInVN9NGjFV73559zOOJiX3lCZfQ/+HYOBIVWzj5wp0PZh7AWuWcfKAcuTanVxesNfnPq1CmXx2tPMdp5ANybA2YfA8B4OeDNMcAvC9q4uDjy8vLYsGEDHTt2LDcvOzub8ePHA9CyZUssljMPIC0tLeXee+9l9OjRNGvWzK3xuCokKNxtv/9cvvthMSs2pzNz7JayfVArpj53936JV9KHkzZuC+EhkRfZyhnxtWr5zDczIRRUaD1bqZWq4RbCExLcHJHrPN3/4Ps5EBjs+X3gbrXia7n1G1pP8/UcuBTF+T9Dzdour2cvyiPBB8YAMN55ANybA2YfA8B4OXCp/V+Reuk0v7zkYMyYMbz22mvUrl2bpUuX0rCh4xVv69atY+jQoezbt4+SkhJGjRrF9OnTy9abOnUqL7/8Mj/88EPZzWTuuOSgIkqLYbmBbrpMHQOBId6OwqHUBs99DMddvI62ZW24y/UvJDzCaP0P7s+BIis8mu6+7Z3myUsOJg1235unlAOXZs1eSP+f6+uN6wW1a7o/noowew54agwAz40D7hwDwHg54M0xwC9vCpswYQI1a9bkwIEDNGvWjBYtWtCgQQM6dOhAvXr1uPrqq4HyN4Tl5uby1FNP8fTTT2O1Wjl69ChHf/vTdWFhIUePHsVms53r14mPCQyAjg1cX6+L/73aWsS02iQ53gTmiitq+k4xKyKu8cuCNjExkZUrV9KnTx/CwsLIzMwkOjqatLQ0Fi1axO7djucz/b6gzcrK4sSJE9x7773UqFGj7B/ApEmTqFGjBvv37/dKe8R13RtDvAtPYWmbBA0u91g4IlLJQoNgUDvnlw8OhJvaey4eEfEsv7yGFqBJkyYsXLjwrOn5+flkZmYSEBBA8+bNy6YnJyezfPnys5ZPTU3lzjvvZNiwYZd0bYdUrvAQuO9qSFsOh45eeNnWdeC2q+B3l1OLH2vadZjbLzUQ39S+HhRb4aN1F35bWFiw43KjK/TtrGloHPA/flvQns/27dux2+00bNiQiIgzdw9WqVKF7t27n3OdpKSk884T31UtAh68Fv63z/HShMN/eHNYg8uhc0PHtbMBKmZF/FLnho5C9ZvdsDETrL+7ciwiBK6s77jcqOa538EjIgZhuoJ269atwIVfeSv+IzQYujaCPzWErDx48ys4WQxVQmFUT29HJyKVoXZNGNIRbmwDEz+FgmKIDIFnBkCI6c6CIv7JdIeyqwWtrz4EIuuXPbycfifHCnKJDKvG+MGzSYor/6ixnCOZvJw+jL2HNhJXoy5pYzeVm7/4u3eYt/wl7DYbrZKvZszANwgKDMZmszFz4SOs2/U5gQFBVI2sycM3vUVCTHIlttC9LBaoHe24Tg4cN44ZnTM5cJrdbmdCWg/2HNzAJ38/CkD2kR/5+5ybKLWVYrNZqX15Ex4eNJOoCMe14/OWT2LJ+vcICgwhJDiMUf2n0fiKDpXVPHGRM/lwoWP7x+ytvPbxKI7mHyYwIIhGV3TggQGvE2rARyedT2QoBP02BgQF+kcxe6n9DrB2x0JmLnyEUnspdeNaMH7wbCLDqpoiJ4zuUs8DcP7+v9g5wtf4wWndNf7yDe3U+fdy/ZUjmP3obganPsrL6cPOWiYirCrDez3P43/+4Kx52Ud+ZPYXTzHl/pW899he8vJ/ZtHamQCs2fEp2zNXkzZ2MzPHbaF1cg9mLX7c000SFzmTA6fN/2YK8TXrl5tWs2otpoxaRdrYTbz1yDZqVq3FnCXPArD34CY++/YNpo/5jrSxm+jfaTTTPxntwdbIpXImHy50bAcHhTH6xunMmvADM8ZuprC4gPTlkyq5FeKqS+33U0X5/OM/d/PssE9479E91Kxai38v/TugnDCCSz0PXKj/L3SO8EWmK2iXLVuG3W6nT58+3g6lwvLyD7M7az0929wOwJ9aDOKXowc4mLu33HJVI6JpXrcLYed4KPLKLR/RsWk/oqvGYbFYuOGq+1i+aS4AFiwUW4soLinEbrdzsvA4sdUSPd8wcZqzOQCQmbOdb7d/wq2pj5WbHhIUWvZNS6mtlMLiAiw4Lia2WCxYbSUUFjteUpFfeJQY5YDPcjYfLnRsJ8Y2oF6tlgAEBgTSKLE9OUcyK7Ud4hp39Pt3PywmuVZrrrisMQD9Oo0sOxcoJ3ybO84DF+r/C50jfJEf/MHFfH45eoDoqvEEBjq6z2KxcFmNKzh8dL/TlwUcPrqfy2vUKfs5LjqJw0cdjyW7qmlfNmUsZ/Df4ggPjSKmWgL/uP9r9zdEKszZHLCWljDlo3sYe/M7BAQEnrWdEmsxo6d14PDRn6gb35K/D/sUgPq1Uhj0p4cZ+mJdoiKiCQ4M5Z8jv6mcxonLnM0HZ4/tU8UFLP7ube7q/WKltUFc545+/+O54PIaSRw5nk1pqbVsu6Cc8EXuOA9crP/Pd47wRab7hlYubnfWejJztjH3qYPMe+oQrZN7MHX+fd4OSyrgX0ueo0vzgdS5vMk55wcHhZA2dhMfPv0zV8Q2ZuHaNMBxScqqrQuY/ehe5j6ZxcCuD/P8+4MrM3TxAGeO7RJrMS+8P5i2Da+lS4sBXopU3OlSx3TlhLFd7DxwIec7R/giFbQGFFu9dtknKHBc6H04bz+XVb/C6W1cVv0Kfs77qeznnCOZZesv+X4OrZKvpkp4dQICArim3Z1syjj7Gb3iPc7mwJZ9X/PJ6te4fWISD7/RhZNFx7l9YhJH838pt1xwUAjXth/O0g3/AmDVlvnUjW9BTLVaAFzXfjjbM1dTYi2uhNaJq5zNh4sd29bSEl54fzDRUfGM7D+1UtsgrnNHv//xXPBzXma5b/2UE77LHeeBi/X/aX88R/giFbQGVKPKZSQntGHphvcBWLl1PjHVE116CsGfWgxizY5POXI8B7vdzsK1M+je6lYA4qPrsWnvsrLiZe3OhSTFNb/Q5qSSOZsDU0au5N9P/MT7j2cyZeQqIkKr8v7jmVSvEsvPeT9RWHwScNwF/c2W/1Av3nG9XFzNemzPXM2ponwA/rdjIYmxDQkO8tJLuuWCnM2HCx3bpaVWXnj/VqIionn4pplY9KYRn+eOfm/fqBd7D25g/+EfAPj02zfonuI4FygnfJs7zgMX6v8LnSN8ka6hNaiHBqXxcvow5i6bSERYVcbf8i4A//jPX+jYtB+dmvWjsPgkwyc3pMRaREHhMW57PpGebYZy9/UvEl+zHnde+xwPvd4ZgJT63bnhqnsB6Nd5FPsP7+TeKSkEBQRTIyqOhwbN8Fpb5dycyYEL2Ze9hXcXPwGA3W4jOaENo/pPA6BL8wHsPrCOUVPbERwUSlhIJH89x9MyxHc4kw8XOrZXbE5n1bYF1ItvyX1TWgPQLKkzYwa+7rU2ycVdar9HhEXx8M1v8+zsGym1WUmKa86Ewe8BygkjuNTzwIX6/0LnCF9ksfvqg1ZNrrQYlvtu3pwldQwEGuDLu2cWwLFTUC0cnhvo7WjOz2j9D+7PgSIrPJruvu1VhkmDIdRNXxMoBzzDKGMAKAfMPgaA8XLAm2OALjkQEREREUNTQSsiIiIihqaCVkREREQMTQWtiIiIiBiaCloRERERMTQ95cBH2e1gK/F2FM4LCAYjPKLQKHc4G63/wf05YLdDcan7tlcZQgLdtw+UA55hlDEAlANmHwPAeDngzTFAz6H1URaL7z/+RjxH/e/YB+58/I3RKAfE7Dlg9jEAlAOu0CUHIiIiImJoKmhFRERExNBU0IqIiIiIoamgFRERERFDU0ErIiIiIoamglZEREREDE0FrYiIiIgYmgpaERERETE0FbQiIiIiYmgqaEVERETE0FTQioiIiIihqaAVEREREUNTQSsiIiIihqaCVkREREQMTQWtiIiIiBiaCloRERERMTQVtCIiIiJiaEHeDkDOzW4HW4m3o3BeQDBYLO7bnt0OxaXu297vt3v6/yKre7cdEui+fWC0/gfP5IDZ94GZmX0MAB0DZm+/uMZit58+vMWXlBbD8mnejsJ5qWMgMMR92yuywqPp7tteZZg0GELd9BHRaP0P7s8B7QNzM/sYADoGzN5+cY0uORARERERQ1NBKyIiIiKGpoJWRERERAxNBa2IiIiIGJoKWhERERExNBW0IiIiImJoKmhFRERExND0YgU/sjljBY/MSC03LSwkksTYhvRsM5QbOz9AYKC63J+ZPQfM3n4RHQPaB2alHvVDqa1uo0Pj67FjJ+9EDku+n8OMz8ay//BOHr5pprfDk0pg9hwwe/tFdAxoH5iNClo/1CChDT3b3l72c99OI7l7cmMWf/c2w3u9QPUqsV6MTiqD2XPA7O0X0TGgfWA2uobWBMJDImlc5yrsdjuHfs3wdjjiBWbPAbO3X0THgPaBv1NBaxLZvx28VSOivRyJeIvZc8Ds7RfRMaB94M90yYEfKiw5ybGCXOx2x3VDn62Zwd6DG2lcuwOJsQ29HZ5UArPngNnbL6JjQPvAbExR0Obm5jJ58mQWLFhAVlYWsbGxDBw4kIkTJzJmzBhmzZrFa6+9xujRo70dqlvM+fIZ5nz5TLlpXZoP5IEBr3spIu9ZNPUmMtZ/zKAnvyahUZez5h/ctYr5z3ejfrsB9HnwIy9E6BlmzwGzt1/KM+M4oGNA+8Bs/L6g3bRpE7179yYnJ4fIyEiaNm3KoUOHmDZtGhkZGRw5cgSAVq1aeTdQN+pz5Qi6trwZq62EH7O3kr5iErnHsggJDitbpthaxMhX25Da+s8M6fFE2fTJ84ZxNP9nJv5lsTdCd7vU4W9ycNdKlqQNY8jEzQSHRZbNKyk6yZK0YYRFxXD1XTO8GKX7mT0HzN5+Kc+M44COAe0Ds/Hra2hzc3Pp27cvOTk5jBs3juzsbDZs2EBOTg6TJk1i0aJFrFu3DovFQsuWLb0drtskxDSgTcOedGjcm8GpE/j78M/YlbWOqfPvK1smJCiUCbfOYd5XE8k4tBmA1ds+Ye3Ozxh78zveCt3tIqrG0uOuNI4dzmDVvAnl5q2e9yjHDmfQ4+6ZhEfFeClCzzB7Dpi9/VKeGccBHQPaB2bj1wXtmDFjyMrKYvTo0bzyyitERUWVzZswYQIpKSlYrVaSkpKoWrWqFyP1rGZJnejZZigrNqezPfPbsukNE9tyU7dHmDzvDn45msWrH43ggQGvE1Otlhejdb/67W6kcZehbPnqTfZv+wqArB0r2Lz0dZp0uYP6bft7OULPM3sOmL39onFAx4D2gb/z24J2586dpKenExMTw4svvnjOZdq2bQtASkpK2bQVK1ZgsVjO+mf0SxKG9HyKgIBA3vvi6T9Mf5LAgCDuf7U1KcmppLa61UsRelb3odOoUiOBpW/dRX7eIZa8dRdVaiTQ7Y5p3g6t0pg9B8zeftE4oGNA+8Cf+W1BO3fuXGw2G0OGDKFKlSrnXCY8PBwoX9Ce9vrrr7NmzZqyf//61788Gq+nJcQkk5pyKxv3fsXWfSvLpgcFBtM0qRPHCnK5rt1wL0boWaGR1el5zzuc+HU/HzyewvHcTHreM4vQiGreDq3SmD0HzN5+0TigY0D7wJ/5bUG7bNkyAFJTU8+7TFZWFnDugrZp06ZcddVVZf9atGjhmUAr0W09niDAEsB7X575ZLp130q+XD+b/p1H88anD1JUcsqLEXpWnRbX0jx1BKdO5NK8+z3UaXGNt0OqdGbPAbO3XzQO6BjQPvBXFrvdbvd2EJ5Qu3ZtsrKy2Lhx4zkvF7BarcTHx5Obm0tGRgb16tUDHJccpKamsnz5crp37+6WWNq1a0dOTo5L64QEhTNz9B63/P7zOVWUz73/TGFQ17H07Xg/42Z0o2FiO+7vN8XlbY2Y3oBiq/sGgMDgcAY87/727/hmNktmDueaEe/StOswt2774ycbUOqmQbAy+h98OweMdgyA+/eBmXlqDADPjQPuHANAx4DGQfOJi4tj/fr1FVrXbx/bVVBQAMCpU+dOrPT0dHJzc4mKiqJu3bpnzR88eDC5ubnUrFmTfv368dJLLxETU7E7YHNycjh48KBL64QFR1Tod7ki7bNxxEXXpV+nkVgsFsbfMpv7Xm1F5+YDaFmvq0vbyj50iMKSk26LLSjU8+13t0PZh7AWuWcfVEb/g2/ngNGOAXD/PjAzs48BoGNA46C4wm8L2ri4OPLy8tiwYQMdO3YsNy87O5vx48cD0LJlSywWS9m8atWqMX78eLp27UqVKlVYs2YNL774ImvXrmX9+vWEhYXhqri4OJfXCQkKd3kdV3z3w2JWbE5n5tgtZe2vFVOfu3u/xCvpw0kbt4XwkMiLbOWM+Fq13P4NrdHUiq/l1m9oPc3Xc8BoxwC4fx+YmdnHANAxoHHQfCpSL53mt5ccjBkzhtdee43atWuzdOlSGjZ0vOZu3bp1DB06lH379lFSUsKoUaOYPn36Bbf12Wef0a9fP2bNmsXw4ZVzsXhpMSw30I23qWMgMMR92yuywqPp7tveaZ685GDSYAh100dEo/U/uD8HtA/MzVNjAHhuHHDnGAA6BszefnGN394UNmHCBGrWrMmBAwdo1qwZLVq0oEGDBnTo0IF69epx9dVXA+e+IeyPbrjhBiIjIyt8XYeIiIiIeI7fFrSJiYmsXLmSPn36EBYWRmZmJtHR0aSlpbFo0SJ2794NOFfQnvb7SxNERERExDf47TW0AE2aNGHhwoVnTc/PzyczM5OAgACaN29+0e18+umnFBQU0KFDB0+EKZWoaddhbr/UQESMReOAiP/x64L2fLZv347dbqdhw4ZERJS/i/L222+nXr16tGnTpuymsMmTJ9OqVStuvVVvDhERERHxNaYsaLdu3Qqc+3KDZs2a8cEHH/Dqq69y6tQpEhMTueeee3jmmWcICdGV3iIiIiK+RgXtH/z1r3/lr3/9a2WH5LKsX/bwcvqdHCvIJTKsGuMHzyYprlm5ZTZnrODxt3uTGNuobNq0B9YQGhyOzWbjrUUTWL/rc0ptVpoldWbMwDcJDgoh50gmd75Un6S4M29He+aO+dSKqV9p7ZOLcyYHPl/3Lh+vnFr2c+6xLFrU68qzdy4g+8iP/H3OTZTaSrHZrNS+vAkPD5pJVEQNAA7n7ee1j0eRlbubAEsgfTvez41dHqjUNl6MM/vgNLvdzoS0Huw5uIFP/n4UcDxQ/bk5g9iT9T2lNmvZdOCi+0fEF5h9HHCm/Rv3LuOd/z7GqaJ8LBYLVzbuw93Xv0RAQMAFx4B1u77g7UWPlv18tOAw0VFxvPnQhspqnrhABa1BTZ1/L9dfOYLr2g/jmy0f8XL6MF5/cN1ZyyXGNiJt7Kazpn++7h32HtzAGw9tICgwmCkfjeDjVVO5pbvj+bzhoVHnXE98hzM50Kv9cHq1P/OouXteaU6P1kMAqFm1FlNGrSL0t+d9vv5/DzJnybOM6j8Vu93Os+8NYHDqY3RLuRmAvBM/V1LLnOfscQAw/5spxNesz56DZ05GgYHBDE59lKjwaB6Z0b3c8hfaPyK+wuzjgDPtjwqvwRND5hFfsx7FJYVMmNmTJd/P4br2wy44BrRvdB3tG11X9vOTs24gpX5qZTRLKsBvn3JwIcuWLcNut9OnTx9vh1IhefmH2Z21np5tbgfgTy0G8cvRAxzM3ev0NjIObaZ1g54EB4VgsVho37g3S7//l6dCFjerSA7s3P8/juYfpmOzfgCEBIWWncRKbaUUFhdgwfEkj417viI4KLTsJAZQI+pyTzWnQlzZB5k52/l2+yfcmvpYuekhQaG0Tr6aKuHVz1rnQvtHxBeYfRxwtv3JCa2Jr+l4vX1IcBj1a7Xi57xMx88XGAN+L/fYITbu+YqebYe6vR3iHqYsaI3ul6MHiK4aT2Cg4wt2i8XCZTWu4PDR/Wctm30kg/tfbcOoqe359Ns3yqY3SGzLmh2fUlB4HGtpCd9s/rDsAAcoLC5g1NT23P9qG/615G+U2ko93i5xnis5cNrn371Dj7ZDCQoMLptWYi3m3n+24qZnYziYu4c7r30OgJ8O76BaZCwvvH8r901pzbOzB5D96z7PNspFzu4Da2kJUz66hwcHpREQEOjS7zjf/hHxBWYfByrS/iPHc1i55SOubHKDS7/ry/Wz6dD4empUueySYhbPUUHrx5IT2jD3iSzefGgDz975MQvXzODrzR8CcF27YbRv1Itxb3Zj3JvdSIhtSGCAY1CIrhrP3KcO8vqD65g0YinbflzJR1//w5tNkUt0qriAFZvm0bvD3eWmBweFkDZ2Ex8+/TNXxDZm4do0AEpLrWzKWMaQnk8x4+GNtG10HX9//xZvhH7J/rXkObo0H0idy5u4vO759o+IEZl5HAAoKDzOU+/25ZbuE2hUu53T69ntdr5YN4tef9hv4ltU0BpQbPXaHDmeTWmpFXAcbIfz9nNZ9SvKLRcZVpXI8Gq/rZNIauvb2PrjSsDxSfaOa59lxsMbmTr6W+pc3pQ6v11IHxIUWvYptGpENNe1v4ttv60nvsHZHDjtm83/oc7lzahzedNzzg8OCuHa9sNZusFx2cllNa4guVbrspsrerYdyt6DG7CWlnigNRXj7D7Ysu9rPln9GrdPTOLhN7pwsug4t09M4mj+L07/rj/uHxFfYPZxwJX2nyw8weNv96JTs/7c1G2sS79ny76vKbYW0u5319OK71FBa0A1qlxGckIblm54H4CVW+cTUz2RhJjkcsv9ejwbm80GOA7mtTsWklyrNQDFJYWcOJkHwLGCXOYte4lbuk8AHNclnR6wiq1FrNq2gPoJrSulbeIcZ3PgtM/XvXPWtws/5/1EYfFJAGw2G99s+Q/14lsC0L5xb345lkXusYMAfLfzv1xxWZNyf6b0Nmf3wZSRK/n3Ez/x/uOZTBm5iojQqrz/eCbVq8RecPsX2j8ivsDs44Cz7T9VlM9f3+5Fu0a9GNLzSZd/z+Lv3uHadsMIdPGSJalcpnzKgT94aFAaL6cPY+6yiUSEVWX8Le8C8I///IWOTfvRqVk/Vm6dz8I1bxIYEESpzUrXljdz3W93uhYUHmPcjO4EWAKw2W0M6PIgHZv2BWDbj6uY88XTBAQEUmqz0ir5av7c4wmvtVXOzZkcADhweBcZhzbxwl3/Lbf+vuwtvLvY0a92u43khDaM6j8NgPCQSB4cOIMn3ukD2IkMq8YTQ+ZVXuOc5Ow+uJAR/2jJsYJfOFl0nNueTySlfiqP3favC+4fEV9h9nHAmfYvWDWVXQe+o7C4gFVbFwDQNeVmhvx2XjvfGABQcOoYq7cuYOa4rd5poDjNYrfb7d4OQs5WWgzLDXTuTB0DgW5870SRFR5Nd9/2KsOkwRDqpo+IRut/cH8OaB+Ym9nHANAxYPb2i2t0yYGIiIiIGJoKWhERERExNBW0IiIiImJoKmhFRERExNBU0IqIiIiIoekpBz7Kbgebbzy72ikBwWBx42vu7XYoNtjbdkMC3bcPjNb/4JkcMPs+MDOzjwGgY8Ds7RfXqKAVEREREUPTJQciIiIiYmgqaEVERETE0FTQioiIiIihqaAVEREREUNTQSsiIiIihqaCVkREREQMTQWtiIiIiBiaCloRERERMTQVtCIiIiJiaCpoRURERMTQVNCKiIiIiKGpoBURERERQ1NBKyIiIiKGpoJWRERERAxNBa2IiIiIGJoKWhERERExNBW0IiIiImJoKmhFRERExNBU0IqIiIiIoamgFRERERFDU0ErIiIiIoamglZEREREDE0FrYiIiIgYmgpaERERETE0FbQiIiIiYmj/D82GiO7q+nxiAAAAAElFTkSuQmCC", "text/plain": [ - "
" + "
" ] }, "execution_count": 3, @@ -174,14 +174,12 @@ }, { "data": { - "image/png": "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\n", + "image/png": "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tlmSrLlz59p148ePt478X17V/LZu3Wqv++yzzyxJ1nPPPWevGzhwoNWoUSPrv//9r71uy5YtVkhISLVt1iQ9Pd2KiIiwdu3aZe3atcvaunWr9de//tUKCgqyLrzwQquystKyLMv66aefrMjISOv222/3e7/X67VcLpe9fs+ePZYk68knnzzmfo/8e1ildevWVnp6uv36vffesyRZ7733nr0uNTXVat26dbX3XnPNNdYFF1xw3DkDpxtfIQG1MH36dOXm5io3N1evvPKK+vbtq+HDh+vNN9885vsOP+tx8OBB/fDDD2rXrp0iIyP16aef2mORkZHauHGjtmzZUuN2fvzxR61cuVJ/+MMf9NNPP2n37t3avXu3fvjhB3k8Hm3ZskX//e9/JUn/+te/1L17d3Xt2tV+f/PmzTV48OATmuvQoUO1dOlS9enTRx988IEeeeQR9erVS+3bt9fatWuPOr89e/aotLRUvXr18ptblSuvvNLvDEG3bt0k/XJGoUmTJtXWH/n1SkhIiP70pz/Zr0NDQ/WnP/1JxcXFys/PP6G5HS4lJUVt27a1X1900UVyOp32fisqKvTuu+/q2muvVXx8vF3Xrl079e/f/4T3s2/fPjVv3lzNmzdXu3btdN9996lHjx56++237a+hcnNzVVJSoptuusk+trt371ZwcLC6deum9957T9IvP+/Q0FCtWrVKe/bsqfWcT0ZkZKS+++47ffzxx7/qfoEjEWCAWujatatSUlKUkpKiwYMHKycnR0lJSRo5cqTKy8uP+r79+/dr3LhxSkhIkMPhULNmzdS8eXOVlJSotLTUrps4caJKSkp03nnnqVOnTrr//vv1+eef2+Nbt26VZVl66KGH7A/DqmX8+PGS/neB7bfffqv27dtX6+X8888/4fl6PB4tW7ZMJSUlWrNmjTIyMvTtt99qwIABfhfyLl68WN27d1dYWJiioqLUvHlzzZw5029uVVq1auX32uVySZISEhJqXH/kB3R8fLwiIiL81p133nmSVKdn2xzZj/TLVzpV+y0uLtb+/fvVrl27anU1rTuasLAwO/zOnTtXHTt2VHFxsV/4qwquV1xxRbXju3z5cvtn7nA49MQTT2jJkiWKjY1V7969NXnyZHm93lrNvS7GjBmjxo0bq2vXrmrfvr0yMjL04Ycfnvb9AkfiGhjgJDRo0EB9+/bV1KlTtWXLFl1wwQU11t19992aO3euMjMz5Xa75XK5FBQUpEGDBvldVNu7d299/fXXevvtt7V8+XLNmTNHU6ZM0axZszR8+HC79r777pPH46lxX7X5UD1RjRo1Uq9evdSrVy81a9ZMDz/8sJYsWaL09HS9//77+t3vfqfevXtrxowZatGihRo2bKi5c+fWeGFncHBwjfs42nrLsk7pXAK13+DgYKWkpNivPR6POnTooD/96U/2BdhVx/fll19WXFxctW0cfrF4ZmamBg4cqIULF2rZsmV66KGHNGnSJK1cuVKXXnrpMXupqKio8zw6duyozZs3a/HixVq6dKn++c9/asaMGRo3bpwefvjhOm8XqC0CDHCSDh06JEnau3fvUWveeOMNpaen66mnnrLXHThwQCUlJdVqo6KiNGTIEA0ZMkR79+5V7969NWHCBA0fPlznnnuuJKlhw4Z+H4Y1ad26dY1fRW3evPlEpnVUVbeRf//995Kkf/7znwoLC9OyZcvkcDjsurlz557Ufo5m586d2rdvn99ZmP/85z+SdNIXr9YkJiZGYWFh2rp1a7WxmtadqBYtWmjUqFF6+OGHtW7dOnXv3t3+KismJua4x1eS2rZtq3vvvVf33nuvtmzZoksuuURPPfWUXnnlFUm/nEk68u9YeXm5feyO5Vh3V0VEROjGG2/UjTfeqPLycl133XX6y1/+orFjx9aLRwjADHyFBJyEgwcPavny5QoNDVXHjh2PWhccHFztX/TPPfdctX8J//DDD36vGzdurHbt2qmsrEzSLx9sffr00ezZs2v8ENq1a5f956uvvlrr1q3TRx995Dd+og9NW7FiRY3r//Wvf0n631dRwcHBCgoK8pvL9u3btXDhwhPaT20dOnRIs2fPtl+Xl5dr9uzZat68uZKTk0/5/qrOnCxcuFA7d+6012/duvWkn8J89913q1GjRnr88ccl/XJWxul06rHHHtPBgwer1Vcd359//rnaE3zbtm2rJk2a2H9XqtatWbPGr+75558/oTMwERERNX4FeOTf0dDQUCUlJcmyrBp7Bk4XzsAAtbBkyRJt2rRJ0i/XRsybN09btmzRAw88IKfTedT3DRgwQC+//LJcLpeSkpKUl5end999175Nt0pSUpL69Omj5ORkRUVF6ZNPPtEbb7yhkSNH2jXTp09Xz5491alTJ91+++06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", "text/plain": [ - "
" + "
" ] }, - "metadata": { - "needs_background": "light" - }, + "metadata": {}, "output_type": "display_data" } ], @@ -227,9 +225,9 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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", "text/plain": [ - "
" + "
" ] }, "metadata": {}, @@ -246,7 +244,7 @@ "\n", "G = nx.random_regular_graph(reg, n, seed=seed)\n", "weights = {(i, j): 1 for i, j in G.edges}\n", - "nx.draw(G)" + "nx.draw_networkx(G)" ] }, { @@ -257,9 +255,9 @@ "outputs": [ { "data": { - "image/png": "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k8rOPacjJ0sW3QAAAFAxFNYAAADcxFODYzVzyWNq1qi1OrbpbXQcAAAAl0dhDQAAwE10bNNLcU8nKCNzn75Knmt0HAAAAJdHYQ0AAMBF5Rdc1csfDNLB46l66YMBGt3/Fe1KX69R0TH69scF+jr5HWVfOqvsS+f03IPztCR+prbtWyvfuk31/EPvGh0fAADA5ZlsNpvN6BDuas4a6cApY7bdtpn0x3uM2TYAACifzZ9JWRnO326jFlL3x5y/XQAAAKNZrVZJUmJiYrn6e1RfFAAAAAAAAKD2orAGAAAAAAAAOIB7rBkooLF7bhsAAJRPg2butV0AAABXQ2HNQA92NzoBAACoydrfbXQCAAAAlIapoAAAAAAAAIADKKwBAAAAAAAADqCwBgAAAAAAADiAwhoAAAAAAADgAAprAAAAAAAAgAMorAEAAAAAAAAOoLAGAAAAAAAAOIDCGgAAAAAAAOAACmsAAAAAAACAAyisAQAAAAAAAA6gsAYAAAAAAAA4gMIaAAAAAAAA4AAKawAAAAAAAIADKKwBAAAAAAAADqCwBgAAAAAAADiAwpoBFi9erNDQUIWHh6tv377au3ev0ZEAAAAAAABQQRTWnOzSpUuaOHGi1q1bp+3bt2vUqFGaOnWq0bEAAAAAAABQQRTWnKywsFA2m005OTmSpPPnz+vWW281OBUAAAAAAAAqytPoAO6mfv36mjt3rjp16qSGDRuqYcOGSklJMToWAAAAAAAAKogRa0529epVvfPOO9q0aZOOHj2qhx9+WC+++KLRsQAAAAAAAFBBFNacbPv27bLZbLrjjjskSY899piSk5MNTgUAAAAAAICKorDmZC1atNDevXt19OhRSdKaNWvUsWNHg1MBAAAAAACgorjHmpPdeuut+stf/qJ77rlHFotFt9xyiz788ENJks1mk8lkMjghAAAAAAAAysNks9lsRoeA3T++/bfMZg/df28fo6MAAAAAAAC4HavVKklKTEwsV3+mgtYQp8+d15ade2U2858EAAAAAADAFdSYKs706dNlMpm0a9cuDRkyRPXr19ett96qN998U5L07bffqmvXrqpbt666dOmi9evXF3t9cnKyBgwYoIYNG6pOnTrq27dviT6bN2/WI488olatWqlOnToKCgrSH//4R50/f75Yv/379+vhhx+Wv7+/vL29FRAQoN/85jc6c+ZMtf3+Ccnb5GH2UNSd4dW2DQAAAAAAAFSdGnePteHDh+sPf/iDJk2apIULF+qFF17QmTNn9M0332jq1Klq0KCBYmJiNGzYMKWnp6tBgwb617/+pfvuu0933323PvroI3l7e2vevHmKjo7W+vXr1aNHD0lSenq6OnfurNGjR6thw4bav3+/3njjDW3dulUbNmwoyjBkyBD5+vpqzpw5at68uU6cOKE1a9YoNze3XL/DS//few7//rPmLXb4tQAAAAAAAHDcwcPHK9S/xtxjbfr06frzn/+s+fPn6+mnn5Yk5eXlqXnz5rp06ZLS0tLUpk0bSdK6desUHR2t5cuX66GHHlK7du3k5+en9evXy8PDPggvPz9fnTp1UmBgoFatWnXDbebn5yslJUV33XWXtm3bpvDwcJ0+fVq33HKLvvzySw0bNsyh36UyhTUAAAAAAAAY49N335IkHT64t1z9a9yItcGDBxd97+3trcDAQBUUFBQV1SSpQ4cOkqQjR45o//792rdvn55//nkVFhaqsLCwqF///v310UcfFf2ck5Ojv/zlL/r888915MgR5eXlFS3bu3evwsPD1bRpUwUGBuqll17SyZMndddddxVtr7z+8uLYcvc9fe683n7/7+rVLURDoyMrtB0AAAAAAABUnR++/bRC/WtcYa1JkybFfvby8pKPj0+JNkm6fPmyTp48KUmaMGGCJkyYcMN15ubmqk6dOvr973+vb7/9VtOnT1fXrl3VoEEDHTlyRA8++GDRNE+TyaS1a9fqtdde09SpU5WZmakWLVpowoQJevHFF2Uymcr8HRwZsbZh8y5t2Lyrwq8DAAAAAABA1cjJN1eof40rrFVU06ZNJdmnkg4ZMuSGfby9vXX58mV98cUXeuWVV/SnP/2paNl/P7hAkm6//XZ99NFHstls2r17tz788EP97//+r/z8/PSHP/yhen4RAAAAAAAAGKr/0Ecr1N/lC2vt27dXYGCgdu7cqVdfffWm/fLy8pSfny+LxVKs/cMPP7zpa0wmkzp16qS3335bf/vb37Rz585yZSrvVNBl/0xU6p4DemHcCPnWr1uu1wAAAAAAAKBmcPnCmslk0t/+9jcNGTJEw4YN0+jRo9WsWTNlZmZq69atunr1qt588001bNhQkZGRiouLU/PmzXXbbbfp73//uzZu3FhsfTt27NBzzz2n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U5KkXbt2GTBgQC6++OJccskl+cAHPpAhQ4bkj3/8Y/r27ZsVK1Zk2LBhWbVqVYm/DQAAAMBbS5sLa7NmzcrMmTNTV1eXIUOGJEmGDRuWxYsXZ+7cuRk4cGCJJ9zkU5/6VKZOnZok+drXvpZPf/rTefnll7fYb8OGDXnggQfywAMP5LrrrsuNN96Y008/PQcccED+8Ic/iGoAAAAAu0mbWwo6ZcqUjBgxojmqbda7d++0b98+NTU1LbbPmDEjZWVlmTNnzh6b8b3vfW+mTp2axsbGXHzxxZk4ceJWo9qfe/nll3PQQQc1/9ypU6fmK9sAAAAA2LXaVFhbtWpVli9fnvPOO2+L91auXJnq6upUVlY2b/v973+f733veznppJP22Iz77bdfvvvd7yZJrrnmmtx00007ddwb76m2YsWK/OpXv8r++++fGTNm7M5xAQAAANqsNrUUdPOSyK5du7bYvn79+tTX1+eMM85o3rZhw4Zceuml+da3vpWJEye+qfPU19cnScrKyt70jH/zN3+Tnj17ZsmSJfnSl760U8f8eVQbNmxYNmzYkIcffjgjR47M4MGDt3h4QX19faH5AAAAAN7qdnYFYJu6Yq2qqipJ0tDQ0GL71KlTs3r16gwaNKh527XXXpszzjgjtbW1e3LEjB8/PknypS99KRs2bNjh/luLaqtWrcrTTz+df/u3f0uSjBs3brfODAAAANAWtakr1nr16pWamppMmTIlXbp0Sffu3TNnzpzMmzcvSZrD2n333Ze77rordXV1hc6z+f5tOzp+6NChzVe3Jclhhx2WY445Js8//3xuueWWHZ5nW1Fts+9973v5+7//+5x++ulbnbHo9wMAAACgjV2xVl5entmzZ6e6ujrjxo3L6NGjU1VVlQkTJqSioqL5wQULFizIo48+mqOOOio9e/bMr3/964wfPz5f+cpXdut8m8PeokWL8vrrr2933x1FtSR5+OGH88ILL6R79+5bLH8FAAAA4C/Tpq5YS5K+fftmwYIFLbZddNFF6d+/fzp27JgkmTRpUiZNmtT8/tChQ/Pxj38855577m6drUePHkmSRx55ZLv77UxUSzatB25oaMjxxx+fHj165Omnn94tcwMAAAC0RW0urG3NokWL9uiTP7flBz/4QebPQwgjYwAAIZJJREFUn58XX3xxu/sdddRR6dev33aj2mYXXHBBKioqsnLlyl09LgAAAECb1ubD2tq1a9PQ0ND80ICt2VP3InvxxRd3GNWS5IEHHsjw4cPz1FNPbTeqJcljjz22q8YDAAAA4A3afFjr1KlTNm7cWOox3rT77ruv1CMAAAAAtGlt6uEFAAAAALCrCGsAAAAAUICwBgAAAAAFCGsAAAAAUICwBgAAAAAFCGsAAAAAUICwBgAAAAAFtCv1AG1ZbW3tmz7msZWrkyS9enRr8XpPnBsAAACA/yOsldD06dPf9DGTrp+RJLnuqrEtXgMAAACwZ1kKCgAAAAAFCGsAAAAAUICwBgAAAAAFCGsAAAAAUICwBgAAAAAFCGsAAAAAUICwBgAAAAAFCGsAAAAAUICwBgAAAAAFCGsAAAAAUICwBgAAAAAFCGsAAAAAUICwBgAAAAAFtCv1AOx9Jk6cmKVLl+7x89bW1mb69Ol7/LwAAAAAWyOs8aYtXbo09fX1pR4DAAAAoKQsBQUAAACAAoQ1AAAAAChAWAMAAACAAoQ1AAAAAChAWAMAAACAAoQ1AAAAAChAWKNVat++falHAAAAANiudqUegLe2Ll26ZMSIERk8eHD69OmTffbZJy+99FIefPDB3HfffbnzzjuzYcOGFsccdthhmT9/fr7whS/kxz/+cYkmBwAAANg+YY3d4sgjj8xnP/vZXHDBBenQocMW7//1X/91kuSpp57KDTfckGnTpuWVV17JYYcdlrq6uvTp0ydXXHFFfvKTn6SxsXFPjw8AAACwQ212KWhjY2OmTZuWPn36pEOHDhkwYEDq6+vTr1+/jB07ttTj7dXGjx+fZcuW5ZJLLsk+++yTO+64I5/5zGdyzjnn5PTTT8+HP/zhTJs2LQ8//HC6d++ea6+9NkuXLs1ZZ53VHNUWL16cESNGiGoAAABAq9Vmr1gbM2ZM5s6dm8mTJ2fQoEFZuHBhRo0alTVr1uTKK68s9Xh7renTp+fyyy9Pkvz4xz/O1Vdfnccee2yL/WbNmpVPfepTOfXUU/O1r30t1dXV+fnPf57y8vIsXrw4p512Wp577rk9PT4AAADATmuTYW3WrFmZOXNm6urqMmTIkCTJsGHDsnjx4sydOzcDBw4s8YR7p09/+tO5/PLL8+qrr+biiy/OzTffvMNj7rzzzrz//e/PkiVL0rlz5zQ2NuZTn/qUqAYAAAC0em1yKeiUKVMyYsSI5qi2We/evdO+ffvU1NQkSYYOHZojjzwytbW1qa2tzaRJk0ox7l6htrY211xzTZLk3HPP3amolmx6UMHtt9+ezp07Z82aNSkvL8/Xv/71VFZW7sZpAQAAAP5ybe6KtVWrVmX58uW54oortnhv5cqVqa6ubhF1vvzlL+fcc899U+eor69PkpSVlf1lw27FVdd9u/mz3/i61L7xjW+kffv2+drXvpZf/vKXO3XMGx9UsHjx4px11lm566670r9//1x++eWZOnVqi/3r6+tbxXcFAAAA3tqampp2ar82d8XaqlWrkiRdu3ZtsX39+vWpr6+3DLSA4447Lu9617vy/PPP5+qrr96pY/48qp122mlZvXp18/3txo0bl/LyNvfPEwAAANiLtLkr1qqqqpIkDQ0NGTlyZPP2qVOnZvXq1Rk0aFCL/a+++up8/vOfT69evXLttdc2LxPdns1LTOvq6nbd4P/PpOtnJNlUTt/4ek8aOnRo81V5SfKRj3wkSTJz5sysW7duh8dvLaptvqfaf/7nf+bRRx/NUUcdlVNOOaXFeYYMGbJbfqcAAAAARbS5sNarV6/U1NRkypQp6dKlS7p37545c+Zk3rx5SdIirN100015+9vfnrKysvz4xz/O6aefnhUrVmS//fYr1fit0gknnJAkue2223a47/aiWrIpEt5+++0ZP358jj/++BZhDQAAAKA1aXNr7crLyzN79uxUV1dn3LhxGT16dKqqqjJhwoRUVFS0uCKtR48ezff0uuCCC7LPPvvkkUceKdXordYxxxyTJFmyZMl299tRVNts8+fszNWBAAAAAKXS5q5YS5K+fftmwYIFLbZddNFF6d+/fzp27JgkeeWVV7J27drmpaN33nlnXnrppfTu3XuPz9va/fCHP8y+++6bZ599dpv7lJWV5dZbb91hVEuSpUuX5rvf/W7uu+++3TUyAAAAwF+sTYa1rVm0aFFOOumk5p9ffPHFnHHGGXnttddSXl6e/fffP7feemv233//Ek7ZOn384x/f4T5NTU355Cc/mS9+8Yv54Ac/uM2olmz6W1x22WW7ckQAAACAXU5YS7J27do0NDRk/PjxzdsOOeSQPPDAAyWc6q1n4cKFGTZsWKnHAAAAANglhLUknTp1ysaNG0s9BgAAAAB7kTb38AIAAAAA2BWENQAAAAAoQFgDAAAAgAKENQAA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xrZ4DQAAAJBYCgoAAAAAhQhrAAAAAFCAsAYAAAAABQhrAAAAAFCAsAYAAAAABQhrAAAAAFCAsAYAAAAABQhrAAAAAFCAsAYAAAAABQhrAAAAAFCAsAYAAAAABQhrAAAAAFCAsAYAAAAABbQr9QDA1k2cODFLly4tyblra2szffr0kpwbAAAA9hbCGrRSS5cuTX19fanHAAAAALbBUlAAAAAAKEBYAwAAAIAChDUAAAAAKEBYAwAAAIAChDUAAAAAKEBYAwAAAIAChDVo4zp37lzqEQAAAGCv1K7UAwC7xrHHHpszzzwzgwcPTq9evdKuXbs8//zzWbp0ae69997ccsstWb9+fYtjTjzxxMybNy8f//jHM2vWrBJNDgAAAHsnYQ32ckOHDs21116bd7/73Vt9/5RTTsknPvGJPPfcc5kxY0auvfbarFu3LieeeGJuv/32dO7cOWeeeaawBgAAAG9Sm10K2tjYmGnTpqVPnz7p0KFDBgwYkPr6+vTr1y9jx44t9XiwQ5WVlfnGN76RBQsW5N3vfndefPHFzJgxI3/zN3+TwYMHp6amJsOHD8+kSZPy61//OgceeGCuuuqqLFu2LGPHjm2OajfffHMuvvjiUn8dAAAA2Ou02SvWxowZk7lz52by5MkZNGhQFi5cmFGjRmXNmjW58sorSz0ebFeHDh1y6623Zvjw4XnttdfyxS9+MV/5yleybt26FvstW7Ys8+fPz/XXX58TTjghN9xwQ4477rjccMMNKSsry80335wLL7wwGzduLNE3AQAAgL1Xmwxrs2bNysyZM1NXV5chQ4YkSYYNG5bFixdn7ty5GThwYIknhO278cYbM3z48Dz99NMZOXJklixZssNj7r///nziE5/InXfemcrKymzYsCFTp04V1QAAAKCgNrkUdMqUKRkxYkRzVNusd+/ead++fWpqavL888+ntra2+b/+/funrKwsy5YtK9HUsMl5552XUaNG5aWXXsqpp566U1Et2fSggv/4j/9IZWVlHnvssbRr1y7f/e530759+908MQAAALw1tbkr1latWpXly5fniiuu2OK9lStXprq6OpWVlamsrMzSpUub37vpppvy1a9+Nccee+wOz1FfX58kKSsr22Vzb3bVdd9u/uw3vm7t9sa5W+PM7dq1yz//8z8nSf7+7/8+v/3tb3fquDc+qODmm2/ORz/60SxZsiS1tbW57LLL8q1vfavF/vX19SX/rgAAAFAqTU1NO7Vfm7tibdWqVUmSrl27tti+fv361NfXb3MZ6He+8x0PNaDkzjnnnHTv3j2//e1vM2PGjJ065s+j2oUXXpiXXnopn/70p5Mk48eP350jAwAAwFtWm7tiraqqKknS0NCQkSNHNm+fOnVqVq9enUGDBm1xzO9+97ssXrw4v/zlL3fqHJuXmNbV1f3lA/+ZSddviilNTU0tXrd2e+PcpZ556NChzVc/bjZq1Kgk2eIKs23ZWlTbfE+1n/3sZ3nmmWdyzDHH5Jhjjsny5cubjxsyZMhu+fcLAAAAbyVtLqz16tUrNTU1mTJlSrp06ZLu3btnzpw5mTdvXpJsNazNmDEj559/fjp37rynx4UWBg8enCSZP3/+DvfdXlRLktdffz319fU5//zzM3jw4BZhDQAAANixNrcUtLy8PLNnz051dXXGjRuX0aNHp6qqKhMmTEhFRUVqampa7P/qq6/mpptusgyUkuvUqVN69OiR9evXp6GhYbv77iiqbbb5PoLV1dW7Y2QAAAB4S2tzV6wlSd++fbNgwYIW2y666KL0798/HTt2bLH9Zz/7Wbp165Z3vvOde3JE2EJjY2M+97nPZePGjWlsbNzmfvvss0/mzJmzw6iWbFqufO2112bhwoW7a2wAAAB4y2qTYW1rFi1alJNOOmmL7d/5znfy0Y9+tAQTQUsvv/xyvvCFL+xwv9deey0f+tCHcumll+ZjH/vYNqNaktx777259957d+WYAAAA0GYIa0nWrl2bhoaGrT4d8c477yzBRPCXWbhwoavQAAAAYDcT1rLp3lXbu6oHAAAAAP5cm3t4AQAAAADsCsIaAAAAABQgrAEAAABAAcIaAAAAABQgrAEAAABAAcIaAAAAABQgrAEAAABAAe1KPQCwdbW1tYWOe2zl6iRJrx7dWrzeE+cGAACAtkRYg1Zq+vTphY6bdP2MJMl1V41t8RoAAADYtSwFBQAAAIAChDUAAAAAKEBYAwAAAIAChDUAAAAAKEBYAwAAAIAChDUAAAAAKEBYAwAAAIAChDUAAAAAKEBYAwAAAIAChDUAAAAAKEBYAwAAAIAChDUAAAAAKEBYAwAAAIAChDUAAAAAKEBYAwAAAIAChLUS+Pd///fU1NSktrY2p5xySh555JE9PsOECRPSrl27PX7eonr27Jnq6urU1tamtrY2y5YtK/VIO7Ru3bpcfPHF6devX97xjnfk29/+dqlH2qE//vGPzb/j2tradO3aNX/1V39V6rEAAACgVdp7yspbxMsvv5zLL788jzzySKqqqnLDDTfkM5/5TGbPnr3HZvjv//7vrF27do+db1e5/fbbc/jhh5d6jJ32d3/3d6murs73v//9NDU1Zc2aNaUeaYcOOeSQLF26tPnn0047Leedd17pBgIAAIBWTFjbwxobG9PU1JS1a9emqqoqL7zwQrp167bHzv/qq69m0qRJueWWW/LDH/5wj523rXnppZdy6623ZuXKlUmSsrKyHHLIISWe6s35wx/+kEWLFuXWW28t9SgAAADQKpU1NTU1lXqIt5qhQ4cmSerq6rb6/o9+9KOMHTs2nTt3TufOnXPvvfemc+fOW923qakpt/zq7qz8wx+TJKv/+GySpNshB7V4vdngmn5516Bjtjnb1VdfnaOOOiqXXnpp2rVrlw0bNrzZr7dTnlj1dH5+xz3NP29v7o4d9snFHzw9lZX7bPPzevbsmQMPPDBNTU0588wzc80116R9+/a7dObGxsbMuvWu/O9zL+xw5iQZcuKA1PbvvdXP+s1vfpNLLrkkJ554Yu6///4cccQRmT59eo444ohdOnOSPPT7JzL/7geaf97e3Afsv18+cs77UlGx41XgX/nKV7Js2bLMnDlzl88MAAAAbwXusbaHvf766/nmN7+Z//mf/8lTTz2Vc889N1ddddU29y8rK8u7jz82//un55sjSZItXq/+47N5/fUNOf7Yftv8rAcffDD33XdfRo8evWu+zHb0PLxrDu7SuXm2N876xter//hsThhw9HajWrJp+eqSJUtyzz335JFHHsm0adN2+czl5eU55fhj8/SaP+1w5nYVFTn2Hb22+VkbNmzI0qVLc+6552bx4sV5//vfn0svvXSXz5wkR/c+Ih077LNTv+t3DT52p6JasulegB/5yEd2+bwAAADwViGs7WFLly5NU1NTjj766CTJBRdckIULF273mIO7HJAz3/vO7e5TVlaW888cmn322fZVXPfcc09++9vf5sgjj0zPnj2zcePG9OzZMy+++OKb/yI74Zz3vTv7d9p3u/sMOPqobV719UZvf/vbkyT77bdfLrvssh3+zorq0f3QDDupdrv7tG/fLuefNTQV5dv+3+fwww/PQQcdlNNOOy3Jpr/zAw88sM39/xLlZWU5b+TQVG7nb58k7x58bHof0X2nPvO3v/1t1qxZk/e+9727YkQAAAB4SxLW9rDDDz88jzzySJ566qkkyR133JH+/fvv8LgTa49O3yPfvs33h73zuPTofuh2P2PcuHH5wx/+kCeeeCJPPPFEKioq8sQTT2T//fd/c19iJ+3bsUPOHTl0m+/v32nffGD4u3b4OevWrWuOfxs3bsxPf/rT1NTU7Koxt3Dquwale9eqbb5/5rCTcnCXA7b7GYceemiqq6uzePHiJJv+ztXV1btyzBYO7Py2nL2d3+UhBx2Y04ccv9Of94Mf/CAf/vCHU76deAgAAABtnYcX7GHdunXLddddl+HDh6d9+/Y5+OCDc+ONNybZdD+1srKyrR5XVlaWc894T6bfOCcvv/Jqi/e6d63KqScP3O2zF9H3yMPzzoHVuXfxQ1u8d97Iodm3Y4cdfsYzzzyTD37wg2lsbMzGjRvzzne+M1dfffXuGDdJUlFRng+dOSz/8v252bBhY4v3+h759pxYe/ROfc63vvWtjBkzJuvWrcsBBxyQf/u3f9sd4zYbWN0nD//+/8vyhsdbbK8oL8+H3j8s7dvt3P/uTU1N+dGPfpRf/OIXu2NMAAAAeMvw8ILdYEcPL9iWn93+36moKM/Zp237yqMHf/dYfvTz+c0/t2tXkU9e/MEcUnVgkVH3iNde35Cvz/xp1vzpheZt7xxYvVNXq5XSPYuW5xd3/t+S0307VGbimPN2uLy1lNa9/Er++cbZWbtuffO2099zfIa987gSTgUAAABvTdZ5tRLPPv9i/ufB3yXZ+hVrm9W8o1eOq/6/e5KdMeTEVh3VkmSf9u1y/lnDUv7/rsY7uEvnnDH0xBJPtWPvHFTd4p5kf3X6Ka06qiXJfvt2yLlnDGn++Yjuh+Y9Jw4o4UQAAADw1tVqwto111yTsrKyLF++PGeeeWY6deqUbt265ctf/nKS5LbbbsvAgQOz77775rjjjsvdd9/d4viFCxfm9NNPT+fOndOxY8eccsopW+yzaNGinH/++enRo0c6duyY3r175xOf+EReeOGFFvutWLEi5557brp27ZrKysp07949Z599dp599tnsLgvuXZLysvIM3YkIcvZp70rnt+2X3kd0zzsH7b77du1Kb+92SN578sCUl5Xl/LOGZZ/2rX8VcnlZWc4dOSQdKvfJcdW9t/sU0NbkHUf1yIm1R28KmmcO2+5DFgAAAIDiWs1S0GuuuSaf//zn8453vCOXXXZZBgwYkJtuuik/+MEPctVVV+WXv/xlPvOZz+Rtb3tbrr766jz55JN54okn8ra3vS2/+tWvctZZZ+W9731vxo4dm8rKyvzrv/5r7rzzztx99905/vhNN22fM2dOHn744QwYMCCdO3fOihUr8qUvfSmHHnpo7rnnnuZZ+vXrl/333z//8A//kEMPPTRPP/107rjjjnzuc5/L4YcfvsPv0qNXvyTJhz/2d7vnlwUAAADAbnPdVWN3ar9WF9a+9a1v5W//9m+TJK+++moOPfTQvPzyy2loaEjPnj2TJHfddVdOPfXUzJkzJ3/913+dvn37pqqqKnfffXfzUww3bNiQY445Jr169cq8efO2es4NGzbk3nvvzXve854sWbIktbW1+d///d8cfPDBueWWW/KBD3yg0HcR1gAAAAD2Xjsb1lrderyRI0c2v66srEyvXr2ycePG5qiWJO94xzuSJE8++WRWrFiR3//+95k4cWIaGxvT2NjYvN9pp52W733ve80/r127Ntddd11uvvnmPPnkk3n11f97uuYjjzyS2traHHTQQenVq1cmTZqUZ555Ju95z3uaz7ezevXolmTn/gjPPv9ivvKdm3PScdU5+7ST39R5AAAAACidVhfWunTp0uLnffbZJx06dNhiW5K88soreeaZZ5IkEyZMyIQJE7b6mevXr0/Hjh1z6aWX5rbbbss111yTgQMH5m1ve1uefPLJfPCDH8z69ZueolhWVpb58+fnC1/4Qj7zmc9kzZo1OfzwwzNhwoRcddVVKSvb/sMFkmTthookyaTrZ+z09174wPIsfGD5Tu8PAAAAwO6x116x9mYddNBBSTYtJT3zzDO3uk9lZWVeeeWV/OxnP8tnP/vZ/N3f/d8SzT9/cEGSHHnkkfne976XpqamPPTQQ7nxxhvzj//4j6mqqspll122w5lOe/+HCn4bAAAAAPYWe31Y69evX3r16pVly5blc5/73Db3e/XVV7Nhw4a0b9++xfYbb7xxm8eUlZXlmGOOyVe/+tXccMMNWbZs2U7NtLNVc85t9Vn60Ir8w8cuyP5v22+njgEAAACgddjrw1pZWVluuOGGnHnmmfnABz6Qj3zkIznkkEOyZs2aLF68OK+//nq+/OUvp3Pnzjn55JMzbdq0HHrooTnssMPyk5/8JPfdd1+Lz3vwwQfzyU9+Mueff3769OmTJJk9e3bWr1+f008/fZfN/ezzL2bx8oacdFy1qAYAAACwF9rrw1qSDB8+PAsXLswXv/jFjBs3Li+99FIOOeSQDBw4MB/96Eeb9/vRj36Uj3/845k4cWIqKipy1lln5eabb87gwYOb9+natWt69uyZr33ta1m1alXat2+fo48+Oj/5yU9aPFjhL/Wn517M/p32y9ATB+yyzwQAAABgzylrampqKvUQbVVjY2PKy8tLPQYAAAAABQhrAAAAAFCAy6UAAAAAoABhDQAAAAAKENYAAAAAoABhDQAAAAAKENYAAAAAoABhDQAAAAAKENYAAAAAoABhDQAAAAAKENYAAAAAoABhDQAAAAAKENYAAAAAoABhDQAAAAAKENYAAAAAoABhDQAAAAAKENYAAAAAoABhDQAAAAAKENYAAAAAoABhDQAAAAAKENYAAAAAoABhDQAAAAAKENYAAAAAoABhDQAAAAAKENYAAAAAoABhDQAAAAAKENYAAAAAoABhDQAAAAAKENYAAAAAoID/H93BbjBl9fh2AAAAAElFTkSuQmCC\n", + "image/png": "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Ew4e2wkTbX8nTjgoO9d2bHVUqVTKqRPHp3u3nTp03PTp03PxxRfn9ttvz4QJEzp0bGcZMnC3HDR27w4f19G/O126dMmQIUMyatSonH/++Zk6dWo+/vGPd/i6L5fQRpLnl0h26bLxH4euXbumtbW10P1gAQAAAABei0qlUk4+5tAOreoatHu/HLz/Plu9/0EHHZSf/OQnG3129913Z8CAAWVf0ZQk++w5OPuOHLrV+3ft2iXvOn58Krq+elfzvViPyu5558TDO3TMYW8e3eHVfJ/85CfzqU99KnfeeecrJrK94JjDDsjr+uyy1fv32aVn4dWgL2hpaSnLwiGhjSTJ2972tkyfPj233XZbli1blp/85Cf5xCc+kWOPPTY9evQo93gAAAAAAK8atTVVecfRW/ecre7ddso7Jx6+yUKILfnwhz+c3/3ud/n4xz+ev/zlL/nud7+br371q/noRz9adORt7qQjt35V19GHHrDVq/le0NjYmMWLF2fx4sV57rnnUl9fn8WLF+fhhx8uMu42N2TAbjlov61b1VVkNd+FF16Ya665JjfddFP23HPP1NfXp76+Pv/4xz+KjLvN7bRTRU49fny6dCm95L6lUimnHt+x1XyXX355/vu//zuPPvpoHnzwwXzzm9/MtGnTcu65576csQsptVqutMPqyIP8mpqacsUVV+TWW2/NU089lde97nU5/vjjc+WVV6Z37479Cw4AAAAAgOQHc36eRX9eusV9TjnusOy3z54dPvecOXNy6aWX5i9/+Uvq6uoyZcqU/H//3/9XdNRO8cjjT+Wb/3nHFvcZtHu/nHf68elSeukg82L33HNPxo8fv8nnhx12WO65554OnauzrF/fnK9+97/yt6ef3ew+FV27ZvK5b+9waCxt5ud17rnn5sYbb+zQuTrT//x6UX42b+EW9xk/bkyOPvSADp33wx/+cH784x/nySefTGVlZQYPHpz3vve9Of/889N1K1dGdqShbInQtgPbVn9IXqy5eUOea25OVWWxh1kCAAAAALxWrF33XL78nVvy7KrGdrePGDowZ7/9yM1Gkx3BHf/zm/xq4X3tbuvebadc+N5T0mvn2u081fbzZH1DvnbTbWlpaT/FTBz/lhxywKjtPNX2s6GlJTO/d3v+96m/tbv9Da/vk387+21luW3otmoobh1Jhyz4018y7evfz/zf/7ncowAAAAAAvKJVdu+Wd048PO1ltJqqHnnHMYfs0JEtSY4+bP+8rk+vdredOOGgHTqyJcludX0z4aD2bws5eI835KAOPJvv1ahrly45deL47LRTxSbbKrp2zbuOP+JV/2w+oY2ttr65OT//zeKse259unbgfsEAAAAAAK9Vb9zjDTl4/01XLJ187KGpqepRhom2r50qKvKuE8Zv8t+URw4bmDftPbRMU21fh71lTPZ4w+s3+qx7t51y6sTDO3zLzFejvr13zsTxb9nk82MOOyCv79t+hH012WFqSalUaiv/P/7xj3PIIYekZ8+e6du3b0455ZQ88sgjbfvecccdOeyww7LLLrukZ8+eOemkk7J06ebvk7tu3bp85StfyYEHHphddtkllZWV2XPPPXPxxRenoaGh3WPuvffeXHLJJdl///1TV1eX7t27Z7fddsupp56aBQsWbPZad955Z4477ri87nWvy0477ZQ+ffpk+PDhee9735v58+cX/OlsGwv/9GBWNTZl59rqQvcMBgAAAAB4LTrq0P02CgoHjN4rw4cMKONE29dur++bCQf/v1VdNdU98o6jD93hV/O9oGuXLjn1+MPT7UWruk468qDs0rOmjFNtX28eMzx7Dt697f0bB7whB+63dxkn2nZ2mND2gq997Ws58cQT8+ijj2bIkCFZvXp1br311hx22GH5+9//ni9/+cs54YQTsnTp0gwePDjNzc25/fbbc+ihh+bvf//7Juf761//mnHjxuXCCy/Mvffem1122SXDhg3L448/nunTp2fs2LF59NFHNznuzDPPzNVXX51HH300ffv2zciRI7Nu3br88Ic/zIEHHphbb711k2Ouu+66TJw4MXfddVdaWloyatSovP71r8/y5ctzww035KabbuqUn9nWeGE1W5KMH7dvKipe3Us5AQAAAAC2l50qKvKu459f1dV7l9pMPGJcuUfa7g578+gM2O35VV2nHHtYqqsqyzzR9tW3186ZeMTzq7r2HjYo+458bazme0GpVMrJxx6Wqh7d073bTnnncTvOar5Sa2tr+0/ge5V5oXxXVVXl61//es4555wkyTPPPJNjjjkmCxYsyPHHH5//+Z//yTe+8Y227U8//XSOPvro/P73v88ll1ySz3/+823nbG1tzfjx4/OLX/wiEydOzIwZMzJw4MAkzz8Yb8qUKfnO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AAAKENoAAAAAAACgAKENAAAAAAAAChDaAAAAAAAAoAChDQAAAAAAAAoQ2gAAAAAAAKAAoQ0AAAAAAAAKENoAAAAAAACgAKENAAAAAAAAChDaAAAAAAAAoAChDQAAAAAAAAoQ2gAAAAAAAKAAoQ0AAAAAAAAKENoAAAAAAACgAKENAAAAAAAAChDaAAAAAAAAoAChDQAAAAAAAAoQ2gAAAAAAAKAAoQ0AAAAAAAAKENoAAAAAAACgAKENAAAAAAAAChDaAAAAAAAAoAChDQAAAAAAAAoQ2gAAAAAAAKAAoQ0AAAAAAAAKENoAAAAAAACgAKENAAAAAAAAChDaAAAAAAAAoAChDQAAAAAAAAoQ2gAAAAAAAKAAoQ0AAAAAAAAKENoAAAAAAACgAKENAAAAAAAAChDaAAAAAAAAoAChDQAAAAAAAAoQ2gAAAAAAAKAAoQ0AAAAAAAAKENoAAAAAAACgAKENAAAAAAAAChDaAAAAAAAAoAChDQAAAAAAAAoQ2gAAAAAAAKAAoQ0AAAAAAAAKENoAAAAAAACgAKENAAAAAAAAChDaAAAAAAAAoAChDQAAAAAAAAoQ2gAAAAAAAKAAoQ0AAAAAAAAKENoAAAAAAACgAKENAAAAAAAAChDaAAAAAAAAoAChDQAAAAAAAAoQ2gAAAAAAAKAAoQ0AAAAAAAAKENoAAAAAAACgAKENAAAAAAAAChDaAAAAAAAAoAChDQAAAAAAAAoQ2gAAAAAAAKAAoQ0AAAAAAAAKENoAAAAAAACgAKENAAAAAAAAChDaAAAAAAAAoAChDQAAAAAAAAoQ2gAAAAAAAKAAoQ0AAAAAAAAKENoAAAAAAACgAKENAAAAAAAAChDaAAAAAAAAoAChDQAAAAAAAAoQ2gAAAAAAAKAAoQ0AAAAAAAAKENoAAAAAAACgAKENAAAAAAAAChDaAAAAAAAAoAChDQAAAAAAAAoQ2gAAAAAAAKAAoQ0AAAAAAAAKENoAAAAAAACgAKENAAAAAAAAChDaAAAAAAAAoAChDQAAAAAAAAoQ2gAAAAAAAKAAoQ0AAAAAAAAKENoAAAAAAACgAKENAAAAAAAAChDaAAAAAAAAoAChDQAAAAAAAAoQ2gAAAAAAAKAAoQ0AAAAAAAAKENoAAAAAAACgAKENAAAAAAAAChDaAAAAAAAAoAChDQAAAAAAAAoQ2gAA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emm7kQ0AAAAAAGB7c+vITrZ48eIce+yxqa+vT3V1dUaMGJGnnnoq1157bR555JE888wzSZIxY8aUd9CXacSIEfn+97+f0aNHt322bt26/PCHP8yPfvSj/P73v89jjz3Wtq1r164ZMWJE9t9//5xxxhl561vfmiTp06dPZs2alVNPPTWLFy/Opz71qbZjLr300lx11VXb70sBAAAAAABsgVtHdqKGhobsu+++Wb58eS666KJcfvnlqa2tTZJcffXVueSSS1JRUZENGzbk2WefTc+ePbfp9bfXrSMPOOCA3H333enVq1eSZP369bnmmmvy5S9/OX//+9+36hx77bVXPvWpT+XUU09td/vWRDa3jgQAAAAAALYnoa0TnXHGGZk1a1YmT56cr371q5tsHzNmTP74xz9m0KBBefTRR7f59bdHaBs9enTuueee7LLLLkmSP/3pTzn33HOzePHiIiPn5JNPzo033rjRtT/zmc/kk5/85EseK7QBAAAAAADbk2e0dZIlS5Zk9uzZ6du372ZXYo0dOzZJNrrd4r869thjUyqVcsUVV3TGmC9LdXV1brvttrbINnfu3Bx44IGFI1uSDBo0aJPAt/fee7+MKQEAAAAAADqH0NZJZs2alZaWlpx55pmbXRnWo0ePJJsPbT/4wQ9eVrTqbNOmTcugQYOSJPfee29OOOGENDU1FT7fRz7ykVxzzTVt718419vf/vacdtppL29YAAAAAACAbUxo6yRz585NkowfP36z+yxfvjxJ+6Ft1apVufDCCzN9+vTOGfBlOvDAA/OhD30oyfNB7Iwzzsjq1asLn+9fI9ull16ac845p+39jBkz2p4BBwAAAAAA8EpQUe4BdlSPP/54kmTAgAHtbm9ubs78+fOTtB/aPv7xj2fYsGE588wzc9ZZZ73seYYOHZouXba+q7a0tGxx+0UXXdT264997GMv6xlz7UW2F263OXv27LzrXe9Knz598u53vztf+tKXNnuejn5HAAAAAACAurq6LFy4sNjBrXSKXr16tSZp/fWvf93u9ptvvrk1SWttbW1rS0vLRtsWLFjQ2r1799Y///nPra2tra1JWi+//PIOz9DY2NiaZJu/dtttt9bm5ubW1tbW1ieffLK1oqKi8Lk+8pGPbDTzxz72sY22Dxs2rG3bQw891FoqlTrlO3l5eXl5eXl5eXl5eXl5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73L++ednt91222hl54EHHiiyAQAAAADwiia0daILLrggy5cvz+TJkzN9+vTU1ta2bZs6dWpGjx6d5ubmDBw4MD179izjpFDctddem3e/+91Jkg0bNuSiiy7KYYcdlr/85S8dOs8//vGP/Pu//3tOPvnkrF+/vu3z5557Lscff7zIBgAAAADAK47Q1kmWLFmS2bNnp2/fvrnqqqva3Wfs2LFJktGjR2+y7bbbbsuBBx6Y6urq7LzzzjnooINy//33d+rM0FFve9vbMnny5CTPr0575zvfmS9+8YtpaWkpdL66urpceeWV2Wmnndo+69atWy644IJtMi8AAAAAAGxLQlsnmTVrVlpaWnLmmWempqam3X169OiRZNPQdu211+bUU0/NwQcfnNtvvz2zZs3KhAkTsmbNmk6fG7ZW7969841vfKPt/fnnn5/bbrut8Pnq6uoyd+7cDB8+PEmyfPnytltOvv/978+RRx758gYGAAAAAIBtrKLcA+yo5s6dmyQZP378ZvdZvnx5ko1D2yOPPJKLL744X/rSl9pWCiXJcccd10mTQjFXXnllXv/61ydJfvSjH+Xb3/524XP9a2R74ZlsxxxzTFvM+8Y3vpGhQ4cWXi0HAAAAAADbmtDWSV54ntSAAQPa3d7c3Jz58+cn2Ti0fec738lOO+2U8847b5vOM3To0HTpUr4FjG9/z4WprumZFfUr0r9//03e7+hejd9/S0Fr5513zjnnnJMkWbVqVc4///zC19lcZHv88cczc+bMnHbaaTn88MMzePDgHHfccbnjjjs2e65y/zkHAAAAAODVp66uLgsXLix0rNDWSZqampJks7d7nD17dhoaGlJbW5tBgwa1ff7rX/86e+65Z26++eZceeWVeeKJJzJ06NB88pOfzOmnn154nhUrVhQ+dlto2bCh7Z9PPvnkJu93dDva9z/33HNTXV2dJPnud7+b+vr6QufZUmR7wdVXX53DDz88SfKhD31oi6Gt3H/OAQAAAAB4bRHaOkldXV1WrlyZRYsWZdy4cRttW7FiRS6++OIkyahRo1IqlTba9uSTT+ZjH/tYpk2blt133z3f/va3c8YZZ2TXXXfNhAkTCs3Tr1+/sq706dK1a9s/d9ttt03e7+hejd+/paVls+HqxdH361//eqHzb01kS5Kf/OQnefTRRzN48OAcc8wx6d27d555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AADAK5FbR3aiCy64IMuXL8/kyZMzffr0tsiWJFOnTs3o0aPT3NycgQMHpmfPnmWcFCiqtrY2P/3pT9si2z//+c+cf/75eetb37rZyNae+fPn59hjj8273/3u/POf/0yStsj285//XGQDAAAAAHgFEto6yZIlSzJ79uz07ds3V111Vbv7jB07NkkyevTots8OP/zwlEqldl/nn3/+dpkd2Ho33nhj9ttvvyTPr1IbN25cZs6cmdbW1kLne+ihh1IqlTb6rKWlRWQDAAAAAHgFcuvITjJr1qy0tLTkzDPPTE1NTbv79OjRI8nGoe26667LqlWrNtpvzpw5ufLKK3P88cd33sBAh5122ml5xzvekSR5+umn89a3vjVLliwpfL5x48blJz/5Sdu/M9atW5fu3bvnrW99a84777x885vf3CZzAwAAAACwbVjR1knmzp2bJBk/fvxm91m+fHmSjUPbiBEj8pa3vGWj1+LFi7Prrru2PbsJKL8+ffpkxowZbe8/8IEPbJPI9sItZn/605/mne98Z9v2L3zhC9ltt92KDwwAAAAAwDZnRVsnefzxx5MkAwYMaHd7c3Nz5s+fn2Tj0Pav/v73v+fuu+/Ov/3bv6Wiovhv19ChQ9OlS/m66tvfc2Gqa3pmRf2K9O/ff5P3O7rX+vdPXn0/g5aWli1uf//7358+ffokSWbPnp1bb7218LXai2wvPJPt29/+diZNmpTa2tp86EMfyqWXXrrZ85T77zkAAAAAwKtRXV1dFi5cWOhYoa2TNDU1JUnWrFnT7vbZs2enoaEhtbW1GTRo0GbPM2vWrDQ3N+fss89+WfOsWLHiZR3/crVs2ND2zyeffHKT9zu61/r3T3asn0GXLl3anpnY0tKSj370o4XPtaXIliSXXnppzj777HTr1i3ve9/7csUVV+S5555r91zl/nsOAAAAAPBaI7R1krq6uqxcuTKLFi3KuHHjNtq2YsWKXHzxxUmSUaNGpVQqbfY8N910U4YPH5799tvvZc3Tr1+/sq506dK1a9s/d9ttt03e7+he698/efX9DFpaWjYbro4++ujsscceSZ5/huKyZcsKXeOlIluS/O1vf8utt96a008/Pbvuumve9ra35Qc/+EG75yv333MAAAAAgFejurq6wscKbZ1kwoQJWbJkSaZNm5Yjjzwyw4YNS5IsWLAgZ599dhoa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v1qOPnT/jB+370vu16NHj7S2tuaUU07JhRdemKeeeiqTJ0/OIYccku9973uFrr0tbNjQkq/f/KMsr/97oeNf37dXJp/79uxUsfX/H8TChQuz//775+67787RRx9d6LoAAAAAAHRcl3IPwCvDLbfckuuuuy433HBDFi1alB/+8Ie566678olPfKJD56mq7J53Tjys0AxVld1zynGHbVVka2hoyFlnnZUbbrjhFXf7xGMOe3N27b3lULY5xx/xlpeMbEly3HHH5fTTT8/o0aNzyCGH5Oabb84hhxySa665ptB1t6Xd+70uRxz4pkLH7vGG1+fQN4/eqn1bWlrSu3fv3HDDDdlvv/1y4okn5otf/GK+//3v55lnnil0/W2ha9cuOfX48amo6NrxY7s8f2xHIlvy/Gq2QYMG5aijjurwNQEAAAAAKE5oI0ly0UUXZcqUKTn77LOzzz775JRTTsnnPve5XH311Vm7dm2HzjV0YP8cOHbvDs/w9qMPSc+arbvl35///Oc89dRTOf7441NRUZGKiop8+tOfziOPPJKKiop8//vf7/D1t5VuO1XkXccfkS5dtn4lYJLsOXj3HDB6eOHrjhs3LsuWLSt8/LY0fty+6d9v15fe8UW67VSRU48/PF27bN2/lvr165dhw4Zlp53+321GR44cmSR5/PHHO3Ttbe11fXbJcYe/ucPHTTh4bHZ7fd8OHbNq1arMmjUr73//+zu0+hQAAAAAgJdPaCPJ87eZ7PIvgaNr165pbW1NkbuLHnvYAVv1nLEX7DtySPbZa/BW77///v9/e/ce42V15gH8OzMMDAwM98sICMpd8IarWxFk2QpyEQHFa7dDtFWsYNCisdnabLpp0najWeta1MYgtSXa7na91NJ28VICWhoqDGgFyjVSrVqxpIIIDMP+QZhCAZ35QQHHzyeZhDnnPed93nfCX9887zk3r7zySqqrq+t+brrppnTv3j3V1dUZN25cg2s+mrpVdmxQV1eL5s1y+Zj6dfMdztKlS9O9e/eC1x9NJSXFuWrciJQ2oKtr3D+fX69uvn2GDRuWtWvXpqampm5s9erVSZKePXvWe5+/l88MHpg+PbvW+/oeXTtneD27+fb3wx/+MDt37sx1113X4LUAAAAAABwZQRtJkokTJ+buu+/OE088kY0bN+aXv/xl7rrrrowZMybNmzdv8H6lpU1y1fgR9erqat2qPJdedEGD9i8vL8+gQYMO+OnUqVOaNm2aQYMGpXXrwj7deDSNOP/sdK9nV1dDuvmS5Mtf/nKef/75rF+/PtXV1Zk2bVrmz5+fW2+9tcBqj76O7dtk7IjP1Ova/r1Oznln9m/Q/rfffnv+9Kc/5eabb86qVavywgsv5Pbbb09VVVXatm1bSMlHVXFRUSaP/ac0L2v2sdc2LW2SK8eNOCjsro+HHnooEydOTOfOnQspEwAAAACAIyBoI0ly3333paqqKjNnzkz//v1zww03ZPTo0fn+979f8J7dunTMZy8452Ovu2Jc/cKIT5q/nrf10V1dgwf1yen96t/NlyR//OMfU1VVlQEDBmTUqFFZvXp1nn322YwfP/5ISj7qPnP2ael7SrePvKZF82a5fPSFDe7mO/PMMzNv3rwsW7YsZ511Vq677rpMmjQpDzzwwJGUfFS1blWeiSM/PkS+5LND0r5tRYP3X7x4cVasWJGpU6cWUh4AAAAAAEeoaE8h3wXkE2Hbtm1p2bJlkmTr1q0pLy8/4j0/2P5h3tvyfr3P39pdW5uH5j6d199855DzQ//h9Fzy2fOPuK4T2eJlr+XJ/1t0yLk2FS1z6/WTU9as6TGu6tj5y/vb8p+z/yfbP9xxyPl/mTgyg/qdcoyrOrYee/q5LF+57pBzA3qfnKrLLna+GgAAAADAJ5CONhpk4ZJXcv+jT+Rnzy+u1/UlxcW5ctyIlJY2OWiuU/u2uXj4uUe7xBPOP541IH1POfjstKLs7eZrzCFbklS0Ks+kUUMPOTd4UN9GH7IlyYRRQ1PR8uCgu7x5WS4roJsPAAAAAIATg6CNetu2/cO89PKrSZKe3brUe12Hdq0z7m/O6iopLs5V40ektMnBAVxjU1RUlMljLkyLv/k85gXnnp5eJ590nKo6ts4Y0Ctnndb7gLE2FS1z6UVDjlNFx1aLsma5Yuzwg8YvG31hWpXX/2w+AAAAAABOLI0maCsqKqrrCvnpT3+aYcOGpaKiIh06dMjkyZOzbt1fP9v2zDPPZPjw4WnTpk0qKioyYcKErFmz5rB779ixI9/5zncyZMiQtGnTJmVlZenXr1/uuOOOvPvuu4dc85vf/CZ33nlnzj333HTp0iXNmjVL165dc+WVV2bJkiWHvde8efMyduzYdOrUKaWlpWnfvn0GDBiQ66+/Pi+++GKBb+foWLTklezYuSuVndrntD49GrT2H88akH6n/rWr66Kh56Rr5w5Hu8QTVkWr8ky8eFjd7507tM3FFzb+br79XTrygrRutberqyjJlZ+Cbr799TmlW4acM7Du93NO75uBfXsev4IAAAAAADhijeaMtn0h2/3335/p06fnpJNOSufOnbNq1aps3749Xbt2zbJlyzJ37tzcdtttqaysTJcuXermu3TpkhUrVqRjxwPPHnv77bczZsyYLFu2LMXFxenevXsqKiry+9//Pjt27MjJJ5+cF154IaeeeuoB63r37p1169alXbt2qaysTNOmTfP6669n8+bNadKkSR5//PFcfvnlB6yZNWtWpk2bliRp3759evToke3bt2fTpk3ZunVrpk6dmgcffLDe7+RontG2bfuH+Y8HH8uOnbvy+UmjCgoI/rL1g9w7+7/TsV2b3Hjt+JQUN5qct95+9MzzWbFyfaZVTcxJn6KgcZ+1G9/Iwz/6WS4874yM/Zsux0+Dnbtq8l9z/jc1u3dnxnWXf6qCRgAAAACAxqjRBW0tWrTIAw88kKqqqiTJe++9l9GjR2fJkiW55JJL8txzz+XBBx+sm9+8eXMuvvjivPzyy7nzzjvzrW99q27PPXv2ZMSIEVmwYEHGjRuX+++/Pz179kyyN7iaMWNGZs+enfPPPz8vvfTSAfU8+uijGTJkSHr3/uvn8mpra/PUU0+lqqoqTZo0yaZNm+qCsJqamnTq1Cl//vOfM2vWrNx4440pKSmpq2PhwoXZvHlzJk2aVO93sn/Q9m/3PJymzcoa8koP8OGOndm5a1eKi4tT3rys4DOlampqUlxSnOKiT1/IliR7sic1u2pSWlp6vEs5bnbu2pWmpU2yt6/t02d37e5kT+r+fwMAAAAAcHy1atk8t0y5rKC1jS5ou+WWW3LfffcdMPeLX/wiY8aMOez8z3/+84wdOzZnnHFGli9fXjc+b968jBs3LoMGDcqSJUtSVnZgULV79+6cd955Wbp0aRYtWpQLLrigXrV+7Wtfyze+8Y089thjufrqq5Mkb731ViorK9O2bdu89957DXv4w9g/aLvt3+9L06bNPmYFAAAAAADAp0tFy/L867TPFbS2yVGu5bj74he/eNDY4MGD6zW/fv36A8Z/8pOfJEmmTJlyUMiW7O1IufTSS7N06dL86le/OihoW7NmTR5//PEsX748mzdvzq5du5Ik77zzTpKkurq6Lmjr2LFjysrKsmXLlsyfPz8jR46s9zPXR0V5i4I72o5WNxsAAAAAAMCJplXL5gWvbXRBW69evQ4a2//ctUPNd+rUKcnez0Hub8WKFUmSRx55JE8++eQh7/f2228nSd54440Dxu+555585StfSU1NzWFr3bx5c92/S0pKMmPGjHz729/OqFGjMnjw4Fx00UUZOnRohg8fnoqKisPuUx93TL26oDPa9p3NliSfm3BRQWezAQAAAAAANEaN7tORh3ucQub79OmTtWvX1uv+U6ZMyZw5c5IkL774YoYOHZqSkpJ8/etfz4QJE9KzZ8+Ul5enqKgos2fPzhe+8IUD1iR7z3CbNWtWvvvd72bVqlV1482aNcu1116bu+++O+3atatXPfue5YMPPsgDc5/Kjp21BXWi6WYDAAAAAAAasyM5o63RdbQdTfvON3v66aczfvz4eq/7wQ9+kCSZOXNmvvrVrx40v38n2/6Ki4szffr0TJ8+PX/4wx+ycOHCzJ8/Pz/+8Y/zyCOPZNOmTZk/f3696ygqKkp5eXl27tqT97d9UO91h1JbW3vEewAAAAAAADQmgraPMHDgwFRXV+fVV19tUNC2YcOGJMnQoUMPOb948eKP3aNbt2655pprcs0112TmzJk5/fTT8+yzz2bDhg055ZRT6l1LUvi3RXWzAQAAAAAAjZ0z2v5OJk+enLlz5+Z73/tebrnllroOt4/TvPneP8hbb7110NyaNWvyzDPPNKiOgQMHpnXr1tmyZUvefPPNBgdthbQ7OpsNAAAAAADgoxUf7wJOZBMmTMjw4cOzcePGjBo1Kq+++uoB87W1tfn1r3+dm266KevXr68bHzZsWJLkm9/8ZtatW1c3/rvf/S7jx49PcfHBr/21117LDTfckMWLFx9wTtzu3btz7733ZsuWLSkrK8vAgQOP9mMe0qIlr2THzl2p7NQ+p/XpcUzuCQAAAAAA8ElStGf/VOcTbN9nDQ/3OIXOv/vuu5kwYUJeeumlJEmPHj3SpUuXbN++PevWrcu2bduSJCtXrkz//v2TJO+//34GDx6ctWvXprS0NP369UttbW1WrlyZysrK3HzzzbnrrrsyZcqUzJkzJ0lSXV2ds88+O0nSqlWr9OrVKyUlJdm4cWPdmW6zZs3Kl770pYLeT0Ps62bbsXNXPj9plG42AAAAAACAQ9DR9jE6dOiQBQsWZM6cORk5cmS2bduW3/72t9mwYUN69+6dGTNmZMGCBenbt2/dmlatWmXRokW5/vrr07Zt26xevTpbt27N1KlTs3Tp0nTt2vWg+/Tt2zcPP/xwrrrqqlRWVmb9+vVZvnx5ysrKcsUVV2ThwoXHJGRL9p7NdvJJnXSzAQAAAAAAfIRG09HG0ffhjp0pa9b0eJcBAAAAAABwQhK0AQAAAAAAQAF8OhIAAAAAAAAKIGgDAAAAAACAAgjaAAAAAAAAoACCNgAAAAAAACiAoA0AAAAAAAAKIGgDAAAAAACAAgjaAAAAAAAAoACCNgAAAAAAACiAoA0AAAAAAAAKIGgDAAAAAACAAgjaAAAAAAAAoACCNgAAAAAAACiAoA0AAAAAAAAKIGgDAAAAAACAAgjaAAAAAAAAoACCNgAAAAAAACiAoA0AAAAAAAAKIGgDAAAAAACAAgjaAAAAAAAAoACCNgAAAAAAACiAoA0AAAAAAAAKIGgDAAAAAACAAgjaAAAAAAAAoACCNgAAAAAAACiAoA0AAAAAAAAKIGgDAAAAAACAAgjaAAAAAAAAoACCNgAAAAAAACiAoA0AAAAAAAAKIGgDAAAAAACAAgjaAAAAAAAAoACCNgAAAAAAACiAoA0AAAAAAAAKIGgDAAAAAACAAgjaAAAAAAAAoACCNgAAAAAAACiAoA0AAAAAAAAKIGgDAAAAAACAAgjaAAAAAAAAoAD/D6beT7XiUGU6AAAAAElFTkSuQmCC", "text/plain": [ - "
" + "
" ] }, "execution_count": 7, @@ -374,7 +372,7 @@ " \n", " cutn.destroy(handle)\n", " \n", - " return e\n", + " return e.get()\n", " \n", " return expectation\n", "\n", @@ -469,7 +467,7 @@ "name": "stdout", "output_type": "stream", "text": [ - " fun: -5.974513781450166\n", + " fun: -5.974513781450138\n", " maxcv: 0.0\n", " message: 'Optimization terminated successfully.'\n", " nfev: 126\n", @@ -599,7 +597,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.12" + "version": "3.10.11" } }, "nbformat": 4, diff --git a/python/samples/cutensornet/coarse/example23_torch_grad.py b/python/samples/cutensornet/coarse/example23_torch_grad.py new file mode 100644 index 0000000..11361eb --- /dev/null +++ b/python/samples/cutensornet/coarse/example23_torch_grad.py @@ -0,0 +1,56 @@ +# Copyright (c) 2023, NVIDIA CORPORATION & AFFILIATES +# +# SPDX-License-Identifier: BSD-3-Clause + +""" +Example: Demo using cuquantum.contract() in a PyTorch compute graph out of box. + +This sample requires PyTorch. +""" + +import torch + +import cuquantum +from cuquantum import cutensornet as cutn + + +# random-initialize input tensors on GPU, and require gradient computation of all inputs +kwargs = {'device': 'cuda', + 'requires_grad': True, + 'dtype': torch.complex128} +a = torch.rand((3, 4), **kwargs) +b = torch.rand((4, 5, 6, 3), **kwargs) +c = torch.rand((3, 3), **kwargs) + +# create a hypothetical workload using PyTorch operators +def compute(func, expr, a, b, c): + # note: cannot perform in-place ops on leaf nodes + a = a * a + b = -10 + b + c = torch.cos(c) + d = func(expr, a, b, c) + return torch.sum(d, dim=0, keepdim=True) + +# use cuquantum.contract() in the workload to compute gradients +out_cuqnt = compute(cuquantum.contract, "ab,bcde,ef->acdf", a, b, c) + +# backprop to fill the gradients +output_grad = torch.ones_like(out_cuqnt) +out_cuqnt.backward(output_grad) + +# store the computed gradients for later verification +input_grads_cuqnt = (a.grad, b.grad, c.grad) + +# now let's reset the gradients and redo the computation using +# torch.einsum() for comparison +a.grad, b.grad, c.grad = None, None, None +out_torch = compute(torch.einsum, "ab,bcde,ef->acdf", a, b, c) +out_torch.backward(output_grad) +input_grads_torch = (a.grad, b.grad, c.grad) + +# check results +assert all( + torch.allclose(grad_cuqnt, grad_torch) + for grad_cuqnt, grad_torch in zip(input_grads_cuqnt, input_grads_torch) +) +print("all checked!") diff --git a/python/samples/cutensornet/experimental/example01-pairwise_canonicalization.py b/python/samples/cutensornet/experimental/example01-pairwise_canonicalization.py index 7ff69c0..70eb727 100644 --- a/python/samples/cutensornet/experimental/example01-pairwise_canonicalization.py +++ b/python/samples/cutensornet/experimental/example01-pairwise_canonicalization.py @@ -5,16 +5,15 @@ """ Example of pairwise tensor canonicalization with contract_decompose -NumPy ndarrays are used as inputs. +CuPy ndarrays are used as inputs. """ -import numpy as np +import cupy as cp from cuquantum import contract from cuquantum.cutensornet.experimental import contract_decompose - -a = np.ones((2,2,2)) -b = np.ones((2,2,2)) +a = cp.ones((2,2,2)) +b = cp.ones((2,2,2)) # use QR to canonicalize two tensors: # i k m i k m @@ -28,8 +27,10 @@ a_qr, b_qr = contract_decompose('ijk,klm->ijk,klm', a, b, algorithm=canonicalize_algorithm) # compare the difference after canonicalization -diff = contract('ijk,klm', a, b) - contract('ijk,klm', a_qr, b_qr) +out1 = contract('ijk,klm', a, b) +out2 = contract('ijk,klm', a_qr, b_qr) + +assert cp.allclose(out1, out2) print("After canonicalization") print(f" Shape of A, B: {a_qr.shape} {b_qr.shape}") -print(f" Maxdiff error: {abs(diff).max()}") \ No newline at end of file diff --git a/python/samples/cutensornet/fine/example5_cupy_grad.py b/python/samples/cutensornet/fine/example5_cupy_grad.py new file mode 100644 index 0000000..4f3f72d --- /dev/null +++ b/python/samples/cutensornet/fine/example5_cupy_grad.py @@ -0,0 +1,79 @@ +# Copyright (c) 2023, NVIDIA CORPORATION & AFFILIATES +# +# SPDX-License-Identifier: BSD-3-Clause + +""" +Example: Computing the gradients of a tensor network with CuPy ndarrays. + +The gradients are returned as CuPy ndarrays. + +This example is also straightforwardly applicable to NumPy ndarrays and PyTorch tensors. +""" + +import cupy as cp +import numpy as np + +import cuquantum +from cuquantum import cutensornet as cutn + + +# random-initialize input tensors +a = cp.random.random((3, 4, 5)) +b = cp.random.random((4, 5, 6)) +c = cp.random.random((6, 5, 2)) + +# create tensor qualifiers for all input tenors +qualifiers = np.zeros(3, dtype=cutn.tensor_qualifiers_dtype) + +# require gradient computation of all inputs +qualifiers[:]['requires_gradient'] = 1 + +# create a network +tn = cuquantum.Network("abc,bcd,dce->ae", a, b, c, qualifiers=qualifiers) + +# perform contraction as usual +# this would prepare the internal cache for gradient computation +tn.contract_path() +out = tn.contract() + +# prepare the seed gradient (w.r.t. the output tensor itself, so it's 1) +output_grad = cp.ones_like(out) + +# compute the gradients +input_grads = tn.gradients(output_grad) + +# the gradient tensors have the same type as the input tensors +assert all(isinstance(arr, cp.ndarray) for arr in input_grads) + +# free the network +tn.free() + +# check results against PyTorch (if installed) +try: + import torch +except ImportError: + torch = None +if torch is not None and torch.cuda.is_available(): + # create torch tenros via zero-copy + a_t = torch.as_tensor(a, device='cuda') + b_t = torch.as_tensor(b, device='cuda') + c_t = torch.as_tensor(c, device='cuda') + + # require gradient computation of all inputs + a_t.requires_grad_(True) + b_t.requires_grad_(True) + c_t.requires_grad_(True) + + # compute the contraction + out_t = torch.einsum("abc,bcd,dce->ae", a_t, b_t, c_t) + + # backprop to fill the gradients + output_grad_t = torch.ones_like(out_t) + out_t.backward(output_grad_t) + + # check results (zero-copy torch tensors as cupy arrays) + assert cp.allclose(out_t.detach(), out) # non-leaf nodes need to be detached first + assert cp.allclose(a_t.grad, input_grads[0]) + assert cp.allclose(b_t.grad, input_grads[1]) + assert cp.allclose(c_t.grad, input_grads[2]) + print("all checked!") diff --git a/python/samples/cutensornet/high_level/amplitudes_example.py b/python/samples/cutensornet/high_level/amplitudes_example.py new file mode 100755 index 0000000..aca6c57 --- /dev/null +++ b/python/samples/cutensornet/high_level/amplitudes_example.py @@ -0,0 +1,128 @@ +# Copyright (c) 2023, NVIDIA CORPORATION & AFFILIATES +# +# SPDX-License-Identifier: BSD-3-Clause + +import cupy as cp +import numpy as np + +import cuquantum +from cuquantum import cutensornet as cutn + + +print("cuTensorNet-vers:", cutn.get_version()) +dev = cp.cuda.Device() # get current device +props = cp.cuda.runtime.getDeviceProperties(dev.id) +print("===== device info ======") +print("GPU-name:", props["name"].decode()) +print("GPU-clock:", props["clockRate"]) +print("GPU-memoryClock:", props["memoryClockRate"]) +print("GPU-nSM:", props["multiProcessorCount"]) +print("GPU-major:", props["major"]) +print("GPU-minor:", props["minor"]) +print("========================") + +################################################# +# Accessor computation of a quantum circuit state +################################################# + +# Quantum state configuration +num_qubits = 6 +dim = 2 +qubits_dims = (dim, ) * num_qubits # qubit size +fixed_modes = (0, 1) # open qubits +num_fixed_modes = len(fixed_modes) +fixed_values = (1, 1) +print(f"Quantum circuit with {num_qubits} qubits") + +############# +# cuTensorNet +############# + +handle = cutn.create() +stream = cp.cuda.Stream() +data_type = cuquantum.cudaDataType.CUDA_C_64F + +# Define quantum gate tensors on device +gate_h = 2**-0.5 * cp.asarray([[1,1], [1,-1]], dtype='complex128', order='F') +gate_h_strides = 0 + +gate_cx = cp.asarray([[1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 0, 1], + [0, 0, 1, 0]], dtype='complex128').reshape(2,2,2,2, order='F') +gate_cx_strides = 0 + +# Allocate device memory for the specified slice of the quantum circuit amplitudes tensor +amplitudes_shape = [qubits_dims[i] for i in range(num_qubits) if i not in fixed_modes] +amplitudes = cp.empty(amplitudes_shape, dtype='complex128') +amplitudes_strides = [stride_in_bytes // amplitudes.itemsize for stride_in_bytes in amplitudes.strides] + +free_mem = dev.mem_info[0] +# use half of the totol free size +scratch_size = free_mem // 2 +scratch_space = cp.cuda.alloc(scratch_size) +print(f"Allocated {scratch_size} bytes of scratch memory on GPU") + +# Create the initial quantum state +quantum_state = cutn.create_state(handle, cutn.StatePurity.PURE, num_qubits, qubits_dims, data_type) +print("Created the initial quantum state") + +# Construct the quantum circuit state with gate application +tensor_id = cutn.state_apply_tensor( + handle, quantum_state, 1, (0, ), + gate_h.data.ptr, gate_h_strides, 1, 0, 1) + +for i in range(1, num_qubits): + tensor_id = cutn.state_apply_tensor( + handle, quantum_state, 2, (i-1, i), # target on i-1 while control on i + gate_cx.data.ptr, gate_cx_strides, 1, 0, 1) +print("Quantum gates applied") + +# Specify the quantum circuit amplitudes accessor +accessor = cutn.create_accessor(handle, + quantum_state, num_fixed_modes, fixed_modes, amplitudes_strides) + +# Configure the computation of the specified slice of the quantum circuit amplitudes tensor +num_hyper_samples_dtype = cutn.accessor_get_attribute_dtype(cutn.AccessorAttribute.OPT_NUM_HYPER_SAMPLES) +num_hyper_samples = np.asarray(8, dtype=num_hyper_samples_dtype) +cutn.accessor_configure(handle, accessor, + cutn.AccessorAttribute.OPT_NUM_HYPER_SAMPLES, + num_hyper_samples.ctypes.data, num_hyper_samples.dtype.itemsize) + +# Prepare the computation of the specified slice of the quantum circuit amplitudes tensor +work_desc = cutn.create_workspace_descriptor(handle) +cutn.accessor_prepare(handle, accessor, scratch_size, work_desc, stream.ptr) +print("Prepare the computation of the specified slice of the quantum circuit amplitudes tensor") + +workspace_size_d = cutn.workspace_get_memory_size(handle, + work_desc, cutn.WorksizePref.RECOMMENDED, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH) + +if workspace_size_d <= scratch_size: + cutn.workspace_set_memory(handle, work_desc, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH, scratch_space.ptr, workspace_size_d) +else: + print("Error:Insufficient workspace size on Device") + cutn.destroy_workspace_descriptor(work_desc) + cutn.destroy_accessor(accessor) + cutn.destroy_state(quantum_state) + cutn.destroy(handle) + del scratch + print("Free resource and exit.") + exit() +print("Set the workspace buffer") + +# Compute the specified slice of the quantum circuit amplitudes tensor +state_norm = np.empty(1, dtype='complex128') +cutn.accessor_compute(handle, accessor, + fixed_values, work_desc, amplitudes.data.ptr, state_norm.ctypes.data, stream.ptr) +stream.synchronize() +print("Computed the specified quantum circuit state amplitudes") + +print(amplitudes) +print(f"norm of the state = {state_norm.item()}") + +cutn.destroy_workspace_descriptor(work_desc) +cutn.destroy_accessor(accessor) +cutn.destroy_state(quantum_state) +cutn.destroy(handle) +del scratch_space +print("Free resource and exit.") \ No newline at end of file diff --git a/python/samples/cutensornet/high_level/expectation_example.py b/python/samples/cutensornet/high_level/expectation_example.py new file mode 100755 index 0000000..f313d12 --- /dev/null +++ b/python/samples/cutensornet/high_level/expectation_example.py @@ -0,0 +1,159 @@ +# Copyright (c) 2023, NVIDIA CORPORATION & AFFILIATES +# +# SPDX-License-Identifier: BSD-3-Clause + +import cupy as cp +import numpy as np + +import cuquantum +from cuquantum import cutensornet as cutn + + +print("cuTensorNet-vers:", cutn.get_version()) +dev = cp.cuda.Device() # get current device +props = cp.cuda.runtime.getDeviceProperties(dev.id) +print("===== device info ======") +print("GPU-name:", props["name"].decode()) +print("GPU-clock:", props["clockRate"]) +print("GPU-memoryClock:", props["memoryClockRate"]) +print("GPU-nSM:", props["multiProcessorCount"]) +print("GPU-major:", props["major"]) +print("GPU-minor:", props["minor"]) +print("========================") + +#################################################### +# Expectation computation of a quantum circuit state +#################################################### + +# Quantum state configuration +num_qubits = 16 +dim = 2 +qubits_dims = (dim, ) * num_qubits # qubit size +print(f"Quantum circuit with {num_qubits} qubits") + +############# +# cuTensorNet +############# + +handle = cutn.create() +stream = cp.cuda.Stream() +data_type = cuquantum.cudaDataType.CUDA_C_64F + +# Define quantum gate tensors on device +gate_h = 2**-0.5 * cp.asarray([[1,1], [1,-1]], dtype='complex128', order='F') +gate_h_strides = 0 + +# Pauli X gate +gate_x = cp.asarray([[0, 1], [1, 0]]).T.astype('complex128', order='F') +# Pauli Y gate +gate_y = cp.asarray([[0, -1j], [1j, 0]]).T.astype('complex128', order='F') +# Pauli Z gate +gate_z = cp.asarray([[1, 0], [0, -1]]).T.astype('complex128', order='F') + +gate_cx = cp.asarray([[1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 0, 1], + [0, 0, 1, 0]], dtype='complex128').reshape(2,2,2,2, order='F') +gate_cx_strides = 0 + +free_mem = dev.mem_info[0] +# use half of the totol free size +scratch_size = free_mem // 2 +scratch_space = cp.cuda.alloc(scratch_size) +print(f"Allocated {scratch_size} bytes of scratch memory on GPU") + +# Create the initial quantum state +quantum_state = cutn.create_state(handle, cutn.StatePurity.PURE, num_qubits, qubits_dims, data_type) +print("Created the initial quantum state") + +# Construct the quantum circuit state with gate application +tensor_id = cutn.state_apply_tensor( + handle, quantum_state, 1, (0, ), + gate_h.data.ptr, gate_h_strides, 1, 0, 1) + +for i in range(1, num_qubits): + tensor_id = cutn.state_apply_tensor( + handle, quantum_state, 2, (i-1, i), # target on i-1 while control on i + gate_cx.data.ptr, gate_cx_strides, 1, 0, 1) +print("Quantum gates applied") + +# Create an empty tensor network operator +hamiltonian = cutn.create_network_operator(handle, + num_qubits, qubits_dims, data_type) +# Append component (0.5 * Z1 * Z2) to the tensor network operator +num_modes = (1, 1) # Z1 acts on 1 mode, Z2 acts on 1 mode +modes_Z1 = (1, ) # state modes Z1 acts on +modes_Z2 = (2, ) # state modes Z2 acts on +state_modes = (modes_Z1, modes_Z2) # state modes (Z1 * Z2) acts on +gate_data = (gate_z.data.ptr, gate_z.data.ptr) # GPU pointers to gate data +operator_id = cutn.network_operator_append_product(handle, hamiltonian, 0.5, + 2, num_modes, state_modes, 0, gate_data) +# Append component (0.25 * Y3) to the tensor network operator +num_modes = (1, ) # Y3 acts on 1 mode +modes_Y3 = (3, ) # state modes Y3 acts on +state_modes = (modes_Y3, ) # state modes (Y3) acts on +gate_data = (gate_y.data.ptr, ) # GPU pointers to gate data +operator_id = cutn.network_operator_append_product(handle, hamiltonian, 0.25, + 1, num_modes, state_modes, 0, gate_data) + +# Append component (0.13 * Y0 X2 Z3) to the tensor network operator +num_modes = (1, 1, 1) # Y0 acts on 1 mode, X2 acts on 1 mode, Z3 acts on 1 mode +modes_Y0 = (0, ) # state modes Y0 acts on +modes_X2 = (2, ) # state modes X2 acts on +modes_Z3 = (3, ) # state modes Z3 acts on +state_modes = (modes_Y0, modes_X2, modes_Z3) # state modes (Y0 * X2 * Z3) acts on +gate_data = (gate_y.data.ptr, gate_x.data.ptr, gate_z.data.ptr) # GPU pointers to gate data +operator_id = cutn.network_operator_append_product(handle, hamiltonian, 0.13, + 3, num_modes, state_modes, 0, gate_data) +print("Constructed a tensor network operator: (0.5 * Z1 * Z2) + (0.25 * Y3) + (0.13 * Y0 * X2 * Z3)") + +# Specify the quantum circuit expectation value computation +expectation = cutn.create_expectation(handle, quantum_state, hamiltonian) + +# Configure the quantum circuit expectation value computation +num_hyper_samples_dtype = cutn.expectation_get_attribute_dtype(cutn.ExpectationAttribute.OPT_NUM_HYPER_SAMPLES) +num_hyper_samples = np.asarray(8, dtype=num_hyper_samples_dtype) +cutn.expectation_configure(handle, expectation, + cutn.ExpectationAttribute.OPT_NUM_HYPER_SAMPLES, + num_hyper_samples.ctypes.data, num_hyper_samples.dtype.itemsize) + +# Prepare the computation of the specified quantum circuit expectation value +work_desc = cutn.create_workspace_descriptor(handle) +cutn.expectation_prepare(handle, expectation, scratch_size, work_desc, stream.ptr) +print("Prepare the computation of the specified quantum circuit expectation value") + +workspace_size_d = cutn.workspace_get_memory_size(handle, + work_desc, cutn.WorksizePref.RECOMMENDED, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH) + +if workspace_size_d <= scratch_size: + cutn.workspace_set_memory(handle, work_desc, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH, scratch_space.ptr, workspace_size_d) +else: + print("Error:Insufficient workspace size on Device") + cutn.destroy_workspace_descriptor(work_desc) + cutn.destroy_expectation(expectation) + cutn.destroy_network_operator(hamiltonian) + cutn.destroy_state(quantum_state) + cutn.destroy(handle) + del scratch + print("Free resource and exit.") + exit() +print("Set the workspace buffer") + +# Compute the specified quantum circuit expectation value +expectation_value = np.empty(1, dtype='complex128') +state_norm = np.empty(1, dtype='complex128') +cutn.expectation_compute(handle, expectation, + work_desc, expectation_value.ctypes.data, state_norm.ctypes.data, stream.ptr) +stream.synchronize() +print("Computed the specified quantum circuit state amplitudes") + +print(f"expectation value = {expectation_value.item()}") +print(f"norm of the state = {state_norm.item()}") + +cutn.destroy_workspace_descriptor(work_desc) +cutn.destroy_expectation(expectation) +cutn.destroy_network_operator(hamiltonian) +cutn.destroy_state(quantum_state) +cutn.destroy(handle) +del scratch_space +print("Free resource and exit.") \ No newline at end of file diff --git a/python/samples/cutensornet/high_level/marginal_example.py b/python/samples/cutensornet/high_level/marginal_example.py index c23e208..e06aece 100755 --- a/python/samples/cutensornet/high_level/marginal_example.py +++ b/python/samples/cutensornet/high_level/marginal_example.py @@ -56,6 +56,12 @@ rdm = cp.empty(rdm_shape, dtype='complex128') rdm_strides = [stride_in_bytes // rdm.itemsize for stride_in_bytes in rdm.strides] +free_mem = dev.mem_info[0] +# use half of the totol free size +scratch_size = free_mem // 2 +scratch_space = cp.cuda.alloc(scratch_size) +print(f"Allocated {scratch_size} bytes of scratch memory on GPU") + # Create the initial quantum state quantum_state = cutn.create_state(handle, cutn.StatePurity.PURE, num_qubits, qubits_dims, data_type) print("Created the initial quantum state") @@ -74,12 +80,7 @@ # Specify the desired reduced density matrix (marginal) marginal = cutn.create_marginal(handle, quantum_state, num_marginal_modes, marginal_modes, 0, 0, rdm_strides) -free_mem = dev.mem_info[0] -# use half of the totol free size -scratch_size = free_mem // 2 -scratch_space = cp.cuda.alloc(scratch_size) -print(f"Allocated {scratch_size} bytes of scratch memory on GPU") - +# Configure the computation of the desired reduced density matrix (marginal) num_hyper_samples_dtype = cutn.marginal_get_attribute_dtype(cutn.MarginalAttribute.OPT_NUM_HYPER_SAMPLES) num_hyper_samples = np.asarray(8, dtype=num_hyper_samples_dtype) cutn.marginal_configure(handle, marginal, diff --git a/python/samples/cutensornet/high_level/mps_amplitudes_example.py b/python/samples/cutensornet/high_level/mps_amplitudes_example.py new file mode 100755 index 0000000..f6183be --- /dev/null +++ b/python/samples/cutensornet/high_level/mps_amplitudes_example.py @@ -0,0 +1,185 @@ +# Copyright (c) 2023, NVIDIA CORPORATION & AFFILIATES +# +# SPDX-License-Identifier: BSD-3-Clause + +import cupy as cp +import numpy as np + +import cuquantum +from cuquantum import cutensornet as cutn + + +print("cuTensorNet-vers:", cutn.get_version()) +dev = cp.cuda.Device() # get current device +props = cp.cuda.runtime.getDeviceProperties(dev.id) +print("===== device info ======") +print("GPU-name:", props["name"].decode()) +print("GPU-clock:", props["clockRate"]) +print("GPU-memoryClock:", props["memoryClockRate"]) +print("GPU-nSM:", props["multiProcessorCount"]) +print("GPU-major:", props["major"]) +print("GPU-minor:", props["minor"]) +print("========================") + +################################################# +# Accessor computation of a quantum circuit state +################################################# + +# Quantum state configuration +num_qubits = 6 +dim = 2 +qubits_dims = (dim, ) * num_qubits # qubit size +fixed_modes = (0, 1) # open qubits +num_fixed_modes = len(fixed_modes) +fixed_values = (1, 1) +print(f"Quantum circuit with {num_qubits} qubits") + +############# +# cuTensorNet +############# + +handle = cutn.create() +stream = cp.cuda.Stream() +data_type = cuquantum.cudaDataType.CUDA_C_64F + +# Define quantum gate tensors on device +gate_h = 2**-0.5 * cp.asarray([[1,1], [1,-1]], dtype='complex128', order='F') +gate_h_strides = 0 + +gate_cx = cp.asarray([[1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 0, 1], + [0, 0, 1, 0]], dtype='complex128').reshape(2,2,2,2, order='F') +gate_cx_strides = 0 + +# Allocate device memory for the final MPS state +max_extent = 2 +mps_tensor_extents = [] +mps_tensor_strides = [] +mps_tensors = [] +mps_tensor_ptrs = [] +for i in range(num_qubits): + if i == 0: + extents = (2, max_extent) + elif i == num_qubits - 1: + extents = (max_extent, 2) + else: + extents = (max_extent, 2, max_extent) + mps_tensor_extents.append(extents) + tensor = cp.zeros(extents, dtype='complex128') + mps_tensors.append(tensor) + mps_tensor_ptrs.append(tensor.data.ptr) + mps_tensor_strides.append([stride_in_bytes // tensor.itemsize for stride_in_bytes in tensor.strides]) + +# Allocate device memory for the specified slice of the quantum circuit amplitudes tensor +amplitudes_shape = [qubits_dims[i] for i in range(num_qubits) if i not in fixed_modes] +amplitudes = cp.empty(amplitudes_shape, dtype='complex128') +amplitudes_strides = [stride_in_bytes // amplitudes.itemsize for stride_in_bytes in amplitudes.strides] + +free_mem = dev.mem_info[0] +# use half of the totol free size +scratch_size = free_mem // 2 +scratch_space = cp.cuda.alloc(scratch_size) +print(f"Allocated {scratch_size} bytes of scratch memory on GPU") + +# Create the initial quantum state +quantum_state = cutn.create_state(handle, cutn.StatePurity.PURE, num_qubits, qubits_dims, data_type) +print("Created the initial quantum state") + +# Construct the quantum circuit state with gate application +tensor_id = cutn.state_apply_tensor( + handle, quantum_state, 1, (0, ), + gate_h.data.ptr, gate_h_strides, 1, 0, 1) + +for i in range(1, num_qubits): + tensor_id = cutn.state_apply_tensor( + handle, quantum_state, 2, (i-1, i), # target on i-1 while control on i + gate_cx.data.ptr, gate_cx_strides, 1, 0, 1) +print("Quantum gates applied") + +# Specify the target MPS state +cutn.state_finalize_mps(handle, quantum_state, cutn.BoundaryCondition.OPEN, mps_tensor_extents, mps_tensor_strides) +print("Set the final MPS representation") + +# Configure the MPS computation +svd_algorithm_dtype = cutn.state_get_attribute_dtype(cutn.StateAttribute.MPS_SVD_CONFIG_ALGO) +svd_algorithm = np.array(cutn.TensorSVDAlgo.GESVDJ, dtype=svd_algorithm_dtype) +cutn.state_configure(handle, quantum_state, + cutn.StateAttribute.MPS_SVD_CONFIG_ALGO, svd_algorithm.ctypes.data, svd_algorithm.dtype.itemsize) + +# Prepare the specified quantum circuit for MPS computation +work_desc = cutn.create_workspace_descriptor(handle) +cutn.state_prepare(handle, quantum_state, scratch_size, work_desc, stream.ptr) +print("Prepared the specified quantum circuit for MPS computation") + +workspace_size_d = cutn.workspace_get_memory_size(handle, + work_desc, cutn.WorksizePref.RECOMMENDED, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH) +if workspace_size_d <= scratch_size: + cutn.workspace_set_memory(handle, work_desc, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH, scratch_space.ptr, workspace_size_d) +else: + print("Error:Insufficient workspace size on Device") + cutn.destroy_workspace_descriptor(work_desc) + cutn.destroy_marginal(marginal) + cutn.destroy_state(quantum_state) + cutn.destroy(handle) + del scratch + print("Free resource and exit.") + exit() +print("Set the workspace buffer for MPS computation") + +# Compute the final MPS state +extents_out, strides_out = cutn.state_compute(handle, quantum_state, work_desc, mps_tensor_ptrs, stream.ptr) + +# If a lower extent is found during runtime, the cupy.ndarray container must be adjusted to reflect the lower extent +for i, (extent_in, extent_out) in enumerate(zip(mps_tensor_extents, extents_out)): + if extent_in != tuple(extent_out): + stride_out = [s * mps_tensors[0].itemsize for s in strides_out[i]] + mps_tensors[i] = cp.ndarray(extent_out, dtype=mps_tensors[i].dtype, memptr=mps_tensors[i].data, strides=stride_out) +print("Computed the final MPS representation") + +# Specify the quantum circuit amplitudes accessor +accessor = cutn.create_accessor(handle, + quantum_state, num_fixed_modes, fixed_modes, amplitudes_strides) + +num_hyper_samples_dtype = cutn.accessor_get_attribute_dtype(cutn.AccessorAttribute.OPT_NUM_HYPER_SAMPLES) +num_hyper_samples = np.asarray(8, dtype=num_hyper_samples_dtype) +cutn.accessor_configure(handle, accessor, + cutn.AccessorAttribute.OPT_NUM_HYPER_SAMPLES, + num_hyper_samples.ctypes.data, num_hyper_samples.dtype.itemsize) + +# Prepare the computation of the specified slice of the quantum circuit amplitudes tensor +cutn.accessor_prepare(handle, accessor, scratch_size, work_desc, stream.ptr) +print("Prepare the computation of the specified slice of the quantum circuit amplitudes tensor") + +workspace_size_d = cutn.workspace_get_memory_size(handle, + work_desc, cutn.WorksizePref.RECOMMENDED, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH) + +if workspace_size_d <= scratch_size: + cutn.workspace_set_memory(handle, work_desc, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH, scratch_space.ptr, workspace_size_d) +else: + print("Error:Insufficient workspace size on Device") + cutn.destroy_workspace_descriptor(work_desc) + cutn.destroy_accessor(accessor) + cutn.destroy_state(quantum_state) + cutn.destroy(handle) + del scratch + print("Free resource and exit.") + exit() +print("Set the workspace buffer") + +# Compute the specified slice of the quantum circuit amplitudes tensor +state_norm = np.empty(1, dtype='complex128') +cutn.accessor_compute(handle, accessor, + fixed_values, work_desc, amplitudes.data.ptr, state_norm.ctypes.data, stream.ptr) +stream.synchronize() +print("Computed the specified quantum circuit state amplitudes") + +print(amplitudes) +print(f"norm of the state = {state_norm.item()}") + +cutn.destroy_workspace_descriptor(work_desc) +cutn.destroy_accessor(accessor) +cutn.destroy_state(quantum_state) +cutn.destroy(handle) +del scratch_space +print("Free resource and exit.") \ No newline at end of file diff --git a/python/samples/cutensornet/high_level/mps_expectation_example.py b/python/samples/cutensornet/high_level/mps_expectation_example.py new file mode 100755 index 0000000..d8e3803 --- /dev/null +++ b/python/samples/cutensornet/high_level/mps_expectation_example.py @@ -0,0 +1,231 @@ +# Copyright (c) 2023, NVIDIA CORPORATION & AFFILIATES +# +# SPDX-License-Identifier: BSD-3-Clause + +import cupy as cp +import numpy as np + +import cuquantum +from cuquantum import cutensornet as cutn + + +print("cuTensorNet-vers:", cutn.get_version()) +dev = cp.cuda.Device() # get current device +props = cp.cuda.runtime.getDeviceProperties(dev.id) +print("===== device info ======") +print("GPU-name:", props["name"].decode()) +print("GPU-clock:", props["clockRate"]) +print("GPU-memoryClock:", props["memoryClockRate"]) +print("GPU-nSM:", props["multiProcessorCount"]) +print("GPU-major:", props["major"]) +print("GPU-minor:", props["minor"]) +print("========================") + +#################################################### +# Expectation computation of a quantum circuit state +#################################################### + +# Quantum state configuration +num_qubits = 16 +dim = 2 +qubits_dims = (dim, ) * num_qubits # qubit size +print(f"Quantum circuit with {num_qubits} qubits") + +############# +# cuTensorNet +############# + +handle = cutn.create() +stream = cp.cuda.Stream() +data_type = cuquantum.cudaDataType.CUDA_C_64F + +# Define quantum gate tensors on device +gate_h = 2**-0.5 * cp.asarray([[1,1], [1,-1]], dtype='complex128', order='F') +gate_h_strides = 0 + +# Pauli X gate +gate_x = cp.asarray([[0, 1], [1, 0]]).T.astype('complex128', order='F') +# Pauli Y gate +gate_y = cp.asarray([[0, -1j], [1j, 0]]).T.astype('complex128', order='F') +# Pauli Z gate +gate_z = cp.asarray([[1, 0], [0, -1]]).T.astype('complex128', order='F') + +gate_cx = cp.asarray([[1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 0, 1], + [0, 0, 1, 0]], dtype='complex128').reshape(2,2,2,2, order='F') +gate_cx_strides = 0 + +# Allocate device memory for the final MPS state +max_extent = 2 +mps_tensor_extents = [] +mps_tensor_strides = [] +mps_tensors = [] +mps_tensor_ptrs = [] +for i in range(num_qubits): + if i == 0: + extents = (2, max_extent) + elif i == num_qubits - 1: + extents = (max_extent, 2) + else: + extents = (max_extent, 2, max_extent) + mps_tensor_extents.append(extents) + tensor = cp.zeros(extents, dtype='complex128') + mps_tensors.append(tensor) + mps_tensor_ptrs.append(tensor.data.ptr) + mps_tensor_strides.append([stride_in_bytes // tensor.itemsize for stride_in_bytes in tensor.strides]) + +free_mem = dev.mem_info[0] +# use half of the totol free size +scratch_size = free_mem // 2 +scratch_space = cp.cuda.alloc(scratch_size) +print(f"Allocated {scratch_size} bytes of scratch memory on GPU") + +# Create the initial quantum state +quantum_state = cutn.create_state(handle, cutn.StatePurity.PURE, num_qubits, qubits_dims, data_type) +print("Created the initial quantum state") + +# Construct the quantum circuit state with gate application +tensor_id = cutn.state_apply_tensor( + handle, quantum_state, 1, (0, ), + gate_h.data.ptr, gate_h_strides, 1, 0, 1) + +for i in range(1, num_qubits): + tensor_id = cutn.state_apply_tensor( + handle, quantum_state, 2, (i-1, i), # target on i-1 while control on i + gate_cx.data.ptr, gate_cx_strides, 1, 0, 1) +print("Quantum gates applied") + +# Create the vacuum quantum state +quantum_state = cutn.create_state(handle, cutn.StatePurity.PURE, num_qubits, qubits_dims, data_type) +print("Created the initial quantum state") + +# Construct the quantum circuit state with gate application +tensor_id = cutn.state_apply_tensor( + handle, quantum_state, 1, (0, ), + gate_h.data.ptr, gate_h_strides, 1, 0, 1) + +for i in range(1, num_qubits): + tensor_id = cutn.state_apply_tensor( + handle, quantum_state, 2, (i-1, i), # target on i-1 while control on i + gate_cx.data.ptr, gate_cx_strides, 1, 0, 1) +print("Quantum gates applied") + +# Specify the target MPS state +cutn.state_finalize_mps(handle, quantum_state, cutn.BoundaryCondition.OPEN, mps_tensor_extents, mps_tensor_strides) +print("Set the final MPS representation") + +# Configure the MPS computation +svd_algorithm_dtype = cutn.state_get_attribute_dtype(cutn.StateAttribute.MPS_SVD_CONFIG_ALGO) +svd_algorithm = np.array(cutn.TensorSVDAlgo.GESVDJ, dtype=svd_algorithm_dtype) +cutn.state_configure(handle, quantum_state, + cutn.StateAttribute.MPS_SVD_CONFIG_ALGO, svd_algorithm.ctypes.data, svd_algorithm.dtype.itemsize) + +# Prepare the specified quantum circuit for MPS computation +work_desc = cutn.create_workspace_descriptor(handle) +cutn.state_prepare(handle, quantum_state, scratch_size, work_desc, stream.ptr) +print("Prepared the specified quantum circuit for MPS computation") + +workspace_size_d = cutn.workspace_get_memory_size(handle, + work_desc, cutn.WorksizePref.RECOMMENDED, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH) +if workspace_size_d <= scratch_size: + cutn.workspace_set_memory(handle, work_desc, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH, scratch_space.ptr, workspace_size_d) +else: + print("Error:Insufficient workspace size on Device") + cutn.destroy_workspace_descriptor(work_desc) + cutn.destroy_marginal(marginal) + cutn.destroy_state(quantum_state) + cutn.destroy(handle) + del scratch + print("Free resource and exit.") + exit() +print("Set the workspace buffer for MPS computation") + +# Compute the final MPS state +extents_out, strides_out = cutn.state_compute(handle, quantum_state, work_desc, mps_tensor_ptrs, stream.ptr) + +# If a lower extent is found during runtime, the cupy.ndarray container must be adjusted to reflect the lower extent +for i, (extent_in, extent_out) in enumerate(zip(mps_tensor_extents, extents_out)): + if extent_in != tuple(extent_out): + stride_out = [s * mps_tensors[0].itemsize for s in strides_out[i]] + mps_tensors[i] = cp.ndarray(extent_out, dtype=mps_tensors[i].dtype, memptr=mps_tensors[i].data, strides=stride_out) +print("Computed the final MPS representation") + +# Create an empty tensor network operator +hamiltonian = cutn.create_network_operator(handle, + num_qubits, qubits_dims, data_type) +# Append component (0.5 * Z1 * Z2) to the tensor network operator +num_modes = (1, 1) # Z1 acts on 1 mode, Z2 acts on 1 mode +modes_Z1 = (1, ) # state modes Z1 acts on +modes_Z2 = (2, ) # state modes Z2 acts on +state_modes = (modes_Z1, modes_Z2) # state modes (Z1 * Z2) acts on +gate_data = (gate_z.data.ptr, gate_z.data.ptr) # GPU pointers to gate data +operator_id = cutn.network_operator_append_product(handle, hamiltonian, 0.5, + 2, num_modes, state_modes, 0, gate_data) +# Append component (0.25 * Y3) to the tensor network operator +num_modes = (1, ) # Y3 acts on 1 mode +modes_Y3 = (3, ) # state modes Y3 acts on +state_modes = (modes_Y3, ) # state modes (Y3) acts on +gate_data = (gate_y.data.ptr, ) # GPU pointers to gate data +operator_id = cutn.network_operator_append_product(handle, hamiltonian, 0.25, + 1, num_modes, state_modes, 0, gate_data) + +# Append component (0.13 * Y0 X2 Z3) to the tensor network operator +num_modes = (1, 1, 1) # Y0 acts on 1 mode, X2 acts on 1 mode, Z3 acts on 1 mode +modes_Y0 = (0, ) # state modes Y0 acts on +modes_X2 = (2, ) # state modes X2 acts on +modes_Z3 = (3, ) # state modes Z3 acts on +state_modes = (modes_Y0, modes_X2, modes_Z3) # state modes (Y0 * X2 * Z3) acts on +gate_data = (gate_y.data.ptr, gate_x.data.ptr, gate_z.data.ptr) # GPU pointers to gate data +operator_id = cutn.network_operator_append_product(handle, hamiltonian, 0.13, + 3, num_modes, state_modes, 0, gate_data) +print("Constructed a tensor network operator: (0.5 * Z1 * Z2) + (0.25 * Y3) + (0.13 * Y0 * X2 * Z3)") + +# Specify the quantum circuit expectation value +expectation = cutn.create_expectation(handle, quantum_state, hamiltonian) + +num_hyper_samples_dtype = cutn.expectation_get_attribute_dtype(cutn.ExpectationAttribute.OPT_NUM_HYPER_SAMPLES) +num_hyper_samples = np.asarray(8, dtype=num_hyper_samples_dtype) +cutn.expectation_configure(handle, expectation, + cutn.ExpectationAttribute.OPT_NUM_HYPER_SAMPLES, + num_hyper_samples.ctypes.data, num_hyper_samples.dtype.itemsize) + +# Prepare the computation of the specified quantum circuit expectation value +cutn.expectation_prepare(handle, expectation, scratch_size, work_desc, stream.ptr) +print("Prepare the computation of the specified quantum circuit expectation value") + +workspace_size_d = cutn.workspace_get_memory_size(handle, + work_desc, cutn.WorksizePref.RECOMMENDED, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH) + +if workspace_size_d <= scratch_size: + cutn.workspace_set_memory(handle, work_desc, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH, scratch_space.ptr, workspace_size_d) +else: + print("Error:Insufficient workspace size on Device") + cutn.destroy_workspace_descriptor(work_desc) + cutn.destroy_expectation(expectation) + cutn.destroy_network_operator(hamiltonian) + cutn.destroy_state(quantum_state) + cutn.destroy(handle) + del scratch + print("Free resource and exit.") + exit() +print("Set the workspace buffer") + +# Compute the specified quantum circuit expectation value +expectation_value = np.empty(1, dtype='complex128') +state_norm = np.empty(1, dtype='complex128') +cutn.expectation_compute(handle, expectation, + work_desc, expectation_value.ctypes.data, state_norm.ctypes.data, stream.ptr) +stream.synchronize() +print("Computed the specified quantum circuit state expectation value") + +print(f"expectation value = {expectation_value.item()}") +print(f"norm of the state = {state_norm.item()}") + +cutn.destroy_workspace_descriptor(work_desc) +cutn.destroy_expectation(expectation) +cutn.destroy_network_operator(hamiltonian) +cutn.destroy_state(quantum_state) +cutn.destroy(handle) +del scratch_space +print("Free resource and exit.") \ No newline at end of file diff --git a/python/samples/cutensornet/high_level/mps_marginal_example.py b/python/samples/cutensornet/high_level/mps_marginal_example.py new file mode 100755 index 0000000..7b53375 --- /dev/null +++ b/python/samples/cutensornet/high_level/mps_marginal_example.py @@ -0,0 +1,182 @@ +# Copyright (c) 2023, NVIDIA CORPORATION & AFFILIATES +# +# SPDX-License-Identifier: BSD-3-Clause + +import cupy as cp +import numpy as np + +import cuquantum +from cuquantum import cutensornet as cutn + + +print("cuTensorNet-vers:", cutn.get_version()) +dev = cp.cuda.Device() # get current device +props = cp.cuda.runtime.getDeviceProperties(dev.id) +print("===== device info ======") +print("GPU-name:", props["name"].decode()) +print("GPU-clock:", props["clockRate"]) +print("GPU-memoryClock:", props["memoryClockRate"]) +print("GPU-nSM:", props["multiProcessorCount"]) +print("GPU-major:", props["major"]) +print("GPU-minor:", props["minor"]) +print("========================") + +################################################# +# Marginal computation of a quantum circuit state +################################################# + +# Quantum state configuration +num_qubits = 16 +dim = 2 +qubits_dims = (dim, ) * num_qubits # qubit size +marginal_modes = (0, 1) # open qubits +num_marginal_modes = len(marginal_modes) +print(f"Quantum circuit with {num_qubits} qubits") + +############# +# cuTensorNet +############# + +handle = cutn.create() +stream = cp.cuda.Stream() +data_type = cuquantum.cudaDataType.CUDA_C_64F + +# Define quantum gate tensors on device +gate_h = 2**-0.5 * cp.asarray([[1,1], [1,-1]], dtype='complex128', order='F') +gate_h_strides = 0 + +gate_cx = cp.asarray([[1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 0, 1], + [0, 0, 1, 0]], dtype='complex128').reshape(2,2,2,2, order='F') +gate_cx_strides = 0 + +# Allocate device memory for the final MPS state +max_extent = 2 +mps_tensor_extents = [] +mps_tensor_strides = [] +mps_tensors = [] +mps_tensor_ptrs = [] +for i in range(num_qubits): + if i == 0: + extents = (2, max_extent) + elif i == num_qubits - 1: + extents = (max_extent, 2) + else: + extents = (max_extent, 2, max_extent) + mps_tensor_extents.append(extents) + tensor = cp.zeros(extents, dtype='complex128') + mps_tensors.append(tensor) + mps_tensor_ptrs.append(tensor.data.ptr) + mps_tensor_strides.append([stride_in_bytes // tensor.itemsize for stride_in_bytes in tensor.strides]) + +# Allocate device memory for the reduced density matrix (marginal) +rdm_shape = (dim, ) * 2 * len(marginal_modes) +rdm = cp.empty(rdm_shape, dtype='complex128') +rdm_strides = [stride_in_bytes // rdm.itemsize for stride_in_bytes in rdm.strides] + +free_mem = dev.mem_info[0] +# use half of the totol free size +scratch_size = free_mem // 2 +scratch_space = cp.cuda.alloc(scratch_size) +print(f"Allocated {scratch_size} bytes of scratch memory on GPU") + +# Create the vacuum quantum state +quantum_state = cutn.create_state(handle, cutn.StatePurity.PURE, num_qubits, qubits_dims, data_type) +print("Created the initial quantum state") + +# Construct the quantum circuit state with gate application +tensor_id = cutn.state_apply_tensor( + handle, quantum_state, 1, (0, ), + gate_h.data.ptr, gate_h_strides, 1, 0, 1) + +for i in range(1, num_qubits): + tensor_id = cutn.state_apply_tensor( + handle, quantum_state, 2, (i-1, i), # target on i-1 while control on i + gate_cx.data.ptr, gate_cx_strides, 1, 0, 1) +print("Quantum gates applied") + +# Specify the target MPS state +cutn.state_finalize_mps(handle, quantum_state, cutn.BoundaryCondition.OPEN, mps_tensor_extents, mps_tensor_strides) +print("Set the final MPS representation") + +# Configure the MPS computation +svd_algorithm_dtype = cutn.state_get_attribute_dtype(cutn.StateAttribute.MPS_SVD_CONFIG_ALGO) +svd_algorithm = np.array(cutn.TensorSVDAlgo.GESVDJ, dtype=svd_algorithm_dtype) +cutn.state_configure(handle, quantum_state, + cutn.StateAttribute.MPS_SVD_CONFIG_ALGO, svd_algorithm.ctypes.data, svd_algorithm.dtype.itemsize) + +# Prepare the specified quantum circuit for MPS computation +work_desc = cutn.create_workspace_descriptor(handle) +cutn.state_prepare(handle, quantum_state, scratch_size, work_desc, stream.ptr) +print("Prepared the specified quantum circuit for MPS computation") + +workspace_size_d = cutn.workspace_get_memory_size(handle, + work_desc, cutn.WorksizePref.RECOMMENDED, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH) +if workspace_size_d <= scratch_size: + cutn.workspace_set_memory(handle, work_desc, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH, scratch_space.ptr, workspace_size_d) +else: + print("Error:Insufficient workspace size on Device") + cutn.destroy_workspace_descriptor(work_desc) + cutn.destroy_marginal(marginal) + cutn.destroy_state(quantum_state) + cutn.destroy(handle) + del scratch + print("Free resource and exit.") + exit() +print("Set the workspace buffer for MPS computation") + +# Compute the final MPS state +extents_out, strides_out = cutn.state_compute(handle, quantum_state, work_desc, mps_tensor_ptrs, stream.ptr) + +# If a lower extent is found during runtime, the cupy.ndarray container must be adjusted to reflect the lower extent +for i, (extent_in, extent_out) in enumerate(zip(mps_tensor_extents, extents_out)): + if extent_in != tuple(extent_out): + stride_out = [s * mps_tensors[0].itemsize for s in strides_out[i]] + mps_tensors[i] = cp.ndarray(extent_out, dtype=mps_tensors[i].dtype, memptr=mps_tensors[i].data, strides=stride_out) +print("Computed the final MPS representation") + +# Specify the desired reduced density matrix (marginal) +marginal = cutn.create_marginal(handle, quantum_state, num_marginal_modes, marginal_modes, 0, 0, rdm_strides) + +# Configure the computation of the desired reduced density matrix (marginal) +num_hyper_samples_dtype = cutn.marginal_get_attribute_dtype(cutn.MarginalAttribute.OPT_NUM_HYPER_SAMPLES) +num_hyper_samples = np.asarray(8, dtype=num_hyper_samples_dtype) +cutn.marginal_configure(handle, marginal, + cutn.MarginalAttribute.OPT_NUM_HYPER_SAMPLES, + num_hyper_samples.ctypes.data, num_hyper_samples.dtype.itemsize) + +# Prepare the specified quantum circuit reduced densitry matrix (marginal) +cutn.marginal_prepare(handle, marginal, scratch_size, work_desc, stream.ptr) +print("Prepared the specified quantum circuit reduced density matrix (marginal)") + +workspace_size_d = cutn.workspace_get_memory_size(handle, + work_desc, cutn.WorksizePref.RECOMMENDED, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH) + +if workspace_size_d <= scratch_size: + cutn.workspace_set_memory(handle, work_desc, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH, scratch_space.ptr, workspace_size_d) +else: + print("Error:Insufficient workspace size on Device") + cutn.destroy_workspace_descriptor(work_desc) + cutn.destroy_marginal(marginal) + cutn.destroy_state(quantum_state) + cutn.destroy(handle) + del scratch + print("Free resource and exit.") + exit() +print("Set the workspace buffer for marginal computation") + +# Compute the specified quantum circuit reduced density matrix (marginal) +cutn.marginal_compute(handle, marginal, 0, work_desc, rdm.data.ptr, stream.ptr) +stream.synchronize() +print("Computed the specified quantum circuit reduced density matrix (marginal)") + +print(f"Reduced density matrix for {num_marginal_modes} qubits") +print(rdm.reshape(dim**num_marginal_modes, dim**num_marginal_modes)) + +cutn.destroy_workspace_descriptor(work_desc) +cutn.destroy_marginal(marginal) +cutn.destroy_state(quantum_state) +cutn.destroy(handle) +del scratch_space +print("Free resource and exit.") \ No newline at end of file diff --git a/python/samples/cutensornet/high_level/mps_sampling_example.py b/python/samples/cutensornet/high_level/mps_sampling_example.py new file mode 100755 index 0000000..67f877b --- /dev/null +++ b/python/samples/cutensornet/high_level/mps_sampling_example.py @@ -0,0 +1,178 @@ +# Copyright (c) 2023, NVIDIA CORPORATION & AFFILIATES +# +# SPDX-License-Identifier: BSD-3-Clause + +import cupy as cp +import numpy as np + +import cuquantum +from cuquantum import cutensornet as cutn + + +print("cuTensorNet-vers:", cutn.get_version()) +dev = cp.cuda.Device() # get current device +props = cp.cuda.runtime.getDeviceProperties(dev.id) +print("===== device info ======") +print("GPU-name:", props["name"].decode()) +print("GPU-clock:", props["clockRate"]) +print("GPU-memoryClock:", props["memoryClockRate"]) +print("GPU-nSM:", props["multiProcessorCount"]) +print("GPU-major:", props["major"]) +print("GPU-minor:", props["minor"]) +print("========================") + +##################################### +# Sampling of a quantum circuit state +##################################### + +# Quantum state configuration +num_samples = 100 +num_qubits = 16 +dim = 2 +qubits_dims = (dim, ) * num_qubits # qubit size +print(f"Quantum circuit with {num_qubits} qubits") + +############# +# cuTensorNet +############# + +handle = cutn.create() +stream = cp.cuda.Stream() +data_type = cuquantum.cudaDataType.CUDA_C_64F + +# Define quantum gate tensors in host memory +gate_h = 2**-0.5 * cp.asarray([[1,1], [1,-1]], dtype='complex128', order='F') +gate_h_strides = 0 + +gate_cx = cp.asarray([[1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 0, 1], + [0, 0, 1, 0]], dtype='complex128').reshape(2,2,2,2, order='F') +gate_cx_strides = 0 + +# Allocate device memory for the final MPS state +max_extent = 2 +mps_tensor_extents = [] +mps_tensor_strides = [] +mps_tensors = [] +mps_tensor_ptrs = [] +for i in range(num_qubits): + if i == 0: + extents = (2, max_extent) + elif i == num_qubits - 1: + extents = (max_extent, 2) + else: + extents = (max_extent, 2, max_extent) + mps_tensor_extents.append(extents) + tensor = cp.zeros(extents, dtype='complex128') + mps_tensors.append(tensor) + mps_tensor_ptrs.append(tensor.data.ptr) + mps_tensor_strides.append([stride_in_bytes // tensor.itemsize for stride_in_bytes in tensor.strides]) + +# Allocate device memory for the samples +samples = np.empty((num_qubits, num_samples), dtype='int64', order='F') # samples are stored in F order with shape (num_qubits, num_qubits) + +free_mem = dev.mem_info[0] +# use half of the totol free size +scratch_size = free_mem // 2 +scratch_space = cp.cuda.alloc(scratch_size) +print(f"Allocated {scratch_size} bytes of scratch memory on GPU") + +# Create the vacuum quantum state +quantum_state = cutn.create_state(handle, cutn.StatePurity.PURE, num_qubits, qubits_dims, data_type) +print("Created the initial quantum state") + +# Construct the quantum circuit state with gate application +tensor_id = cutn.state_apply_tensor( + handle, quantum_state, 1, (0, ), + gate_h.data.ptr, gate_h_strides, 1, 0, 1) + +for i in range(1, num_qubits): + tensor_id = cutn.state_apply_tensor( + handle, quantum_state, 2, (i-1, i), # target on i-1 while control on i + gate_cx.data.ptr, gate_cx_strides, 1, 0, 1) +print("Quantum gates applied") + +# Specify the target MPS state +cutn.state_finalize_mps(handle, quantum_state, cutn.BoundaryCondition.OPEN, mps_tensor_extents, mps_tensor_strides) +print("Set the final MPS representation") + +# Configure the MPS computation +svd_algorithm_dtype = cutn.state_get_attribute_dtype(cutn.StateAttribute.MPS_SVD_CONFIG_ALGO) +svd_algorithm = np.array(cutn.TensorSVDAlgo.GESVDJ, dtype=svd_algorithm_dtype) +cutn.state_configure(handle, quantum_state, + cutn.StateAttribute.MPS_SVD_CONFIG_ALGO, svd_algorithm.ctypes.data, svd_algorithm.dtype.itemsize) + +# Prepare the specified quantum circuit for MPS computation +work_desc = cutn.create_workspace_descriptor(handle) +cutn.state_prepare(handle, quantum_state, scratch_size, work_desc, stream.ptr) +print("Prepared the specified quantum circuit for MPS computation") + +workspace_size_d = cutn.workspace_get_memory_size(handle, + work_desc, cutn.WorksizePref.RECOMMENDED, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH) +if workspace_size_d <= scratch_size: + cutn.workspace_set_memory(handle, work_desc, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH, scratch_space.ptr, workspace_size_d) +else: + print("Error:Insufficient workspace size on Device") + cutn.destroy_workspace_descriptor(work_desc) + cutn.destroy_marginal(marginal) + cutn.destroy_state(quantum_state) + cutn.destroy(handle) + del scratch + print("Free resource and exit.") + exit() +print("Set the workspace buffer for MPS computation") + +# Compute the final MPS state +extents_out, strides_out = cutn.state_compute(handle, quantum_state, work_desc, mps_tensor_ptrs, stream.ptr) + +# If a lower extent is found during runtime, the cupy.ndarray container must be adjusted to reflect the lower extent +for i, (extent_in, extent_out) in enumerate(zip(mps_tensor_extents, extents_out)): + if extent_in != tuple(extent_out): + stride_out = [s * mps_tensors[0].itemsize for s in strides_out[i]] + mps_tensors[i] = cp.ndarray(extent_out, dtype=mps_tensors[i].dtype, memptr=mps_tensors[i].data, strides=stride_out) +print("Computed the final MPS representation") + +# Create the quantum circuit sampler +sampler = cutn.create_sampler(handle, quantum_state, num_qubits, 0) + +# Configure the quantum circuit sampler +num_hyper_samples_dtype = cutn.sampler_get_attribute_dtype(cutn.SamplerAttribute.OPT_NUM_HYPER_SAMPLES) +num_hyper_samples = np.asarray(8, dtype=num_hyper_samples_dtype) +cutn.sampler_configure(handle, sampler, + cutn.SamplerAttribute.OPT_NUM_HYPER_SAMPLES, + num_hyper_samples.ctypes.data, num_hyper_samples.dtype.itemsize) + +# Prepare the quantum circuit sampler +cutn.sampler_prepare(handle, sampler, scratch_size, work_desc, stream.ptr) +print("Prepared the specified quantum circuit state sampler") + +workspace_size_d = cutn.workspace_get_memory_size(handle, + work_desc, cutn.WorksizePref.RECOMMENDED, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH) + +if workspace_size_d <= scratch_size: + cutn.workspace_set_memory(handle, work_desc, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH, scratch_space.ptr, workspace_size_d) +else: + print("Error:Insufficient workspace size on Device") + cutn.destroy_workspace_descriptor(work_desc) + cutn.destroy_sampler(sampler) + cutn.destroy_state(quantum_state) + cutn.destroy(handle) + del scratch + print("Free resource and exit.") + exit() +print("Set the workspace buffer for sampling") + +# Sample the quantum circuit state +cutn.sampler_sample(handle, sampler, num_samples, work_desc, samples.ctypes.data, stream.ptr) +stream.synchronize() +print("Performed quantum circuit state sampling") +print("Bit-string samples:") +print(samples.T) + +cutn.destroy_workspace_descriptor(work_desc) +cutn.destroy_sampler(sampler) +cutn.destroy_state(quantum_state) +cutn.destroy(handle) +del scratch_space +print("Free resource and exit.") diff --git a/python/samples/cutensornet/high_level/sampling_example.py b/python/samples/cutensornet/high_level/sampling_example.py index 948d182..e14ff8e 100755 --- a/python/samples/cutensornet/high_level/sampling_example.py +++ b/python/samples/cutensornet/high_level/sampling_example.py @@ -52,6 +52,13 @@ # Allocate device memory for the samples samples = np.empty((num_qubits, num_samples), dtype='int64', order='F') # samples are stored in F order with shape (num_qubits, num_qubits) + +free_mem = dev.mem_info[0] +# use half of the totol free size +scratch_size = free_mem // 2 +scratch_space = cp.cuda.alloc(scratch_size) +print(f"Allocated {scratch_size} bytes of scratch memory on GPU") + # Create the initial quantum state quantum_state = cutn.create_state(handle, cutn.StatePurity.PURE, num_qubits, qubits_dims, data_type) print("Created the initial quantum state") @@ -71,12 +78,7 @@ # Create the quantum circuit sampler sampler = cutn.create_sampler(handle, quantum_state, num_qubits, 0) -free_mem = dev.mem_info[0] -# use half of the totol free size -scratch_size = free_mem // 2 -scratch_space = cp.cuda.alloc(scratch_size) -print(f"Allocated {scratch_size} bytes of scratch memory on GPU") - +# Configure the quantum circuit sampler num_hyper_samples_dtype = cutn.sampler_get_attribute_dtype(cutn.SamplerAttribute.OPT_NUM_HYPER_SAMPLES) num_hyper_samples = np.asarray(8, dtype=num_hyper_samples_dtype) cutn.sampler_configure(handle, sampler, diff --git a/python/samples/cutensornet/tensor/example11-svd_algorithms.py b/python/samples/cutensornet/tensor/example11-svd_algorithms.py index 05ffdea..6df39bd 100644 --- a/python/samples/cutensornet/tensor/example11-svd_algorithms.py +++ b/python/samples/cutensornet/tensor/example11-svd_algorithms.py @@ -3,16 +3,15 @@ # SPDX-License-Identifier: BSD-3-Clause """ -truncated SVD Example using NumPy ndarray with various SVD algorithms. +truncated SVD Example using CuPy ndarray with various SVD algorithms. -The decomposition results are also NumPy ndarrays. +The decomposition results are also CuPy ndarrays. """ -import numpy as np +import cupy as cp from cuquantum import tensor - -a = np.ones((3,2,4,5)) +a = cp.ones((3,2,4,5)) base_options = {'max_extent': 4, 'abs_cutoff': 0.1, diff --git a/python/setup.py b/python/setup.py index 4e59fa7..4b71dc5 100644 --- a/python/setup.py +++ b/python/setup.py @@ -29,10 +29,10 @@ # - cuTENSOR version is constrained in the cutensornet-cuXX package, so we don't # need to list it install_requires = [ - 'numpy>=1.21', + 'numpy~=1.21', # ">=1.21,<2" # 'torch', # <-- PyTorch is optional; also, the PyPI version does not support GPU... - f'custatevec-cu{utils.cuda_major_ver}~=1.4', # ">=1.4.0,<2" - f'cutensornet-cu{utils.cuda_major_ver}~=2.2', # ">=2.2.0,<3" + f'custatevec-cu{utils.cuda_major_ver}~=1.5', # ">=1.5.0,<2" + f'cutensornet-cu{utils.cuda_major_ver}~=2.3', # ">=2.3.0,<3" ] if utils.cuda_major_ver == '11': # CuPy has 3+ wheels for CUDA 11.x, only the cuquantum-python meta package has diff --git a/python/tests/conftest.py b/python/tests/conftest.py index 4c03835..61bcef6 100644 --- a/python/tests/conftest.py +++ b/python/tests/conftest.py @@ -6,6 +6,8 @@ # various reasons, see pytest-dev/pytest#3730. In particular, this strategy # is borrowed from https://github.com/pytest-dev/pytest/issues/3730#issuecomment-567142496. +from collections.abc import Iterable + def pytest_configure(config): config.addinivalue_line( @@ -17,13 +19,20 @@ def pytest_collection_modifyitems(config, items): removed = [] kept = [] for item in items: + is_removed = False m = item.get_closest_marker('uncollect_if') if m: - func = m.kwargs['func'] - if func(**item.callspec.params): - removed.append(item) - continue - kept.append(item) + funcs = m.kwargs['func'] + if not isinstance(funcs, Iterable): + funcs = (funcs,) + # loops over all deselect requirements + for func in funcs: + if func(**item.callspec.params): + removed.append(item) + is_removed = True + break + if not is_removed: + kept.append(item) if removed: config.hook.pytest_deselected(items=removed) items[:] = kept diff --git a/python/tests/cuquantum_tests/custatevec_tests/test_custatevec.py b/python/tests/cuquantum_tests/custatevec_tests/test_custatevec.py index 364d3c3..946fb7e 100644 --- a/python/tests/cuquantum_tests/custatevec_tests/test_custatevec.py +++ b/python/tests/cuquantum_tests/custatevec_tests/test_custatevec.py @@ -6,6 +6,7 @@ import cupy as cp from cupy import testing +import cupyx as cpx import numpy as np try: from mpi4py import MPI # init! @@ -204,18 +205,24 @@ class TestLibHelper: def test_get_version(self): ver = cusv.get_version() - assert ver == (cusv.MAJOR_VER * 1000 + major = ver // 1000 + minor = (ver % 1000) // 100 + + # run-time version must be compatible with build-time version + assert major == cusv.MAJOR_VER + assert minor >= cusv.MINOR_VER + + # sanity check (build-time versions should agree) + assert cusv.VERSION == (cusv.MAJOR_VER * 1000 + cusv.MINOR_VER * 100 + cusv.PATCH_VER) - assert ver == cusv.VERSION def test_get_property(self): + # run-time version must be compatible with build-time version assert cusv.MAJOR_VER == cusv.get_property( cuquantum.libraryPropertyType.MAJOR_VERSION) - assert cusv.MINOR_VER == cusv.get_property( + assert cusv.MINOR_VER <= cusv.get_property( cuquantum.libraryPropertyType.MINOR_VERSION) - assert cusv.PATCH_VER == cusv.get_property( - cuquantum.libraryPropertyType.PATCH_LEVEL) class TestHandle: @@ -1790,6 +1797,94 @@ def test_scheduler(self, handle, scheduler_args, input_form, param_form): pass +class TestSubSVMigrator: + ''' This class runs random tests to check all API arguemnts + are correctly passed to C-API + ''' + @classmethod + def setup_class(cls): + np.random.seed(20231003) + + @pytest.mark.parametrize( + 'dtype', (np.complex64, np.complex128) + ) + @pytest.mark.parametrize( + 'exec_num', range(5) + ) + def test_sub_sv_migrator(self, handle, dtype, exec_num): + n_local_index_bits = np.random.randint(low=1, high=22) + n_device_slots = np.random.randint(low=2, high=16) + device_slot_idx = np.random.randint(n_device_slots) + check_in_out = np.random.randint(4) + randnum_dtype = np.float32 if dtype == np.complex64 else np.float64 + + data_type = dtype_to_data_type[dtype] + sub_sv_size = 2 ** n_local_index_bits + device_slot_size = sub_sv_size * n_device_slots + + randnums = np.random.rand(device_slot_size) + 1.j * np.random.rand(device_slot_size) + host_slots_ref = np.asarray(randnums, dtype=dtype) + device_slots = cp.array(host_slots_ref) + begin = np.random.randint(low=0, high=sub_sv_size-1) + end = np.random.randint(low=begin + 1, high=sub_sv_size) + + src_sub_sv_ptr = 0 + dst_sub_sv_ptr = 0 + if check_in_out == 0: + # swap + check_in = check_out = True + randnums = np.random.rand(sub_sv_size) + 1.j * np.random.rand(sub_sv_size) + src_sub_sv_ref = np.asarray(randnums, dtype=dtype) + src_sub_sv = cpx.empty_pinned(sub_sv_size, dtype=dtype) + src_sub_sv[:] = src_sub_sv_ref[:] + # src and dst are the same memory chunk + dst_sub_sv = src_sub_sv + dst_sub_sv_ref = src_sub_sv_ref + src_sub_sv_ptr = dst_sub_sv_ptr = src_sub_sv.ctypes.data + else: + # check-in / check-out + check_in = (check_in_out & 1) != 0 + check_out = (check_in_out & 2) != 0 + if check_out: + randnums = np.random.rand(sub_sv_size) + 1.j * np.random.rand(sub_sv_size) + src_sub_sv_ref = np.asarray(randnums, dtype=dtype) + src_sub_sv = cpx.empty_pinned(sub_sv_size, dtype=dtype) + src_sub_sv[:] = src_sub_sv_ref[:] + src_sub_sv_ptr = src_sub_sv.ctypes.data + if check_in: + randnums = np.random.rand(sub_sv_size) + 1.j * np.random.rand(sub_sv_size) + dst_sub_sv_ref = np.asarray(randnums, dtype=dtype) + dst_sub_sv = cpx.empty_pinned(sub_sv_size, dtype=dtype) + dst_sub_sv[:] = dst_sub_sv_ref[:] + dst_sub_sv_ptr = dst_sub_sv.ctypes.data + + # create SubStateVectorMigrator + migrator = cusv.sub_sv_migrator_create( + handle, device_slots.data.ptr, data_type, n_device_slots, n_local_index_bits) + # migrate + cusv.sub_sv_migrator_migrate( + handle, migrator, device_slot_idx, src_sub_sv_ptr, dst_sub_sv_ptr, begin, end) + # destroy + cp.cuda.Stream().synchronize() + cusv.sub_sv_migrator_destroy(handle, migrator) + + # reference + offset = sub_sv_size * device_slot_idx + if check_in: + # copy values for swap + tmp = host_slots_ref[offset+begin:offset+end].copy() + if check_out: + host_slots_ref[offset+begin:offset+end] = src_sub_sv_ref[begin:end] + if check_in: + dst_sub_sv_ref[begin:end] = tmp[:] + + assert cp.all(cp.asarray(host_slots_ref) == device_slots) + if check_out: + assert np.all(src_sub_sv == src_sub_sv_ref) + if check_in: + assert np.all(dst_sub_sv == dst_sub_sv_ref) + + class TestMemHandler(MemHandlerTestBase): mod = cusv diff --git a/python/tests/cuquantum_tests/cutensornet_tests/approxTN_utils.py b/python/tests/cuquantum_tests/cutensornet_tests/approxTN_utils.py index 605c4a1..7a7dcde 100644 --- a/python/tests/cuquantum_tests/cutensornet_tests/approxTN_utils.py +++ b/python/tests/cuquantum_tests/cutensornet_tests/approxTN_utils.py @@ -155,8 +155,17 @@ def is_exact_split(**split_options): max_extent = split_options.get("max_extent", 0) abs_cutoff = split_options.get("abs_cutoff", 0) rel_cutoff = split_options.get("rel_cutoff", 0) + discarded_weight_cutoff = split_options.get("discarded_weight_cutoff", 0) normalization = split_options.get("normalization", None) - return (max_extent == 0 or max_extent is None) and abs_cutoff == 0 and rel_cutoff == 0 and normalization is None + return (max_extent == 0 or max_extent is None) and \ + abs_cutoff == 0 and rel_cutoff == 0 and \ + discarded_weight_cutoff == 0 and normalization is None + +def is_dw_truncation_only(**split_options): + max_extent = split_options.get("max_extent", 0) + abs_cutoff = split_options.get("abs_cutoff", 0) + rel_cutoff = split_options.get("rel_cutoff", 0) + return (max_extent == 0 or max_extent is None) and abs_cutoff == 0 and rel_cutoff == 0 def split_contract_decompose(subscripts): @@ -170,8 +179,8 @@ def split_contract_decompose(subscripts): decompose_subscripts = f"{intm_modes}->{outputs}" return contract_subscripts, decompose_subscripts -#NOTE: torch does not have native support on F order -# We here get around this by converting to CuPy/NumPy ndarrays as a get arounds +# NOTE: torch does not have native support on F order +# We here get around this by converting to CuPy/NumPy ndarrays as a workaround # the overhead for torch tensors on GPU should be minimal as torch tensors support __cuda_array_interface__ def torch_support_wrapper(func): def new_func(T, *args, **kwargs): @@ -195,6 +204,7 @@ def get_einsum_kwargs(backend): #################################### ############ Execution ############# #################################### + @torch_support_wrapper def tensor_permute(T, input_modes, output_modes): axes = [input_modes.index(i) for i in output_modes] @@ -216,6 +226,7 @@ def matrix_svd( max_extent=0, abs_cutoff=0, rel_cutoff=0, + discarded_weight_cutoff=0, partition=None, normalization=None, return_info=True, @@ -234,6 +245,14 @@ def matrix_svd( if max_extent == 0 or max_extent is None: max_extent = len(s) reduced_extent = min(max_extent, int((s>cutoff).sum())) + if discarded_weight_cutoff != 0: + s_square_sum = backend.cumsum(s**2, 0) + if backend not in (cp, np): # torch + s_square_sum /= s_square_sum[-1].clone() + else: + s_square_sum /= s_square_sum[-1] + dw_reduced_extent = int((s_square_sum<(1-discarded_weight_cutoff)).sum()) + 1 + reduced_extent = min(reduced_extent, dw_reduced_extent) reduced_extent = max(reduced_extent, 1) info["reduced_extent"] = reduced_extent if reduced_extent != len(s): @@ -533,14 +552,14 @@ def verify_split_SVD( algorithm = 'gesvd' if algorithm == 'gesvdj': if dtype_name in ['float64', 'complex128']: - rtol = 1e-8 + rtol = 1e-6 if 'gesvdj_residual' not in info: logging.warning("gesvdj_residual not recorded in info; verification may fail due to unknown runtime status") else: - rtol = max(rtol, info['gesvdj_residual']) + rtol = max(rtol, info['gesvdj_residual'] * max_mid_extent) elif algorithm == 'gesvdp': if dtype_name in ['float64', 'complex128']: - rtol = 1e-8 + rtol = 1e-6 if 'gesvdp_err_sigma' not in info: logging.warning("gesvdp_err_sigma not recorded in info; verification may fail due to unknown runtime status") elif info['gesvdp_err_sigma'] > 1e-4: @@ -588,6 +607,18 @@ def verify_split_SVD( # For gesvdr, discarded weight is only computed when fix extent truncation is not enabled if info['algorithm'] != 'gesvdr' or max_extent == max_mid_extent: info_equal = info_equal and (abs(info["discarded_weight"]-info_ref["discarded_weight"]) < rtol) + if is_dw_truncation_only(**split_options) and max_extent == max_mid_extent: + # when only dw is in use, verify that discarded weight is less than the cutoff + dw_cutn = info["discarded_weight"] + dw_ref = info_ref["discarded_weight"] + dw_cutoff = split_options.get('discarded_weight_cutoff', 0) + if dw_cutn > dw_cutoff: + logging.error("cutensornet SVD runtime discarded weight {dw_cutn} larger than cutoff {dw_cutoff}") + return False + if dw_ref > dw_cutoff: + logging.error("reference SVD runtime discarded weight {dw_ref} larger than cutoff {dw_cutoff}") + return False + if not info_equal: info_details = "".join([f"{key}:({info.get(key)}, {info_ref.get(key)}); " for key in info.keys()]) logging.error(f"SVD Info not matching the reference: {info_details}") diff --git a/python/tests/cuquantum_tests/cutensornet_tests/circuit_utils.py b/python/tests/cuquantum_tests/cutensornet_tests/circuit_utils.py index 69f1fa1..b675d15 100644 --- a/python/tests/cuquantum_tests/cutensornet_tests/circuit_utils.py +++ b/python/tests/cuquantum_tests/cutensornet_tests/circuit_utils.py @@ -2,17 +2,15 @@ # # SPDX-License-Identifier: BSD-3-Clause -from collections import Counter -import itertools from types import MappingProxyType try: import cirq + from cuquantum.cutensornet._internal import circuit_parser_utils_cirq except ImportError: - cirq = None + cirq = circuit_parser_utils_cirq = None import cupy as cp import numpy as np -import pytest try: import torch if not torch.cuda.is_available(): @@ -21,8 +19,9 @@ torch = None try: import qiskit + from cuquantum.cutensornet._internal import circuit_parser_utils_qiskit except ImportError: - qiskit = None + qiskit = circuit_parser_utils_qiskit = None from cuquantum import contract, CircuitToEinsum from cuquantum import cutensornet as cutn @@ -31,10 +30,19 @@ from cuquantum.cutensornet._internal.circuit_converter_utils import EINSUM_SYMBOLS_BASE from cuquantum.cutensornet._internal.circuit_converter_utils import get_pauli_gates from cuquantum.cutensornet._internal.circuit_converter_utils import parse_gates_to_mode_labels_operands +from cuquantum.cutensornet._internal.decomposition_utils import SVD_ALGORITHM_MAP, NORMALIZATION_MAP from cuquantum.cutensornet._internal.utils import infer_object_package -from .test_utils import atol_mapper, get_stream_for_backend, rtol_mapper +from .approxTN_utils import SVD_TOLERANCE, verify_unitary +from .mps_utils import MPS, gen_random_mps, get_mps_tolerance +from .mps_utils import amplitude_from_sv +from .mps_utils import batched_amplitude_from_sv +from .mps_utils import expectation_from_sv +from .mps_utils import reduced_density_matrix_from_sv +from .mps_utils import sample_from_sv +from .test_utils import atol_mapper, rtol_mapper from .test_cutensornet import manage_resource +from .. import dtype_to_data_type # note: this implementation would cause pytorch tests being silently skipped @@ -49,14 +57,19 @@ qiskit_circuits = [] EMPTY_DICT = MappingProxyType(dict()) +GLOBAL_RNG = np.random.default_rng(2023) +DEFAULT_NUM_RANDOM_LAYERS = 2 +EXACT_MPS_QUBIT_COUNT_LIMIT = 63 # limit the number of qubits for exact MPS to avoid extent overflowing - -def gen_qubits_map(qubits): - n_qubits = len(qubits) - if n_qubits > len(EINSUM_SYMBOLS_BASE): - raise NotImplementedError(f'test suite only supports up to {len(EINSUM_SYMBOLS_BASE)} qubits') - qubits_map = dict(zip(qubits, EINSUM_SYMBOLS_BASE[:n_qubits])) - return qubits_map +STATE_ATTRIBUTE_MAP = { + 'canonical_center' : cutn.StateAttribute.MPS_CANONICAL_CENTER, + 'abs_cutoff' : cutn.StateAttribute.MPS_SVD_CONFIG_ABS_CUTOFF, + 'rel_cutoff' : cutn.StateAttribute.MPS_SVD_CONFIG_REL_CUTOFF, + 'normalization' : cutn.StateAttribute.MPS_SVD_CONFIG_S_NORMALIZATION, + 'discarded_weight_cutoff' : cutn.StateAttribute.MPS_SVD_CONFIG_DISCARDED_WEIGHT_CUTOFF, + 'algorithm' : cutn.StateAttribute.MPS_SVD_CONFIG_ALGO, + #'algorithm_params' : cutn.StateAttribute.MPS_SVD_CONFIG_ALGO_PARAMS, # NOTE: special treatment required + 'num_hyper_samples' : cutn.StateAttribute.NUM_HYPER_SAMPLES} def bitstring_generator(n_qubits, nsample=1): @@ -86,20 +99,58 @@ def random_pauli_string_generator(n_qubits, num_strings=4): yield ''.join(np.random.choice(['I','X', 'Y', 'Z'], n_qubits)) -def get_partial_indices(qubits, fixed): - partial_indices = [slice(None)] * len(qubits) - index_map = {'0': slice(0, 1), - '1': slice(1, 2)} - for ix, q in enumerate(qubits): - if q in fixed: - partial_indices[ix] = index_map[fixed[q]] - return partial_indices - - ################################################ # functions to generate cirq.Circuit for testing ################################################ +def get_cirq_random_2q_gate(): + class Random2QGate(cirq.Gate): + def __init__(self): + super(Random2QGate, self) + setattr(self, '_internal_array_', cirq.testing.random_unitary(4)) + + def _num_qubits_(self): + return 2 + + def _unitary_(self): + return getattr(self, '_internal_array_') + + def __pow__(self, power): + if power == 1: + return self + elif power == -1: + new_gate = Random2QGate() + unitary = getattr(self, '_internal_array_').T.conj() + setattr(new_gate, '_internal_array_', unitary) + return new_gate + else: + return NotImplementedError + + def _circuit_diagram_info_(self, args): + return "Q1", "Q2" + + return Random2QGate() + + +def gen_random_layered_cirq_circuit(qubits, num_random_layers=2): + n_qubits = len(qubits) + operations = [] + for n in range(num_random_layers): + for i in range(n%2, n_qubits-1, 2): + operations.append(get_cirq_random_2q_gate().on(qubits[i], qubits[i+1])) + return cirq.Circuit(operations) + + +def cirq_insert_random_layers(circuit, num_random_layers=DEFAULT_NUM_RANDOM_LAYERS): + if num_random_layers == 0: + return circuit + qubits = sorted(circuit.all_qubits()) + circuit = circuit_parser_utils_cirq.remove_measurements(circuit) + pre_circuit = gen_random_layered_cirq_circuit(qubits, num_random_layers=num_random_layers) + post_circuit = gen_random_layered_cirq_circuit(qubits, num_random_layers=num_random_layers) + return pre_circuit.concat_ragged(circuit, post_circuit) + + def get_cirq_qft_circuit(n_qubits): qubits = cirq.LineQubit.range(n_qubits) qreg = list(qubits)[::-1] @@ -139,11 +190,46 @@ def get_cirq_random_circuit(n_qubits, n_moments, op_density=0.9, seed=3): except: pass +cirq_circuits_mps = [cirq_insert_random_layers(circuit) for circuit in cirq_circuits] ######################################################### # functions to generate qiskit.QuantumCircuit for testing ######################################################### +def get_qiskit_unitary_gate(rng=GLOBAL_RNG, control=None): + # random unitary two qubit gate + from qiskit.extensions import UnitaryGate + m = rng.standard_normal(size=(4, 4)) + 1j*rng.standard_normal(size=(4, 4)) + q, r = np.linalg.qr(m) + d = np.diag(r) + q *= d/abs(d) + gate = UnitaryGate(q) + if control is None: + return gate + else: + return gate.control(control) + + +def gen_random_layered_qiskit_circuit(qubits, num_random_layers=DEFAULT_NUM_RANDOM_LAYERS): + n_qubits = len(qubits) + circuit = qiskit.QuantumCircuit(qubits) + for n in range(num_random_layers): + for i in range(n%2, n_qubits-1, 2): + circuit.append(get_qiskit_unitary_gate(), qubits[i:i+2]) + return circuit + + +def qiskit_insert_random_layers(circuit, num_random_layers=DEFAULT_NUM_RANDOM_LAYERS): + if num_random_layers == 0: + return circuit + qubits = circuit.qubits + circuit.remove_final_measurements() + pre_circuit = gen_random_layered_qiskit_circuit(qubits, num_random_layers=num_random_layers) + post_circuit = gen_random_layered_qiskit_circuit(qubits, num_random_layers=num_random_layers) + circuit.data = pre_circuit.data + circuit.data + post_circuit.data + return circuit + + def get_qiskit_qft_circuit(n_qubits): return qiskit.circuit.library.QFT(n_qubits, do_swaps=False).decompose() @@ -189,20 +275,6 @@ def get_qiskit_nested_circuit(): return circ -def get_cc_unitary_gate(seed=None): - # random unitary two qubit gate - from qiskit.extensions import UnitaryGate - if seed is None: - seed = 1234 - rng = np.random.default_rng(seed) - m = rng.standard_normal(size=(4, 4)) + 1j*rng.standard_normal(size=(4, 4)) - q, r = np.linalg.qr(m) - d = np.diag(r) - q *= d/abs(d) - gate = UnitaryGate(q).control(2) - return gate - - def get_qiskit_multi_control_circuit(): qubits = qiskit.QuantumRegister(5) circuit = qiskit.QuantumCircuit(qubits) @@ -211,9 +283,10 @@ def get_qiskit_multi_control_circuit(): qs = list(qubits) # 3 layers of multi-controlled qubits np.random.seed(0) + rng = np.random.default_rng(1234) for i in range(2): - np.random.shuffle(qs) - ccu_gate = get_cc_unitary_gate(i) + rng.shuffle(qs) + ccu_gate = get_qiskit_unitary_gate(rng, control=2) circuit.append(ccu_gate, qs[:4]) for q in qubits: if i % 2 == 1: @@ -250,6 +323,13 @@ def get_qiskit_multi_control_circuit(): except: pass +qiskit_circuits_mps = [qiskit_insert_random_layers(circuit) for circuit in qiskit_circuits] + +def is_converter_mps_compatible(converter): + for _, qubits in converter.gates: + if len(qubits) > 2: + return False + return True def compute_histogram_overlap(hist1, hist2, nshots): # assuming hist1 & hist2 have the same sample size (=nshots) @@ -261,142 +341,344 @@ def compute_histogram_overlap(hist1, hist2, nshots): overlap /= nshots return overlap +class _BaseComputeEngine: + + @property + def qubits(self): + raise NotImplementedError + + @property + def n_qubits(self): + return len(self.qubits) + + @property + def tolerance(self): + raise NotImplementedError + + def setup_resources(self, *args, **kwargs): + raise NotImplementedError + + def get_sv(self): + raise NotImplementedError + + def get_norm(self): + if self.norm is None: + sv = self.get_sv() + if sv is not None: + self.norm = self.backend.linalg.norm(self.get_sv()).item() ** 2 + return self.norm + + def get_amplitude(self, bitstring): + raise NotImplementedError + + def get_batched_amplitudes(self, fixed): + raise NotImplementedError + + def get_reduced_density_matrix(self, where, fixed=EMPTY_DICT): + r""" + For where = (a, b), reduced density matrix is formulated as: + :math: `rho_{a,b,a^{\prime},b^{\prime}} = \sum_{c,d,e,...} SV^{\star}_{a^{\prime}, b^{\prime}, c, d, e, ...} SV_{a, b, c, d, e, ...}` + """ + raise NotImplementedError + + def get_expectation(self, pauli_string): + raise NotImplementedError + + def get_sampling(self, qubits_to_sample=None, seed=None, nshots=5000): + raise NotImplementedError -################################################################### -# -# Simulator APIs inside cirq and qiskit may be subject to change. -# Version tests are needed. In cases where simulator API changes, -# the implementatitons to be modified are: -# `CirqTest._get_state_vector_from_simulator` and -# `QiskitTest._get_state_vector_from_simulator` -# -################################################################### -class BaseTester: - def __init__(self, circuit, dtype, backend, nsample, nsite_max, nfix_max, nshots=5000, seed=1024): +class BaseFrameworkComputeEngine(_BaseComputeEngine): + ################################################################### + # + # Reference implementation from framework providers. + # + # Simulator APIs inside cirq and qiskit may be subject to change. + # Version tests are needed. In cases where simulator API changes, + # the methods to be modified are: + # 1. `CirqComputeEngine._get_state_vector` + # 2. `CirqComputeEngine.get_sampling` + # 3. `QiskitComputeEngine._get_state_vector` + # 4. `QiskitComputeEngine.get_sampling` + # + ################################################################### + + def __init__(self, circuit, dtype, backend): self.circuit = circuit - self.converter = CircuitToEinsum(circuit, dtype=dtype, backend=backend) self.backend = backend - self.qubits = list(self.converter.qubits) - self.n_qubits = self.converter.n_qubits self.dtype = dtype self.sv = None - self.nsample = nsample - self.nsite_max = max(1, min(nsite_max, self.n_qubits-1)) - self.nfix_max = max(min(nfix_max, self.n_qubits-nsite_max-1), 0) - self.nshots = nshots - self.seed = seed - self.state_purity = cutn.StatePurity.PURE - self.state_prepared = False - - def get_state_vector_from_simulator(self): + self._tolerance = None + self.norm = None + + @property + def tolerance(self): + if self._tolerance is None: + self._tolerance = {'atol': atol_mapper[self.dtype], + 'rtol': rtol_mapper[self.dtype]} + return self._tolerance + + def setup_resources(self, *args, **kwargs): + # No additional resources needed + pass + + def _get_state_vector(self): + # implementation for different frameworks + raise NotImplementedError + + def get_sv(self): if self.sv is None: - self.sv = self._get_state_vector_from_simulator() + self.sv = self._get_state_vector() return self.sv - def get_amplitude_from_simulator(self, bitstring): - sv = self.get_state_vector_from_simulator() - index = [int(ibit) for ibit in bitstring] - return sv[tuple(index)] + def get_amplitude(self, bitstring): + return amplitude_from_sv(self.get_sv(), bitstring) - def get_batched_amplitudes_from_simulator(self, fixed): - sv = self.get_state_vector_from_simulator() - partial_indices = get_partial_indices(self.qubits, fixed) - batched_amplitudes = sv[tuple(partial_indices)] - return batched_amplitudes.reshape((2,)*(self.n_qubits-len(fixed))) + def get_batched_amplitudes(self, fixed): + fixed = dict([(self.qubits.index(q), bit) for q, bit in fixed.items()]) + return batched_amplitude_from_sv(self.get_sv(), fixed) - def get_reduced_density_matrix_from_simulator(self, where, fixed=EMPTY_DICT): - r""" - For where = (a, b), reduced density matrix is formulated as: - :math: `rho_{a,b,a^{\prime},b^{\prime}} = \sum_{c,d,e,...} SV^{\star}_{a^{\prime}, b^{\prime}, c, d, e, ...} SV_{a, b, c, d, e, ...}` - """ - sv = self.get_state_vector_from_simulator() - partial_indices = get_partial_indices(self.qubits, fixed) - sv = sv[tuple(partial_indices)] - - qubits_map = gen_qubits_map(self.qubits) - output_inds = ''.join([qubits_map[q] for q in where]) - output_inds += output_inds.upper() - left_inds = ''.join([qubits_map[q] for q in self.qubits]) - right_inds = '' - for q in self.qubits: - if q in where: - right_inds += qubits_map[q].upper() - else: - right_inds += qubits_map[q] - expression = left_inds + ',' + right_inds + '->' + output_inds + def get_reduced_density_matrix(self, where, fixed=EMPTY_DICT): + sv = self.get_sv() + where = [self.qubits.index(q) for q in where] + fixed = dict([(self.qubits.index(q), bit) for q, bit in fixed.items()]) + return reduced_density_matrix_from_sv(sv, where, fixed=fixed) + + def get_expectation(self, pauli_string): + return expectation_from_sv(self.get_sv(), pauli_string) + + def get_sampling(self, qubits_to_sample=None, seed=None, nshots=5000): + # implementation for different framework providers + raise NotImplementedError + + +class CirqComputeEngine(BaseFrameworkComputeEngine): + + @property + def qubits(self): + return sorted(self.circuit.all_qubits()) + + def _get_state_vector(self): + qubits = self.qubits + simulator = cirq.Simulator(dtype=self.dtype) + circuit = circuit_parser_utils_cirq.remove_measurements(self.circuit) + result = simulator.simulate(circuit, qubit_order=qubits) + statevector = result.state_vector().reshape((2,)*self.n_qubits) if self.backend is torch: - rdm = contract(expression, sv, sv.conj().resolve_conj()) + statevector = torch.as_tensor(statevector, dtype=getattr(torch, self.dtype), device='cuda') else: - rdm = contract(expression, sv, sv.conj()) - return rdm + statevector = self.backend.asarray(statevector, dtype=self.dtype) + return statevector - def get_expectation_from_sv(self, pauli_string): - - input_mode_labels = [[*range(self.n_qubits)]] - qubits_frontier = dict(zip(self.qubits, itertools.count())) - next_frontier = max(qubits_frontier.values()) + 1 - - pauli_map = dict(zip(self.qubits, pauli_string)) - dtype = getattr(self.backend, self.dtype) - pauli_gates = get_pauli_gates(pauli_map, dtype=dtype, backend=self.backend) - gate_mode_labels, gate_operands = parse_gates_to_mode_labels_operands(pauli_gates, - qubits_frontier, - next_frontier) - - mode_labels = input_mode_labels + gate_mode_labels + [[qubits_frontier[ix] for ix in self.qubits]] - output_mode_labels = [] - expression = convert_mode_labels_to_expression(mode_labels, output_mode_labels) - - sv = self.get_state_vector_from_simulator() + def get_sampling(self, qubits_to_sample=None, seed=None, nshots=5000): + if qubits_to_sample is None: + qubits_to_sample = self.qubits + circuit = circuit_parser_utils_cirq.remove_measurements(self.circuit) + circuit.append(cirq.measure_each(qubits_to_sample)) + circuit.append(cirq.measure(*qubits_to_sample, key='meas')) + result = cirq.sample( + circuit, repetitions=nshots, seed=seed, dtype=getattr(np, self.dtype)) + result = result.histogram(key='meas') + sampling = {} + nsamples = 0 + for bitstring, nsample in result.items(): + sampling[int(bitstring)] = nsample + nsamples += nsample + assert nsamples == nshots + return sampling + + +class QiskitComputeEngine(BaseFrameworkComputeEngine): + + @property + def qubits(self): + return list(self.circuit.qubits) + + def _get_precision(self): + precision = {'complex64': 'single', + 'complex128': 'double'}[self.dtype] + return precision + + def _get_state_vector(self): + # requires qiskit >= 0.24.0 + precision = self._get_precision() + circuit = circuit_parser_utils_qiskit.remove_measurements(self.circuit) + try: + # for qiskit >= 0.25.0 + simulator = qiskit.Aer.get_backend('aer_simulator_statevector', precision=precision) + circuit = qiskit.transpile(circuit, simulator) + circuit.save_statevector() + result = simulator.run(circuit).result() + except: + # for qiskit 0.24.* + simulator = qiskit.Aer.get_backend('statevector_simulator', precision=precision) + result = qiskit.execute(circuit, simulator).result() + sv = np.asarray(result.get_statevector()).reshape((2,)*circuit.num_qubits) + # statevector returned by qiskit's simulator is labelled by the inverse of :attr:`qiskit.QuantumCircuit.qubits` + # this is different from `cirq` and different from the implementation in :class:`CircuitToEinsum` + sv = sv.transpose(list(range(circuit.num_qubits))[::-1]) if self.backend is torch: - operands = [sv] + gate_operands + [sv.conj().resolve_conj()] + sv = torch.as_tensor(sv, dtype=getattr(torch, self.dtype), device='cuda') else: - operands = [sv] + gate_operands + [sv.conj()] - expec = contract(expression, *operands) - return expec + sv = self.backend.asarray(sv, dtype=self.dtype) + return sv + + def get_sampling(self, qubits_to_sample=None, seed=None, nshots=5000): + if qubits_to_sample is None: + qubits_to_sample = self.qubits + circuit = self.circuit.remove_final_measurements(inplace=False) + new_creg = circuit._create_creg(len(qubits_to_sample), "meas") + circuit.add_register(new_creg) + circuit.measure(qubits_to_sample, new_creg) + precision = self._get_precision() + backend = qiskit.Aer.get_backend('qasm_simulator', precision=precision) + result = backend.run(qiskit.transpile(circuit, backend), shots=nshots, seed=seed).result() + counts = result.get_counts(circuit) + sampling = {} + nsamples = 0 + for bitstring, nsample in counts.items(): + # little endian from qiskit + value = int(bitstring[::-1], 2) + sampling[value] = nsample + nsamples += nsample + assert nsamples == nshots + return sampling - def _get_state_vector_from_simulator(self): - raise NotImplementedError - def _get_sampling_from_simulator(self, qubits_to_sample=None, seed=None): - raise NotImplementedError +class CircuitToEinsumComputeEngine(_BaseComputeEngine): + + def __init__(self, converter): + self.converter = converter + self.backend = self.converter.backend + if self.backend is torch: + self.dtype = str(converter.dtype).split('.')[-1] + else: + self.dtype = converter.dtype.__name__ + self._tolerance = None + self.handle = None # Non-owning + self.sv = None + self.norm = None - def get_sampling_from_sv(self, qubits_to_sample=None, seed=None): - sv = self.get_state_vector_from_simulator() - p = abs(sv) ** 2 - # convert p to double type in case probs does not add up to 1 - if self.backend is np: - p = p.astype('float64') - elif self.backend is cp: - p = cp.asnumpy(p).astype('float64') - elif self.backend is torch: - if p.device.type == 'cpu': - p = p.numpy().astype('float64') - else: - p = p.cpu().numpy().astype('float64') - if qubits_to_sample is not None: - sorted_qubits_to_sample = [q for q in self.qubits if q in qubits_to_sample] - axis = [i for (i, q) in enumerate(self.qubits) if q not in qubits_to_sample] - if axis: - p = p.sum(tuple(axis)) - # potential transpose to match the order of qubits_to_sample - transpose_order = [sorted_qubits_to_sample.index(q) for q in qubits_to_sample] - p = p.transpose(*transpose_order) - # normalize - p /= p.sum() - if seed is not None: - np.random.seed(seed) - samples = np.random.choice(np.arange(p.size), p=p.flat, size=self.nshots) - hist_sv = np.unique(samples, return_counts=True) - return dict(zip(*hist_sv)) - - def maybe_prepare_state(self): - if not self.state_prepared: - if not hasattr(self, 'state'): - raise RuntimeError("state not initialized") - if self.backend is not cp: - raise RuntimeError("This func is only expected to be executed for cupy backend") + @property + def qubits(self): + return list(self.converter.qubits) + + @property + def tolerance(self): + if self._tolerance is None: + self._tolerance = {'atol': atol_mapper[self.dtype], + 'rtol': rtol_mapper[self.dtype]} + return self._tolerance + + def setup_resources(self, *args, **kwargs): + self.handle = kwargs.get('handle', None) + + def _compute_from_converter(self, task, *args, **kwargs): + assert self.handle is not None, "handle not provided" + expression, operands = getattr(self.converter, task)(*args, **kwargs) + return contract(expression, *operands, options={'handle': self.handle}) + + def get_sv(self): + if self.sv is None: + self.sv = self._compute_from_converter('state_vector') + return self.sv + + def get_reduced_density_matrix(self, where, fixed=EMPTY_DICT, lightcone=True): + return self._compute_from_converter('reduced_density_matrix', where, fixed=fixed, lightcone=lightcone) + + def get_sampling(self, qubits_to_sample=None, seed=None, nshots=5000): + sv = self.get_sv() + if qubits_to_sample is None: + modes_to_sample = list(range(self.n_qubits)) + else: + modes_to_sample = [self.qubits.index(q) for q in qubits_to_sample] + return sample_from_sv(sv, nshots, modes_to_sample=modes_to_sample, seed=seed) + + def get_amplitude(self, bitstring): + return self._compute_from_converter('amplitude', bitstring) + + def get_batched_amplitudes(self, fixed): + return self._compute_from_converter('batched_amplitudes', fixed) + + def get_expectation(self, pauli_string, lightcone=True): + return self._compute_from_converter('expectation', pauli_string, lightcone=lightcone) + + +class StateComputeEngine(_BaseComputeEngine): + ##################################################################### + # + # Implementation from cutensornetState_t APIs. + # This reference are only meant to be tested when backend is `cupy`. + # + # The methods below must have the same API signature with + # their counterer parts in `BastFrameworkComputeEngine` + # (up to the first few arguments being handle and workspace): + # 1. `StateComputeEngine.get_sv` + # 2. `StateComputeEngine.get_amplitude` + # 3. `StateComputeEngine.get_batched_amplitudes` + # 4. `StateComputeEngine.get_reduced_density_matrix` + # 5. `StateComputeEngine.get_expectation` + # 6. `StateComputeEngine.get_sampling` + # + ##################################################################### + + def __init__(self, converter, **options): + if converter.backend is not cp: + raise RuntimeError("This class is only expected to be executed for cupy backend") + self.converter = converter + self.state = None + self.state_computed = False + self.circuit_state_parsed = False + if converter.backend is torch: + self.dtype = str(converter.dtype).split('.')[-1] + else: + self.dtype = converter.dtype.__name__ + self.options = options + self._tolerance = None + gate_i = cp.asarray([[1,0], [0,1]], dtype=self.dtype, order=np.random.choice(['C', 'F'])) + gate_x = cp.asarray([[0,1], [1,0]], dtype=self.dtype, order=np.random.choice(['C', 'F'])) + gate_y = cp.asarray([[0,-1j], [1j,0]], dtype=self.dtype, order=np.random.choice(['C', 'F'])) + gate_z = cp.asarray([[1,0], [0,-1]], dtype=self.dtype, order=np.random.choice(['C', 'F'])) + self.pauli_map = {'I': gate_i.T, + 'X': gate_x.T, + 'Y': gate_y.T, + 'Z': gate_z.T} + self.norm = None + self.sv = None + self.handle = None # non-owning + self.workspace = None # non-owning + + @property + def qubits(self): + return list(self.converter.qubits) + + @property + def tolerance(self): + if self._tolerance is None: + self._tolerance = {'atol': atol_mapper[self.dtype], + 'rtol': rtol_mapper[self.dtype]} + return self._tolerance + + def __del__(self): + if self.state is not None: + cutn.destroy_state(self.state) + + def setup_resources(self, *args, **kwargs): + self.handle = kwargs.get('handle', None) + self.workspace = kwargs.get('workspace', None) + + def _maybe_create_state(self): + assert self.handle is not None and self.workspace is not None, f"handle or workspace not setted up" + if self.state is None: + dtype = dtype_to_data_type[getattr(np, self.dtype)] + # create the state object + self.state = cutn.create_state(self.handle, + cutn.StatePurity.PURE, self.n_qubits, (2,)*self.n_qubits, dtype) + + def _maybe_parse_state(self): + self._maybe_create_state() + + if not self.circuit_state_parsed: gates = self.converter.gates immutable = 0 adjoint = 0 @@ -415,15 +697,31 @@ def maybe_prepare_state(self): tensor_id = cutn.state_apply_tensor(self.handle, self.state, n_state_modes, state_modes, tmp.data.ptr, tensor_mode_strides, immutable, adjoint, unitary) - cutn.state_update_tensor(self.handle, self.state, tensor_id, operand.data.ptr, unitary) + cutn.state_update_tensor(self.handle, self.state, + tensor_id, operand.data.ptr, unitary) else: cutn.state_apply_tensor(self.handle, self.state, n_state_modes, state_modes, operand.data.ptr, tensor_mode_strides, immutable, adjoint, unitary) - self.state_prepared = True - - def _run_cutensornet_sampling_marginal(self, task, create_args, execute_args, stream): - self.maybe_prepare_state() + self.circuit_state_parsed = True + + def _maybe_parse_options(self): + if self.options: + raise NotImplementedError + + def _maybe_compute_state(self): + # Implement this for different type of simulators + # For tensor network simulator, final state is not computed + # For other types of simulator, final state must be explictly computed and stored + if not self.state_computed: + self._maybe_parse_state() + self._maybe_parse_options() + self.state_computed = True + + def _compute_target(self, task, create_args, execute_args, stream): + if task != 'state': + # avoid going into infinite loops + self._maybe_compute_state() if task == 'marginal': create_func = cutn.create_marginal configure_func = cutn.marginal_configure @@ -440,36 +738,117 @@ def _run_cutensornet_sampling_marginal(self, task, create_args, execute_args, st prepare_func = cutn.sampler_prepare execute_func = cutn.sampler_sample destroy_func = cutn.destroy_sampler + elif task == 'accessor': + create_func = cutn.create_accessor + configure_func = cutn.accessor_configure + hyper_sample_attr = cutn.AccessorAttribute.OPT_NUM_HYPER_SAMPLES + num_hyper_samples_dtype = cutn.accessor_get_attribute_dtype(hyper_sample_attr) + prepare_func = cutn.accessor_prepare + execute_func = cutn.accessor_compute + destroy_func = cutn.destroy_accessor + elif task == 'expectation': + create_func = cutn.create_expectation + configure_func = cutn.expectation_configure + hyper_sample_attr = cutn.ExpectationAttribute.OPT_NUM_HYPER_SAMPLES + num_hyper_samples_dtype = cutn.accessor_get_attribute_dtype(hyper_sample_attr) + prepare_func = cutn.expectation_prepare + execute_func = cutn.expectation_compute + destroy_func = cutn.destroy_expectation + elif task == 'state': + # full state_vector computation does not need to destroy state + create_func = None + configure_func = cutn.state_configure + hyper_sample_attr = cutn.StateAttribute.NUM_HYPER_SAMPLES + num_hyper_samples_dtype = cutn.state_get_attribute_dtype(hyper_sample_attr) + prepare_func = cutn.state_prepare + execute_func = cutn.state_compute + destroy_func = None else: - raise ValueError("only supports marginal and sampler") + raise ValueError("only supports marginal, sampler, accessor, expectation and state") dev = cp.cuda.Device() free_mem = dev.mem_info[0] scratch_size = free_mem // 2 # maximal usage of 50% device memory - - task_obj = create_func(self.handle, self.state, *create_args) + if create_func is None: # state vector computation + task_obj = self.state + else: + task_obj = create_func(self.handle, self.state, *create_args) num_hyper_samples = np.asarray(8, dtype=num_hyper_samples_dtype) configure_func(self.handle, task_obj, hyper_sample_attr, num_hyper_samples.ctypes.data, num_hyper_samples.dtype.itemsize) prepare_func(self.handle, task_obj, scratch_size, self.workspace, stream.ptr) # similar args for marginal and sampler - workspace_size_d = cutn.workspace_get_memory_size(self.handle, - self.workspace, cutn.WorksizePref.RECOMMENDED, cutn.Memspace.DEVICE, cutn.WorkspaceKind.SCRATCH) - if workspace_size_d >= scratch_size: + for memspace in (cutn.Memspace.DEVICE, cutn.Memspace.HOST): + workspace_size = cutn.workspace_get_memory_size(self.handle, + self.workspace, cutn.WorksizePref.RECOMMENDED, + memspace, cutn.WorkspaceKind.SCRATCH) + workspace_ptr = None + if memspace == cutn.Memspace.DEVICE: + if workspace_size > scratch_size: + destroy_func(task_obj) + return None + else: + workspace_ptr = cp.cuda.alloc(workspace_size).ptr + else: + workspace_ptr = np.empty(workspace_size, dtype=np.int8).ctypes.data + if workspace_size != 0: + cutn.workspace_set_memory(self.handle, + self.workspace, memspace, + cutn.WorkspaceKind.SCRATCH, workspace_ptr, workspace_size) + + output = execute_func(self.handle, task_obj, *execute_args, stream.ptr) + stream.synchronize() + if destroy_func is not None: destroy_func(task_obj) + if isinstance(output, tuple): + return output + else: + return True + + def _run_state_accessor(self, bitstring=None, fixed=None): + if bitstring is not None: + # compute a single bitstring amplitude + assert fixed is None + shape = 1 + num_fixed_modes = self.n_qubits + fixed_modes = list(range(self.n_qubits)) + fixed_values = [int(i) for i in bitstring] + elif fixed is not None: + # compute batched amplitudes + shape = (2,) * (self.n_qubits - len(fixed)) + num_fixed_modes = len(fixed) + fixed_modes = [] + fixed_values = [] + for q, bit in fixed.items(): + fixed_modes.append(self.qubits.index(q)) + fixed_values.append(int(bit)) + else: + # compute full state vector + shape = (2, ) * self.n_qubits + num_fixed_modes = fixed_modes = fixed_values = 0 + + amplitudes = cp.empty(shape, dtype=self.dtype, order=np.random.choice(('C', 'F'))) + amplitudes_strides = [stride_in_bytes // amplitudes.itemsize for stride_in_bytes in amplitudes.strides] + norm = np.empty(1, dtype=self.dtype) + + create_args = (num_fixed_modes, fixed_modes, amplitudes_strides) + execute_args = (fixed_values, self.workspace, amplitudes.data.ptr, norm.ctypes.data) + stream = cp.cuda.get_current_stream() + if self._compute_target('accessor', create_args, execute_args, stream): + if self.norm is None: + self.norm = norm.item() + else: + assert np.allclose(self.norm, norm.item(), **self.tolerance) + return amplitudes + else: return None - scratch_space = cp.cuda.alloc(workspace_size_d) - cutn.workspace_set_memory(self.handle, - self.workspace, cutn.Memspace.DEVICE, - cutn.WorkspaceKind.SCRATCH, scratch_space.ptr, workspace_size_d) - - execute_func(self.handle, task_obj, *execute_args, stream.ptr) - stream.synchronize() - destroy_func(task_obj) - return True + def get_sv(self): + if self.sv is None: + self.sv = self._run_state_accessor() + return self.sv - def get_reduced_density_matrix_from_cutn(self, where, fixed=EMPTY_DICT): + def get_reduced_density_matrix(self, where, fixed=EMPTY_DICT): n_marginal_modes = len(where) marginal_modes = [self.qubits.index(q) for q in where] if fixed: @@ -482,28 +861,30 @@ def get_reduced_density_matrix_from_cutn(self, where, fixed=EMPTY_DICT): else: n_projected_modes = projected_modes = projected_mode_values = 0 - rdm = cp.empty((2,2)*n_marginal_modes, dtype=self.dtype, order=np.random.choice(['C', 'F'])) + rdm = cp.empty((2,2)*n_marginal_modes, + dtype=self.dtype, order=np.random.choice(['C', 'F'])) rdm_strides = [s // rdm.itemsize for s in rdm.strides] stream = cp.cuda.get_current_stream() create_args = (n_marginal_modes, marginal_modes, n_projected_modes, projected_modes, rdm_strides) execute_args = (projected_mode_values, self.workspace, rdm.data.ptr) - if self._run_cutensornet_sampling_marginal('marginal', create_args, execute_args, stream): + if self._compute_target('marginal', create_args, execute_args, stream): return rdm else: return None - def get_sampling_from_cutensornet(self, qubits_to_sample=None, seed=None): + def get_sampling(self, qubits_to_sample=None, seed=None, nshots=5000): if qubits_to_sample is None: qubits_to_sample = self.qubits n_modes_to_sample = len(qubits_to_sample) modes_to_sample = [self.qubits.index(q) for q in qubits_to_sample] - samples = np.empty((self.nshots, n_modes_to_sample), dtype='int64', order='C') # equivalent to (n_modes, nshots) in F order + samples = np.empty((nshots, n_modes_to_sample), + dtype='int64', order='C') # equivalent to (n_modes, nshots) in F order stream = cp.cuda.get_current_stream() create_args = (n_modes_to_sample, modes_to_sample) - execute_args = (self.nshots, self.workspace, samples.ctypes.data) - if self._run_cutensornet_sampling_marginal('sampler', create_args, execute_args, stream): + execute_args = (nshots, self.workspace, samples.ctypes.data) + if self._compute_target('sampler', create_args, execute_args, stream): sampling = {} for bitstring, n_sampling in zip(*np.unique(samples, axis=0, return_counts=True)): bitstring = np.array2string(bitstring, separator='')[1:-1] @@ -511,209 +892,454 @@ def get_sampling_from_cutensornet(self, qubits_to_sample=None, seed=None): return sampling else: return None + + def get_amplitude(self, bitstring): + return self._run_state_accessor(bitstring=bitstring) - def test_qubits(self): - assert len(self.qubits) == self.n_qubits + def get_batched_amplitudes(self, fixed): + return self._run_state_accessor(fixed=fixed) - def test_gates(self): - for (gate_operand, qubits) in self.converter.gates: - assert gate_operand.ndim == len(qubits) * 2 - assert infer_object_package(gate_operand) == self.backend.__name__ + # cutensornet State APIs can not compute a single expectation. + # Here we compute the sum of all Pauli strings + def get_expectation_sum(self, pauli_strings): + if not isinstance(pauli_strings, dict): + raise ValueError("pauli_strings is expected to be a map from paul strings to coefficients") + dtype = dtype_to_data_type[getattr(np, self.dtype)] + hamiltonian = cutn.create_network_operator(self.handle, + self.n_qubits, (2,)*self.n_qubits, dtype) + for pauli_string, coefficient in pauli_strings.items(): + num_tensors = 0 + num_modes = [] + state_modes = [] + tensor_mode_strides = [] + tensor_data = [] + for q, pauli_char in enumerate(pauli_string): + if pauli_char == 'I': continue + operand = self.pauli_map[pauli_char] + num_tensors += 1 + num_modes.append(1) + state_modes.append([q]) + tensor_mode_strides.append([stride_in_bytes//operand.itemsize for stride_in_bytes in operand.strides]) + tensor_data.append(operand.data.ptr) + if num_tensors == 0: + # pauli string being IIIIII + num_tensors = self.n_qubits + num_modes = [1,] * num_tensors + state_modes = list(range(num_tensors)) + operand = self.pauli_map['I'] + tensor_data = [operand.data.ptr] * num_tensors + tensor_mode_strides = [stride_in_bytes//operand.itemsize for stride_in_bytes in operand.strides] * num_tensors + cutn.network_operator_append_product(self.handle, + hamiltonian, coefficient, num_tensors, + num_modes, state_modes, tensor_mode_strides, tensor_data) + + expectation_value = np.empty(1, dtype=self.dtype) + norm = np.empty(1, dtype=self.dtype) + create_args = (hamiltonian, ) + execute_args = (self.workspace, expectation_value.ctypes.data, norm.ctypes.data) + stream = cp.cuda.get_current_stream() + if self._compute_target('expectation', create_args, execute_args, stream): + output = expectation_value.item() + if self.norm is None: + self.norm = norm.item() + else: + assert np.allclose(self.norm, norm.item(), **self.tolerance) + else: + output = None + cutn.destroy_network_operator(hamiltonian) + return output + + +class SVStateComputeEngine(StateComputeEngine): + + def _maybe_compute_state(self): + # Implement this for different type of simulators + # For tensor network simulator, final state is not computed + # For other types of simulator, final state must be explictly computed and stored + if not self.state_computed: + self._maybe_parse_state() + self._maybe_parse_options() + order = np.random.choice(('C', 'F')) + sv = cp.empty((2,) * self.n_qubits, dtype=self.dtype, order=order) + stream = cp.cuda.get_current_stream() + create_args = () + execute_args = (self.workspace, [sv.data.ptr]) + output = self._compute_target('state', create_args, execute_args, stream) + if output: + extents = output[0][0] + strides = [s * sv.dtype.itemsize for s in output[1][0]] + if order == 'F': + self.sv = sv + else: + self.sv = cp.ndarray(extents, + dtype=sv.dtype, memptr=sv.data, strides=strides) + self.state_computed = True + else: + self.sv = None + + def get_sv(self): + self._maybe_compute_state() + return self.sv + + +class MPSStateComputeEngine(StateComputeEngine): + + @property + def tolerance(self): + if self._tolerance is None: + # tolerance for double precision is increase + self._tolerance = get_mps_tolerance(self.dtype) + return self._tolerance + + def _maybe_parse_options(self): + self._maybe_create_state() + # parse max extent + max_extent = self.options.get('max_extent', None) + if max_extent is None: + if self.n_qubits > EXACT_MPS_QUBIT_COUNT_LIMIT: + raise ValueError(f"Exact MPS will encounter overflow with n_qubits={self.n_qubits}") + else: + max_extent = 2**EXACT_MPS_QUBIT_COUNT_LIMIT + self.mps_tensors = [] + prev_extent = 1 + output_mps_extents = [] + output_mps_strides = [] + for i in range(self.n_qubits): + next_extent = min(max_extent, 2**(i+1), 2**(self.n_qubits-i-1)) + if i==0: + extents = (2, next_extent) + elif i !=self.n_qubits - 1: + extents = (prev_extent, 2, next_extent) + else: + extents = (prev_extent, 2) + prev_extent = next_extent + tensor = cp.empty(extents, dtype=self.dtype, order=np.random.choice(['C', 'F'])) + self.mps_tensors.append(tensor) + output_mps_extents.append(extents) + output_mps_strides.append([stride_in_bytes // tensor.itemsize for stride_in_bytes in tensor.strides]) + cutn.state_finalize_mps(self.handle, self.state, + cutn.BoundaryCondition.OPEN, output_mps_extents, output_mps_strides) + + algorithm = 'gesvd' + for key, value in self.options.items(): + if key in STATE_ATTRIBUTE_MAP: + attr = STATE_ATTRIBUTE_MAP[key] + dtype = cutn.state_get_attribute_dtype(attr) + if key == 'algorithm': + algorithm = value + value = SVD_ALGORITHM_MAP[value] + elif key == 'normalization': + value = NORMALIZATION_MAP[value] + elif key == 'canonical_center' and value is None: + continue + value = np.asarray(value, dtype=dtype) + cutn.state_configure(self.handle, self.state, attr, value.ctypes.data, value.dtype.itemsize) + + if algorithm in ('gesvdj', 'gesvdr'): + dtype = cutn.tensor_svd_algo_params_get_dtype(SVD_ALGORITHM_MAP[algorithm]) + algo_params = np.zeros(1, dtype=dtype) + + for name in dtype.names: + value = self.options.get(f'{algorithm}_{name}', 0) + if value != 0: + algo_params[name] = value + cutn.state_configure(self.handle, self.state, + cutn.StateAttribute.MPS_SVD_CONFIG_ALGO_PARAMS, + algo_params.ctypes.data, algo_params.dtype.itemsize) + + def _maybe_compute_state(self): + # Implement this for different type of simulators + # For tensor network simulator, final state is not computed + # For other types of simulator, final state must be explictly computed and stored + if not self.state_computed: + self._maybe_parse_state() + self._maybe_parse_options() + stream = cp.cuda.get_current_stream() + create_args = () + execute_args = (self.workspace, [o.data.ptr for o in self.mps_tensors]) + output = self._compute_target('state', create_args, execute_args, stream) + if output is None: + return False + else: + extents, strides = output + for i in range(self.n_qubits): + extent_in = self.mps_tensors[i].shape + extent_out = extents[i] + if extent_in != tuple(extent_out): + tensor_strides = [s * self.mps_tensors[i].dtype.itemsize for s in strides[i]] + self.mps_tensors[i] = cp.ndarray(extent_out, + dtype=self.mps_tensors[i].dtype, memptr=self.mps_tensors[i].data, strides=tensor_strides) + self.state_computed = True + + def check_canonicalization(self): + self._maybe_compute_state() + center = self.options.get('canonical_center', None) + if center is None: + return + for i in range(self.n_qubits): + if i == 0: + modes = 'pj' + elif i == self.n_qubits - 1: + modes = 'ip' + else: + modes = 'ipj' + if i == center: + continue + if i < center: + shared_mode = 'j' + elif i > center: + shared_mode = 'i' + else: + continue + verify_unitary(self.mps_tensors[i], modes, shared_mode, + SVD_TOLERANCE[self.dtype], tensor_name=f"Site {i} canonicalization") + + +class BaseTester: + + @property + def reference_engine(self): + raise NotImplementedError + + @property + def target_engines(self): + raise NotImplementedError + + @property + def all_engines(self): + return [self.reference_engine] + self.target_engines + + def test_misc(self): + raise NotImplementedError + + def test_norm(self): + norm1 = self.reference_engine.get_norm() + for engine in self.target_engines: + norm2 = engine.get_norm() + message = f"{engine.__class__.__name__} maxDiff={abs(norm1-norm2)}" + assert np.allclose(norm1, norm2, **engine.tolerance), message def test_state_vector(self): - expression, operands = self.converter.state_vector() - sv1 = contract(expression, *operands) - sv2 = self.get_state_vector_from_simulator() - assert self.backend.allclose( - sv1, sv2, atol=atol_mapper[self.dtype], rtol=rtol_mapper[self.dtype]) + sv1 = self.reference_engine.get_sv() + for engine in self.target_engines: + sv2 = engine.get_sv() + message = f"{engine.__class__.__name__} maxDiff={abs(sv1-sv2).max()}" + assert self.backend.allclose(sv1, sv2, **engine.tolerance), message def test_amplitude(self): for bitstring in bitstring_generator(self.n_qubits, self.nsample): - expression, operands = self.converter.amplitude(bitstring) - amp1 = contract(expression, *operands) - amp2 = self.get_amplitude_from_simulator(bitstring) - assert self.backend.allclose( - amp1, amp2, atol=atol_mapper[self.dtype], rtol=rtol_mapper[self.dtype]) + amp1 = self.reference_engine.get_amplitude(bitstring) + for engine in self.target_engines: + amp2 = engine.get_amplitude(bitstring) + message = f"{engine.__class__.__name__} maxDiff={abs(amp1-amp2).max()}" + assert self.backend.allclose(amp1, amp2, **engine.tolerance), message def test_batched_amplitudes(self): for fixed in where_fixed_generator(self.qubits, self.nfix_max): - expression, operands = self.converter.batched_amplitudes(fixed) - batched_amps1 = contract(expression, *operands) - batched_amps2 = self.get_batched_amplitudes_from_simulator(fixed) - assert self.backend.allclose( - batched_amps1, batched_amps2, atol=atol_mapper[self.dtype], rtol=rtol_mapper[self.dtype]) + batched_amps1 = self.reference_engine.get_batched_amplitudes(fixed) + for engine in self.target_engines: + batched_amps2 = engine.get_batched_amplitudes(fixed) + message = f"{engine.__class__.__name__} maxDiff={abs(batched_amps1-batched_amps2).max()}" + assert self.backend.allclose(batched_amps1, batched_amps2, **engine.tolerance), message def test_reduced_density_matrix(self): for where, fixed in where_fixed_generator(self.qubits, self.nfix_max, nsite_max=self.nsite_max): - expression1, operands1 = self.converter.reduced_density_matrix(where, fixed=fixed, lightcone=True) - expression2, operands2 = self.converter.reduced_density_matrix(where, fixed=fixed, lightcone=False) + operands1 = self.converter.reduced_density_matrix(where, fixed=fixed, lightcone=True)[1] + operands2 = self.converter.reduced_density_matrix(where, fixed=fixed, lightcone=False)[1] assert len(operands1) <= len(operands2) + 2 # potential phase handling for qiskit Circuit - rdm1 = contract(expression1, *operands1) - rdm2 = contract(expression2, *operands2) - rdm3 = self.get_reduced_density_matrix_from_simulator(where, fixed=fixed) - - assert self.backend.allclose( - rdm1, rdm2, atol=atol_mapper[self.dtype], rtol=rtol_mapper[self.dtype]) - assert self.backend.allclose( - rdm1, rdm3, atol=atol_mapper[self.dtype], rtol=rtol_mapper[self.dtype]) - if self.backend is cp: - rdm4 = self.get_reduced_density_matrix_from_cutn(where, fixed=fixed) - if rdm4 is not None: - assert self.backend.allclose( - rdm1, rdm4, atol=atol_mapper[self.dtype], rtol=rtol_mapper[self.dtype]) + rdm1 = self.reference_engine.get_reduced_density_matrix(where, fixed=fixed) + if isinstance(self.reference_engine, CircuitToEinsumComputeEngine): + # by default CircuitToEinsumComputeEngine.get_reduced_density_matrix uses lightcone=True + rdm2 = self.reference_engine.get_reduced_density_matrix(where, fixed=fixed, lightcone=False) + assert self.backend.allclose(rdm1, rdm2, **self.reference_engine.tolerance) + + # comparision with different references + for engine in self.target_engines: + rdm2 = engine.get_reduced_density_matrix(where, fixed=fixed) + message = f"{engine.__class__.__name__} maxDiff={abs(rdm1-rdm2).max()}" + assert self.backend.allclose(rdm1, rdm2, **engine.tolerance), message def test_expectation(self): - for pauli_string in random_pauli_string_generator(self.n_qubits, 2): - expression1, operands1 = self.converter.expectation(pauli_string, lightcone=True) - expression2, operands2 = self.converter.expectation(pauli_string, lightcone=False) + full_expectation = 0. + pauli_strings = dict() + for pauli_string in random_pauli_string_generator(self.n_qubits, 6): + coefficient = np.random.random(1).item() + 1j * np.random.random(1).item() + if pauli_string not in pauli_strings: + pauli_strings[pauli_string] = coefficient + else: + # in case duplicate pauli string is reproduced by the random generator + pauli_strings[pauli_string] += coefficient + operands1 = self.converter.expectation(pauli_string, lightcone=True)[1] + operands2 = self.converter.expectation(pauli_string, lightcone=False)[1] assert len(operands1) <= len(operands2) + 2 # potential phase handling for qiskit Circuit - expec1 = contract(expression1, *operands1) - expec2 = contract(expression2, *operands2) - expec3 = self.get_expectation_from_sv(pauli_string) - - assert self.backend.allclose( - expec1, expec2, atol=atol_mapper[self.dtype], rtol=rtol_mapper[self.dtype]) - assert self.backend.allclose( - expec1, expec3, atol=atol_mapper[self.dtype], rtol=rtol_mapper[self.dtype]) + + expec1 = self.reference_engine.get_expectation(pauli_string) + if isinstance(self.reference_engine, CircuitToEinsumComputeEngine): + expec2 = self.reference_engine.get_expectation(pauli_string, lightcone=False) + assert self.backend.allclose(expec1, expec2, **self.reference_engine.tolerance) + + full_expectation += coefficient * expec1 + + for engine in self.target_engines: + if not isinstance(engine, StateComputeEngine): + expec2 = engine.get_expectation(pauli_string) + message = f"{engine.__class__.__name__} maxDiff={abs(expec1-expec2).max()}" + assert self.backend.allclose(expec1, expec2, **engine.tolerance), message + + for engine in self.target_engines: + if isinstance(engine, StateComputeEngine): + expec2 = engine.get_expectation_sum(pauli_strings) + message = f"{engine.__class__.__name__} maxDiff={abs(full_expectation-expec2).max()}" + assert self.backend.allclose(full_expectation, expec2, **engine.tolerance), message def test_sampling(self): full_qubits = list(self.qubits) np.random.shuffle(full_qubits) selected_qubits = full_qubits[:len(full_qubits)//2] - for qubits_to_sample in (None, selected_qubits): - seed = self.seed - nshots = self.nshots - max_try = 3 - overlap_best = 0. - - for counter in range(1, max_try+1): - # build a histogram for the reference impl - hist_ref = self._get_sampling_from_simulator(qubits_to_sample=qubits_to_sample, seed=seed) - - # do the same for cutensornet sampling - hist_cutn = self.get_sampling_from_cutensornet(qubits_to_sample=qubits_to_sample, seed=seed) - - # compute overlap of the histograms (cutn vs ref) - overlap = compute_histogram_overlap(hist_cutn, hist_ref, self.nshots) - if overlap > overlap_best: - overlap_best = overlap - else: - print("WARNING: overlap not improving as nshots increases!") - - # do the same for sampling from the (exactly computed) SV - hist_sv = self.get_sampling_from_sv(qubits_to_sample=qubits_to_sample, seed=seed) - - # compute overlap of the histograms (sv vs ref) - overlap_check = compute_histogram_overlap(hist_sv, hist_ref, self.nshots) - print(f"with nshots = {self.nshots}, {overlap_best = }, {overlap_check = }") - - # to reduce test time we set 95% here, but 99% will also work - if np.round(overlap, decimals=2) < 0.95: - self.nshots *= 10 - print(f"retry with nshots = {self.nshots} ...") + for engine in self.target_engines: + for qubits_to_sample in (None, selected_qubits): + seed = self.seed + nshots = self.nshots + max_try = 3 + overlap_best = 0. + + for counter in range(1, max_try+1): + # build a histogram for the reference impl + hist_ref = self.reference_engine.get_sampling(qubits_to_sample=qubits_to_sample, seed=seed, nshots=self.nshots) + + # do the same for cutensornet sampling + hist_cutn = engine.get_sampling(qubits_to_sample=qubits_to_sample, seed=seed, nshots=self.nshots) + + # compute overlap of the histograms (cutn vs ref) + overlap = compute_histogram_overlap(hist_cutn, hist_ref, self.nshots) + if overlap > overlap_best: + overlap_best = overlap + else: + print(f"WARNING: overlap not improving {counter=} {overlap_best=} {overlap=} as nshots increases!") + + # to reduce test time we set 95% here, but 99% will also work + if np.round(overlap, decimals=2) < 0.95: + self.nshots *= 10 + print(f"retry with nshots = {self.nshots} ...") + else: + self.nshots = nshots # restore + break else: self.nshots = nshots # restore - break - else: - self.nshots = nshots # restore - assert False, f"{overlap_best=} after {counter} retries..." - + assert False, f"{overlap_best=} after {counter} retries..." + @manage_resource("handle") - @manage_resource("state") @manage_resource("workspace") def run_tests(self): + resources = {'handle': self.handle, 'workspace': self.workspace} + # share cutensornet resources for all compute engines + for engine in self.all_engines: + engine.setup_resources(**resources) + self.test_state_vector() self.test_amplitude() self.test_batched_amplitudes() self.test_reduced_density_matrix() self.test_expectation() - self.test_gates() - self.test_qubits() + self.test_norm() + self.test_misc() if self.backend is cp: # sampling only needed to be tested for cupy backend self.test_sampling() -class CirqTester(BaseTester): - def _get_state_vector_from_simulator(self): - qubits = self.qubits - simulator = cirq.Simulator(dtype=self.dtype) - circuit = circuit_parser_utils_cirq.remove_measurements(self.circuit) - result = simulator.simulate(circuit, qubit_order=qubits) - statevector = result.state_vector().reshape((2,)*self.n_qubits) - if self.backend is torch: - statevector = torch.as_tensor(statevector, dtype=getattr(torch, self.dtype), device='cuda') +class CircuitToEinsumTester(BaseTester): + def __init__(self, circuit, dtype, backend, nsample, nsite_max, nfix_max, nshots=5000, seed=1024): + self.circuit = circuit + self.converter = CircuitToEinsum(circuit, dtype=dtype, backend=backend) + self.backend = backend + self.qubits = list(self.converter.qubits) + self.n_qubits = self.converter.n_qubits + self.dtype = dtype + self.sv = None + self.nsample = nsample + self.nsite_max = max(1, min(nsite_max, self.n_qubits-1)) + self.nfix_max = max(min(nfix_max, self.n_qubits-nsite_max-1), 0) + self.nshots = nshots + self.seed = seed + + self._reference_engine = CircuitToEinsumComputeEngine(self.converter) + + # Framework provider as reference + if qiskit and isinstance(circuit, qiskit.QuantumCircuit): + self._target_engines = [QiskitComputeEngine(circuit, dtype, backend)] + elif cirq and isinstance(circuit, cirq.Circuit): + self._target_engines = [CirqComputeEngine(circuit, dtype, backend)] else: - statevector = self.backend.asarray(statevector, dtype=self.dtype) - return statevector + raise ValueError(f"circuit type {type(circuit)} not supported") + + if backend == cp: + # Tensor network state simulator + self._target_engines.append(StateComputeEngine(self.converter)) + # SV state simulator + self._target_engines.append(SVStateComputeEngine(self.converter)) + # MPS simulators are only functioning if no multicontrol gates exist in the circuit. + if is_converter_mps_compatible(self.converter): + # MPS state simulator + self._target_engines.append(MPSStateComputeEngine(self.converter)) + # reference MPS implementation + self._target_engines.append(MPS.from_converter(self.converter)) - def _get_sampling_from_simulator(self, qubits_to_sample=None, seed=None): - if qubits_to_sample is None: - qubits_to_sample = list(self.qubits) - circuit = circuit_parser_utils_cirq.remove_measurements(self.circuit) - circuit.append(cirq.measure_each(qubits_to_sample)) - circuit.append(cirq.measure(*qubits_to_sample, key='meas')) - result = cirq.sample( - circuit, repetitions=self.nshots, seed=seed, dtype=getattr(np, self.dtype)) - result = result.histogram(key='meas') - sampling = {} - nsamples = 0 - for bitstring, nsample in result.items(): - sampling[int(bitstring)] = nsample - nsamples += nsample - assert nsamples == self.nshots - return sampling + @property + def reference_engine(self): + return self._reference_engine + + @property + def target_engines(self): + return self._target_engines + def test_misc(self): + self.test_qubits() + self.test_gates() + norm = self.reference_engine.get_norm() + assert np.allclose(norm, 1, **self.reference_engine.tolerance) + + def test_qubits(self): + assert len(self.qubits) == self.n_qubits + + def test_gates(self): + for (gate_operand, qubits) in self.converter.gates: + assert gate_operand.ndim == len(qubits) * 2 + assert infer_object_package(gate_operand) == self.backend.__name__ -class QiskitTester(BaseTester): - def _get_precision(self): - precision = {'complex64': 'single', - 'complex128': 'double'}[self.dtype] - return precision + +class ApproximateMPSTester(BaseTester): + def __init__(self, converter, nsample, nsite_max, nfix_max, nshots=5000, seed=1024, **mps_options): + self.converter = converter + self.backend = converter.backend + if self.backend is not cp: + raise ValueError("This tester is only meant for cupy testing") + self.qubits = list(self.converter.qubits) + self.n_qubits = self.converter.n_qubits + self.dtype = self.converter.dtype.__name__ + self.sv = None + self.norm = None + self.nsample = nsample + self.nsite_max = max(1, min(nsite_max, self.n_qubits-1)) + self.nfix_max = max(min(nfix_max, self.n_qubits-nsite_max-1), 0) + self.nshots = nshots + self.seed = seed + self.mps_options = mps_options + if not is_converter_mps_compatible(self.converter): + raise ValueError("circuit contains gates acting on more than 2 qubits") + self._reference_engine = MPS.from_converter(self.converter, **self.mps_options) + self._target_engines = [MPSStateComputeEngine(self.converter, **self.mps_options)] - def _get_state_vector_from_simulator(self): - # requires qiskit >= 0.24.0 - precision = self._get_precision() - circuit = circuit_parser_utils_qiskit.remove_measurements(self.circuit) - try: - # for qiskit >= 0.25.0 - simulator = qiskit.Aer.get_backend('aer_simulator_statevector', precision=precision) - circuit = qiskit.transpile(circuit, simulator) - circuit.save_statevector() - result = simulator.run(circuit).result() - except: - # for qiskit 0.24.* - simulator = qiskit.Aer.get_backend('statevector_simulator', precision=precision) - result = qiskit.execute(circuit, simulator).result() - sv = np.asarray(result.get_statevector()).reshape((2,)*circuit.num_qubits) - # statevector returned by qiskit's simulator is labelled by the inverse of :attr:`qiskit.QuantumCircuit.qubits` - # this is different from `cirq` and different from the implementation in :class:`CircuitToEinsum` - sv = sv.transpose(list(range(circuit.num_qubits))[::-1]) - if self.backend is torch: - sv = torch.as_tensor(sv, dtype=getattr(torch, self.dtype), device='cuda') - else: - sv = self.backend.asarray(sv, dtype=self.dtype) - return sv + @property + def reference_engine(self): + return self._reference_engine - def _get_sampling_from_simulator(self, qubits_to_sample=None, seed=None): - if qubits_to_sample is None: - qubits_to_sample = list(self.qubits) - circuit = self.circuit.remove_final_measurements(inplace=False) - new_creg = circuit._create_creg(len(qubits_to_sample), "meas") - circuit.add_register(new_creg) - circuit.measure(qubits_to_sample, new_creg) - precision = self._get_precision() - backend = qiskit.Aer.get_backend('qasm_simulator', precision=precision) - result = backend.run(qiskit.transpile(circuit, backend), shots=self.nshots, seed=seed).result() - counts = result.get_counts(circuit) - sampling = {} - nsamples = 0 - for bitstring, nsample in counts.items(): - # little endian from qiskit - value = int(bitstring[::-1], 2) - sampling[value] = nsample - nsamples += nsample - assert nsamples == self.nshots - return sampling + @property + def target_engines(self): + return self._target_engines + + def test_misc(self): + for engine in self.all_engines: + engine.check_canonicalization() \ No newline at end of file diff --git a/python/tests/cuquantum_tests/cutensornet_tests/data.py b/python/tests/cuquantum_tests/cutensornet_tests/data.py index 2ecee3c..631c599 100644 --- a/python/tests/cuquantum_tests/cutensornet_tests/data.py +++ b/python/tests/cuquantum_tests/cutensornet_tests/data.py @@ -31,6 +31,8 @@ # the second variant is suitable for testing exotic TNs that require further customization # TODO: expand the tests einsum_expressions = ( + "ii->", + "jii->ij", "ij,jb,ah", "ea,fb,abcd,gc,hd->efgh", "ea,fb,abcd,gc,hd", diff --git a/python/tests/cuquantum_tests/cutensornet_tests/mps_utils.py b/python/tests/cuquantum_tests/cutensornet_tests/mps_utils.py new file mode 100644 index 0000000..04d8e80 --- /dev/null +++ b/python/tests/cuquantum_tests/cutensornet_tests/mps_utils.py @@ -0,0 +1,415 @@ +# Copyright (c) 2023, NVIDIA CORPORATION & AFFILIATES +# +# SPDX-License-Identifier: BSD-3-Clause + +# Note: This file must be self-contained and not import private helpers! + +from dataclasses import asdict, dataclass +import importlib +import logging +from types import MappingProxyType +from typing import Optional + +try: + import cupy as cp +except ImportError: + cp = None +import numpy as np +import opt_einsum as oe +try: + import torch + if not torch.cuda.is_available(): + raise ImportError +except ImportError: + torch = None + +from cuquantum import CircuitToEinsum +from cuquantum.cutensornet._internal.circuit_converter_utils import get_pauli_gates +from cuquantum.cutensornet._internal.utils import infer_object_package +from cuquantum.cutensornet._internal.tensor_wrapper import _get_backend_asarray_func +from .approxTN_utils import tensor_decompose, gate_decompose, SVD_TOLERANCE, verify_unitary +from .test_utils import gen_rand_svd_method, atol_mapper, rtol_mapper + +#################################################### +################# Helper functions ################# +#################################################### + + +EMPTY_DICT = MappingProxyType(dict()) + +def get_partial_indices(n, fixed=EMPTY_DICT): + partial_indices = [slice(None)] * n + index_map = {'0': slice(0, 1), + '1': slice(1, 2)} + for q, val in fixed.items(): + partial_indices[q] = index_map[val] + return tuple(partial_indices) + + +def reduced_density_matrix_from_sv(sv, where, fixed=EMPTY_DICT): + n = sv.ndim + sv = sv[get_partial_indices(n, fixed)] + bra_modes = list(range(n)) + ket_modes = [i+n if i in where else i for i in range(n)] + output_modes = list(where) + [i+n for i in where] + if infer_object_package(sv) is torch: + inputs = [sv, bra_modes, sv.conj().resolve_conj(), ket_modes] + else: + inputs = [sv, bra_modes, sv.conj(), ket_modes] + inputs.append(output_modes) + return oe.contract(*inputs) + + +def batched_amplitude_from_sv(sv, fixed): + n = sv.ndim + sv = sv[get_partial_indices(n, fixed)] + return sv.reshape([2,]* (n-len(fixed))) + + +def amplitude_from_sv(sv, bitstring): + index = [int(ibit) for ibit in bitstring] + return sv[tuple(index)] + + +def expectation_from_sv(sv, pauli_string): + n = sv.ndim + pauli_map = dict(zip(range(n), pauli_string)) + backend = importlib.import_module(infer_object_package(sv)) + pauli_gates = get_pauli_gates(pauli_map, dtype=sv.dtype, backend=backend) + # tentative bra/ket indices + if backend is torch: + inputs = [sv, list(range(n)), sv.conj().resolve_conj(), list(range(n))] + else: + inputs = [sv, list(range(n)), sv.conj(), list(range(n))] + for o, qs in pauli_gates: + q = qs[0] + inputs[3][q] += n # update ket indices + inputs.extend([o, [q+n, q]]) + return oe.contract(*inputs) + + +def sample_from_sv(sv, nshots, modes_to_sample=None, seed=None): + backend = infer_object_package(sv) + p = abs(sv) ** 2 + # convert p to double type in case probs does not add up to 1 + if backend == 'numpy': + p = p.astype('float64') + elif backend == 'cupy': + p = cp.asnumpy(p).astype('float64') + elif backend == 'torch': + if p.device.type == 'cpu': + p = p.numpy().astype('float64') + else: + p = p.cpu().numpy().astype('float64') + if modes_to_sample is not None: + sorted_modes_to_sample = sorted(modes_to_sample) + axis = [q for q in range(sv.ndim) if q not in modes_to_sample] + if axis: + p = p.sum(tuple(axis)) + # NOTE: bug here + transpose_order = [sorted_modes_to_sample.index(q) for q in modes_to_sample] + p = p.transpose(*transpose_order) + # normalize + p /= p.sum() + if seed is not None: + np.random.seed(seed) + samples = np.random.choice(np.arange(p.size), p=p.flat, size=nshots) + hist_sv = np.unique(samples, return_counts=True) + return dict(zip(*hist_sv)) + + +def gen_random_mps(n_qubits, backend, rng, dtype, D=None): + assert backend in (cp, np), "backend not supported" + assert dtype in ('complex64', 'complex128'), f"dtype {dtype} not supported" + real_dtype = 'float32' if dtype == 'complex64' else 'float64' + mps_tensors = [] + for i in range(n_qubits): + next_D = D if D is not None else rng.integers(1, 8) + if i == 0: + shape = (2, next_D) + elif i == n_qubits - 1: + shape = (prev_D, 2) + else: + shape = (prev_D, 2, next_D) + t = rng.random(shape, dtype=real_dtype) + 1j * rng.random(shape, dtype=real_dtype) + t = backend.asarray(t, order=rng.choice(['C', 'F'])) + t /= backend.linalg.norm(t) + mps_tensors.append(t) + prev_D = next_D + return mps_tensors + + +def get_mps_tolerance(dtype): + tolerance = {'rtol': rtol_mapper[dtype], + 'atol': atol_mapper[dtype]} + if dtype in ('float64', 'complex128'): + # for double precision, relax the tolerance + tolerance['rtol'] += SVD_TOLERANCE[dtype] ** .5 + tolerance['atol'] += SVD_TOLERANCE[dtype] ** .5 + else: + tolerance['rtol'] += SVD_TOLERANCE[dtype] + tolerance['atol'] += SVD_TOLERANCE[dtype] + return tolerance + + +@dataclass +class MPSConfig: + """Class for MPS simulation.""" + # final state + canonical_center: Optional[int] = None + # svd options + max_extent: Optional[int] = None + abs_cutoff: Optional[float] = 0 + rel_cutoff: Optional[float] = 0 + discarded_weight_cutoff: Optional[float] = 0 + normalization: Optional[str] = None + algorithm: Optional[str] = 'gesvd' + gesvdj_tol: Optional[float] = 0 + gesvdj_max_sweeps: Optional[int] = 0 + gesvdr_oversampling: Optional[int] = 0 + gesvdr_niters: Optional[int] = 0 + + def __post_init__(self): + # to be parsed to reference MPS implementation, algorithm and params not supported + self.svd_options = {'max_extent': self.max_extent, + 'abs_cutoff': self.abs_cutoff, + 'rel_cutoff': self.rel_cutoff, + 'discarded_weight_cutoff': self.discarded_weight_cutoff, + 'normalization': self.normalization, + 'partition': 'UV'} # must be enforced to UV partition + + @staticmethod + def rand(n_qubits, rng, dtype, fixed=None, dict_format=True): + config = dict() + config['canonical_center'] = rng.integers(0, high=n_qubits) + svd_method = asdict(gen_rand_svd_method(rng, dtype, fixed=fixed)) + # MPS simulation does not take allow partition other than 'UV' + svd_method.pop('partition') + config.update(svd_method) + # if found exact MPS simulation setting, shrink it down to truncated extent + if config['max_extent'] >= 2**(n_qubits//2): + config['max_extent'] = rng.integers(1, high=2**(n_qubits//2)) + if dict_format: + return config + else: + return MPSConfig(**config) + + +class MPS: + + def __init__( + self, + mps_tensors, + qubits=None, + **mps_config + ): + self.n = len(mps_tensors) + # avoid in-place modification + self.mps_tensors = mps_tensors.copy() + # potentially insert dummy labels for boundary tensors for consistent notation in this class + if self.mps_tensors[0].ndim == 2: + self.mps_tensors[0] = self.mps_tensors[0].reshape(1, *self.mps_tensors[0].shape) + if self.mps_tensors[-1].ndim == 2: + new_shape = self.mps_tensors[-1].shape + (1, ) + self.mps_tensors[-1] = self.mps_tensors[-1].reshape(*new_shape) + self.qubits = qubits + self.dtype = mps_tensors[0].dtype.name + self.sv = None + self.norm = None + self.backend = importlib.import_module(infer_object_package(mps_tensors[0])) + self.swap_gate = None + self.mps_config = MPSConfig(**mps_config) + self._tolerance = get_mps_tolerance(self.dtype) + + @property + def tolerance(self): + return self._tolerance + + def setup_resources(self, *args, **kwargs): + pass + + def get_swap_gate(self): + if self.swap_gate is None: + asarray = _get_backend_asarray_func(self.backend) + self.swap_gate = asarray([[1,0,0,0], + [0,0,1,0], + [0,1,0,0], + [0,0,0,1]], dtype=self.dtype).reshape(2,2,2,2) + return self.swap_gate + + def __getitem__(self, key): + assert key >= 0 and key < self.n + return self.mps_tensors[key] + + def __setitem__(self, key, val): + assert key>=0 and key < self.n + self.mps_tensors[key] = val + # resetting SV and norm + self.sv = self.norm = None + + def get_norm(self): + if self.norm is None: + self.norm = self.backend.linalg.norm(self.get_sv()) ** 2 + return self.norm + + def get_sv(self): + if self.sv is None: + inputs = [] + output_modes = [] + for i, o in enumerate(self.mps_tensors): + modes = [2*i, 2*i+1, 2*i+2] + inputs.extend([o, modes]) + output_modes.append(2*i+1) + inputs.append(output_modes) + self.sv = oe.contract(*inputs) + return self.sv + + def get_amplitude(self, bitstring): + return amplitude_from_sv(self.get_sv(), bitstring) + + def get_batched_amplitudes(self, fixed=EMPTY_DICT): + if self.qubits is not None: + _fixed = dict([(self.qubits.index(q), bit) for q, bit in fixed.items()]) + else: + _fix = fixed + return batched_amplitude_from_sv(self.get_sv(), fixed=_fixed) + + def get_reduced_density_matrix(self, where, fixed=EMPTY_DICT): + if self.qubits is not None and not isinstance(where[0], int): + _where = [self.qubits.index(q) for q in where] + _fixed = dict([(self.qubits.index(q), bit) for q, bit in fixed.items()]) + else: + _where = where + _fixed = fixed + return reduced_density_matrix_from_sv(self.get_sv(), _where, fixed=_fixed) + + def get_expectation(self, pauli_string): + return expectation_from_sv(self.get_sv(), pauli_string) + + def get_sampling(self, qubits_to_sample=None, seed=None, nshots=5000): + if qubits_to_sample is None: + _qubits_to_sample = None + else: + _qubits_to_sample = [self.qubits.index(q) for q in qubits_to_sample] + return sample_from_sv(self.get_sv(), nshots, modes_to_sample=_qubits_to_sample, seed=seed) + + def _apply_gate_1q(self, i, operand): + self[i] = self.backend.einsum('ipj,Pp->iPj', self[i], operand) + + def _apply_gate_2q(self, i, j, operand): + if i > j: + return self._apply_gate_2q(j, i, operand.transpose(1,0,3,2)) + elif i == j: + raise ValueError(f"gate acting on the same site {i} twice") + elif i == j - 1: + self[i], _, self[j] = gate_decompose('ipj,jqk,PQpq->iPj,jQk', self[i], self[j], operand, **self.mps_config.svd_options) + else: + # insert swap gates recursively + swap_gate = self.get_swap_gate() + if (j - i) % 2 == 0: + self._apply_gate_2q(i, i+1, swap_gate) + self._apply_gate_2q(i+1, j, operand) + self._apply_gate_2q(i, i+1, swap_gate) + else: + self._apply_gate_2q(j-1, j, swap_gate) + self._apply_gate_2q(i, j-1, operand) + self._apply_gate_2q(j-1, j, swap_gate) + + def apply_gate(self, sites, operand): + if len(sites) == 1: + return self._apply_gate_1q(*sites, operand) + elif len(sites) == 2: + return self._apply_gate_2q(*sites, operand) + else: + raise NotImplementedError("Only single- and two- qubit gate supported") + + @staticmethod + def from_converter(converter, initial_state=None, **mps_config): + if initial_state is None: + asarray = _get_backend_asarray_func(converter.backend) + t = asarray([1,0], dtype=converter.dtype).reshape(1,2,1) + initial_state = [t, ] * len(converter.qubits) + mps = MPS(initial_state, qubits=list(converter.qubits), **mps_config) + for operand, qs in converter.gates: + sites = [converter.qubits.index(q) for q in qs] + mps.apply_gate(sites, operand) + mps.canonicalize() + return mps + + def print(self): + print([o.shape[2] for o in self.mps_tensors[:-1]]) + + def canonicalize(self): + center = self.mps_config.canonical_center + if center is None: + return + max_extent = self.mps_config.max_extent + svd_method = self.mps_config.svd_options.copy() + svd_method['partition'] = 'V' + for i in range(center): + shared_extent = self[i+1].shape[0] + if max_extent is not None and shared_extent > max_extent: + self[i], r = tensor_decompose('ipj->ipx,xj', self[i], method='svd', **svd_method) + else: + self[i], r = tensor_decompose('ipj->ipx,xj', self[i], method='qr') + self[i+1] = self.backend.einsum('xj,jqk->xqk', r, self[i+1]) + for i in range(self.n-1, center, -1): + shared_extent = self[i].shape[0] + if max_extent is not None and shared_extent > max_extent: + self[i], r = tensor_decompose('ipj->xpj,ix', self[i], method='svd', **svd_method) + else: + self[i], r = tensor_decompose('ipj->xpj,ix', self[i], method='qr') + self[i-1] = self.backend.einsum('mqi,ix->mqx', self[i-1], r) + + def check_canonicalization(self): + center = self.mps_config.canonical_center + if center is None: + return + modes = 'ipj' + for i in range(self.n): + if i < center: + shared_mode = 'j' + elif i > center: + shared_mode = 'i' + else: + continue + verify_unitary(self[i], modes, shared_mode, + SVD_TOLERANCE[self.dtype], tensor_name=f"Site {i} canonicalization") + + +if __name__ == '__main__': + from cuquantum_benchmarks.frontends.frontend_qiskit import Qiskit as cuqnt_qiskit + from cuquantum_benchmarks.benchmarks import qpe, quantum_volume, qaoa, random + from cuquantum import contract + generators = [qpe.QPE, quantum_volume.QuantumVolume, qaoa.QAOA] + config = {'measure': True, 'unfold': True, 'p': 4} + n_qubits = 8 + nshots = 10000 + + # exact MPS for reference + mps_config = {'abs_cutoff':1e-8, 'rel_cutoff':1e-5, 'canonical_center': 2} + for generator in generators: + seq = generator.generateGatesSequence(n_qubits, config) + circuit = cuqnt_qiskit(n_qubits, config).generateCircuit(seq) + converter = CircuitToEinsum(circuit) + expr, operands = converter.state_vector() + sv = contract(expr, *operands) + mps0 = MPS.from_converter(converter, **mps_config) + mps1 = MPS.from_converter(converter, max_extent=8, **mps_config) + + mps0.check_canonicalization() + mps1.check_canonicalization() + sv0 = mps0.get_sv() + sv1 = mps1.get_sv() / mps1.get_norm() + samples = sample_from_sv(sv, nshots, seed=1) + samples0 = mps0.get_sampling(seed=1, nshots=nshots) + samples1 = mps1.get_sampling(seed=1, nshots=nshots) + + print("Exact MPS bonds:") + mps0.print() + print("MPS bonds with max extent 8") + mps1.print() + print(f"Exact MPS SV error: {abs(sv0-sv).max()}; Approximate MPS SV error: {abs(sv1-sv).max()}") + sv_ovlp = abs(cp.dot(sv.ravel(), sv1.ravel().conj())) + print(f"approx MPS sv overlap: {sv_ovlp}") + \ No newline at end of file diff --git a/python/tests/cuquantum_tests/cutensornet_tests/test_circuit_converter.py b/python/tests/cuquantum_tests/cutensornet_tests/test_circuit_converter.py index 039674c..fe8b068 100644 --- a/python/tests/cuquantum_tests/cutensornet_tests/test_circuit_converter.py +++ b/python/tests/cuquantum_tests/cutensornet_tests/test_circuit_converter.py @@ -4,25 +4,65 @@ import pytest -from .circuit_utils import backends -from .circuit_utils import cirq_circuits, CirqTester -from .circuit_utils import qiskit_circuits, QiskitTester +import cupy as cp +from cuquantum import CircuitToEinsum + +from .circuit_utils import ApproximateMPSTester, backends, CircuitToEinsumTester +from .circuit_utils import cirq_circuits, cirq_circuits_mps +from .circuit_utils import qiskit_circuits, qiskit_circuits_mps +from .circuit_utils import GLOBAL_RNG, is_converter_mps_compatible +from .mps_utils import MPSConfig class TestCircuitToEinsum: # If PyTorch/Qiskit/Cirq is not installed, the corresponding tests are silently # skipped. - @pytest.mark.parametrize("circuit", cirq_circuits) + @pytest.mark.parametrize("circuit", cirq_circuits + qiskit_circuits) @pytest.mark.parametrize("dtype", ('complex64', 'complex128',)) @pytest.mark.parametrize("backend", backends) - def test_cirq(self, circuit, dtype, backend, nsample=3, nsite_max=3, nfix_max=3): - cirq_tests = CirqTester(circuit, dtype, backend, nsample, nsite_max, nfix_max) - cirq_tests.run_tests() + def test_circuit_converter(self, circuit, dtype, backend, nsample=3, nsite_max=3, nfix_max=3): + # Results from CircuitToEinsum are compared with Cirq/Qiskit + # If the backend is set to cupy, additional references below are also tested: + # 1. Tensor network simulation based on cutensornet state APIs if backend is cupy + # 2. State vector simulation based on cutensornet state APIs + # 3. Exact MPS simulation based on cutensornet state APIs if no mulit-qubit gates exist in the circuit + # 4. Exact MPS simulation based on a reference cupy implementation in `mps_utils.MPS` if no multi-qubit gates exist in the circuit + circuit_tests = CircuitToEinsumTester(circuit, dtype, backend, nsample, nsite_max, nfix_max) + circuit_tests.run_tests() + +class TestMPSStateAPIs: + + @pytest.mark.parametrize("circuit", cirq_circuits_mps + qiskit_circuits_mps) + def test_exact_mps(self, circuit, nsamples=3, nsite_max=3, nfix_max=3): + # Computation results from approaches below are compared: + # 1. Exact MPS simulation based on cutensornet state APIs if no mulit-qubit gates exist in the circuit + # 2. Exact MPS simulation based on a reference cupy-gesvd implementation + # Here we only perform exact MPS simulation for double precision as + # different SVD algorithms may lead to drastically different results for single precision circuits + converter = CircuitToEinsum(circuit, dtype="complex128", backend=cp) + n_qubits = converter.n_qubits + if not is_converter_mps_compatible(converter): + pytest.skip("MPS test skipped due to multi-qubit gate") + + mps_options = {'algorithm': GLOBAL_RNG.choice(('gesvd', 'gesvdr', 'gesvdp', 'gesvdj'))} + exact_mps_tests = ApproximateMPSTester(converter, nsamples, nsite_max, nfix_max, **mps_options) + exact_mps_tests.run_tests() - @pytest.mark.parametrize("circuit", qiskit_circuits) + @pytest.mark.parametrize("circuit", cirq_circuits_mps + qiskit_circuits_mps) @pytest.mark.parametrize("dtype", ('complex64', 'complex128',)) - @pytest.mark.parametrize("backend", backends) - def test_qiskit(self, circuit, dtype, backend, nsample=3, nsite_max=3, nfix_max=3): - qiskit_tests = QiskitTester(circuit, dtype, backend, nsample, nsite_max, nfix_max) - qiskit_tests.run_tests() + def test_approximate_mps(self, circuit, dtype, nsamples=3, nsite_max=3, nfix_max=3): + # Computation results from approaches below are compared: + # 1. Approximate MPS simulation based on cutensornet state APIs if no mulit-qubit gates exist in the circuit + # 2. Approximate MPS simulation based on a reference cupy implementation in `mps_utils.MPS` if no multi-qubit gates exist in the circuit + converter = CircuitToEinsum(circuit, dtype=dtype, backend=cp) + n_qubits = converter.n_qubits + if not is_converter_mps_compatible(converter): + pytest.skip("MPS test skipped due to multi-qubit gate") + + # test two different types of randomly generated MPS options + for _ in range(2): + # restrict to gesvd algorithm to avoid accuracy fallout + mps_options = MPSConfig.rand(n_qubits, GLOBAL_RNG, dtype, fixed={'algorithm': 'gesvd'}, dict_format=True) + approximate_mps_tests = ApproximateMPSTester(converter, nsamples, nsite_max, nfix_max, **mps_options) + approximate_mps_tests.run_tests() diff --git a/python/tests/cuquantum_tests/cutensornet_tests/test_contract.py b/python/tests/cuquantum_tests/cutensornet_tests/test_contract.py index 51f2efb..e0c75a7 100644 --- a/python/tests/cuquantum_tests/cutensornet_tests/test_contract.py +++ b/python/tests/cuquantum_tests/cutensornet_tests/test_contract.py @@ -18,18 +18,15 @@ from .test_utils import atol_mapper, EinsumFactory, rtol_mapper from .test_utils import compute_and_normalize_numpy_path from .test_utils import deselect_contract_tests +from .test_utils import deselect_gradient_tests from .test_utils import get_stream_for_backend from .test_utils import set_path_to_optimizer_options # TODO: parametrize compute type? -@pytest.mark.uncollect_if(func=deselect_contract_tests) @pytest.mark.parametrize( "use_numpy_path", (False, True) ) -@pytest.mark.parametrize( - "stream", (None, True) -) @pytest.mark.parametrize( "order", ("C", "F") ) @@ -42,11 +39,11 @@ @pytest.mark.parametrize( "einsum_expr_pack", einsum_expressions ) -class TestContract: +class _TestContractBase: def _test_runner( self, func, einsum_expr_pack, xp, dtype, order, - stream, use_numpy_path, **kwargs): + use_numpy_path, gradient, **kwargs): einsum_expr = copy.deepcopy(einsum_expr_pack) if isinstance(einsum_expr, list): einsum_expr, network_opts, optimizer_opts, _ = einsum_expr @@ -57,9 +54,20 @@ def _test_runner( factory = EinsumFactory(einsum_expr) operands = factory.generate_operands( factory.input_shapes, xp, dtype, order) + qualifiers, picks = factory.generate_qualifiers(xp, gradient) + factory.setup_torch_grads(xp, picks, operands) backend = sys.modules[infer_object_package(operands[0])] + stream = kwargs.get('stream') if stream: - stream = get_stream_for_backend(backend) + stream_obj = get_stream_for_backend(backend) + if stream == "as_int": + if backend is numpy or backend is cupy: + stream = stream_obj.ptr + else: + pytest.skip("we do not support torch operands + " + "a raw stream pointer") + else: + stream = stream_obj path = None if use_numpy_path: @@ -77,13 +85,49 @@ def _test_runner( if path is not None: optimizer_opts = set_path_to_optimizer_options( optimizer_opts, path) - out = func( - *data, options=network_opts, optimize=optimizer_opts, - stream=stream, return_info=return_info) + try: + out = func( + *data, options=network_opts, optimize=optimizer_opts, + stream=stream, return_info=return_info) + if stream: + stream_obj.synchronize() + except cutn.cuTensorNetError as e: + # differentiating some edge TNs is not yet supported + if "NOT_SUPPORTED" in str(e): + pytest.skip("this TN is currently not supported") + else: + raise + if return_info: out, (path, info) = out assert isinstance(path, list) assert isinstance(info, cuquantum.OptimizerInfo) + + if gradient: + # compute gradients + output_grad = backend.ones_like(out) + try: + out.backward(output_grad) + except cutn.cuTensorNetError as e: + # differentiating some edge TNs is not yet supported; + if "NOT_SUPPORTED" in str(e): + # we don't wanna skip because we can still verify + # contraction ouput + gradient = None + else: + raise + + if gradient: + input_grads = tuple(op.grad for op in operands) + + # check gradient result types + assert all((sys.modules[infer_object_package(grad)] is backend) + if grad is not None else True + for grad in input_grads) + assert all((grad.dtype == operands[0].dtype) + if grad is not None else True + for grad in input_grads) + else: # cuquantum.einsum() optimize = kwargs.pop('optimize') if optimize == 'path': @@ -97,33 +141,70 @@ def _test_runner( else: raise - if stream: - stream.synchronize() backend_out = sys.modules[infer_object_package(out)] assert backend_out is backend assert out.dtype == operands[0].dtype + # check contraction + factory.setup_torch_grads(xp, picks, operands) out_ref = opt_einsum.contract( *data, backend="torch" if "torch" in xp else xp) assert backend.allclose( out, out_ref, atol=atol_mapper[dtype], rtol=rtol_mapper[dtype]) - @pytest.mark.parametrize( - "return_info", (False, True) - ) + # check gradients + if gradient and func is cuquantum.contract: + out_ref.backward(output_grad) + + # check gradients + try: + is_close = backend.tensor(tuple( + backend.allclose( + cutn_grad, op.grad, + atol=atol_mapper[dtype], rtol=rtol_mapper[dtype]) + if cutn_grad is not None else cutn_grad is op.grad + for cutn_grad, op in zip(input_grads, operands) + )) + assert all(is_close) + except AssertionError as e: + # for easier debugging + print(tuple(op.shape for op in operands)) + print(input_grads) + print(tuple(op.grad for op in operands)) + raise + + +@pytest.mark.uncollect_if(func=(deselect_contract_tests, + deselect_gradient_tests)) +@pytest.mark.parametrize( + "gradient", (False, "random", "all") +) +@pytest.mark.parametrize( + "stream", (None, True, "as_int") +) +@pytest.mark.parametrize( + "return_info", (False, True) +) +class TestContract(_TestContractBase): + def test_contract( self, einsum_expr_pack, xp, dtype, order, - stream, use_numpy_path, return_info): + use_numpy_path, gradient, stream, return_info): self._test_runner( cuquantum.contract, einsum_expr_pack, xp, dtype, order, - stream, use_numpy_path, return_info=return_info) + use_numpy_path, gradient, stream=stream, return_info=return_info) + + +# einsum does not support gradient (at some point we should deprecate it...) +@pytest.mark.uncollect_if(func=deselect_contract_tests) +@pytest.mark.parametrize( + "optimize", (False, True, "path") +) +class TestEinsum(_TestContractBase): - @pytest.mark.parametrize( - "optimize", (False, True, "path") - ) def test_einsum( self, einsum_expr_pack, xp, dtype, order, - stream, use_numpy_path, optimize): + use_numpy_path, optimize): self._test_runner( cuquantum.einsum, einsum_expr_pack, xp, dtype, order, - stream, use_numpy_path, optimize=optimize) + use_numpy_path, None, optimize=optimize) diff --git a/python/tests/cuquantum_tests/cutensornet_tests/test_contract_path.py b/python/tests/cuquantum_tests/cutensornet_tests/test_contract_path.py index 59ec90f..1783ac6 100644 --- a/python/tests/cuquantum_tests/cutensornet_tests/test_contract_path.py +++ b/python/tests/cuquantum_tests/cutensornet_tests/test_contract_path.py @@ -62,7 +62,7 @@ def _test_runner( # sanity checks; the correctness checks are done in the contract() tests assert len(path) == len(operands)-1 - operand_ids = list(range(len(operands))) + operand_ids = list(range(len(operands))) if path else [-1] # handle single operand case. for i, j in path: op_i, op_j = operand_ids[i], operand_ids[j] operand_ids.remove(op_i) diff --git a/python/tests/cuquantum_tests/cutensornet_tests/test_cutensornet.py b/python/tests/cuquantum_tests/cutensornet_tests/test_cutensornet.py index b2e4291..03e6f76 100644 --- a/python/tests/cuquantum_tests/cutensornet_tests/test_cutensornet.py +++ b/python/tests/cuquantum_tests/cutensornet_tests/test_cutensornet.py @@ -155,10 +155,17 @@ class TestLibHelper: def test_get_version(self): ver = cutn.get_version() - assert ver == (cutn.MAJOR_VER * 10000 + major = ver // 10000 + minor = (ver % 10000) // 100 + + # run-time version must be compatible with build-time version + assert major == cutn.MAJOR_VER + assert minor >= cutn.MINOR_VER + + # sanity check (build-time versions should agree) + assert cutn.VERSION == (cutn.MAJOR_VER * 10000 + cutn.MINOR_VER * 100 + cutn.PATCH_VER) - assert ver == cutn.VERSION def test_get_cudart_version(self): # CUDA runtime is statically linked, so we can't compare @@ -734,8 +741,6 @@ def test_contraction_gradient_workflow( # compare gradients for grad_cutn, in_torch in zip(tn.gradients, inputs): grad_torch = in_torch.grad - if torch.is_complex(grad_torch): - grad_torch = grad_torch.conj().resolve_conj() # zero-copy if on GPU assert cp.allclose(grad_cutn, cp.asarray(grad_torch)) @@ -959,6 +964,7 @@ def test_tensor_qr(self): 'options': ( {}, # standard exact svd {'max_extent': 4, 'normalization':'L1', 'partition':'U', 'algorithm': 'gesvdr', 'gesvdr_niters': 40}, # fix extent truncation + {'abs_cutoff': 0.1, 'discarded_weight_cutoff': 0.05, 'normalization': 'L2'}, # discarded weight truncation {'abs_cutoff': 0.1, 'rel_cutoff': 0.1, 'algorithm': 'gesvdj', 'gesvdj_tol':1e-14, 'gesvdj_max_sweeps': 80}, # value based truncation {'abs_cutoff': 0.1, 'normalization':'L2', 'partition':'V', 'algorithm': 'gesvdj'}, # absolute value based truncation {'rel_cutoff': 0.1, 'normalization':'LInf', 'partition':'UV', 'algorithm': 'gesvdp'}, # relative value based truncation @@ -981,6 +987,13 @@ def test_tensor_svd(self): svd_config, svd_info = self.svd_config, self.svd_info dtype = cp.dtype(self.dtype) + # relax gesvdj_tol for single precision operand + algorithm = self.options.get('algorithm', None) + if algorithm == 'gesvdj' and self.dtype in [np.float32, np.complex64]: + gesvdj_tol = self.options.get('gesvdj_tol', None) + if gesvdj_tol is not None: + self.options['gesvdj_tol'] = 1e-7 + # parse svdConfig svd_method = check_or_create_options(tensor.SVDMethod, self.options, "SVDMethod") parse_svd_config(handle, svd_config, svd_method, logger=None) @@ -1075,6 +1088,7 @@ def test_tensor_svd(self): 'options': ( {}, # standard exact svd {'max_extent': 4, 'normalization':'L1', 'partition':'U', 'algorithm': 'gesvdr', 'gesvdr_niters': 40}, # fix extent truncation + {'abs_cutoff': 0.1, 'discarded_weight_cutoff': 0.05, 'normalization': 'L2'}, # discarded weight truncation {'abs_cutoff': 0.1, 'rel_cutoff': 0.1, 'algorithm': 'gesvdj', 'gesvdj_tol':1e-14, 'gesvdj_max_sweeps': 80}, # value based truncation {'abs_cutoff': 0.1, 'normalization':'L2', 'partition':'V', 'algorithm': 'gesvdj'}, # absolute value based truncation {'rel_cutoff': 0.1, 'normalization':'LInf', 'partition':'UV', 'algorithm': 'gesvdp'}, # relative value based truncation @@ -1101,6 +1115,13 @@ def test_gate_split(self): gate_algorithm = self.GATE_ALGO_MAP[algo] svd_config, svd_info = self.svd_config, self.svd_info + # relax gesvdj_tol for single precision operand + algorithm = self.options.get('algorithm', None) + if algorithm == 'gesvdj' and self.dtype in [np.float32, np.complex64]: + gesvdj_tol = self.options.get('gesvdj_tol', None) + if gesvdj_tol is not None: + self.options['gesvdj_tol'] = 1e-7 + # parse svdConfig svd_method = check_or_create_options(tensor.SVDMethod, self.options, "SVDMethod") parse_svd_config(handle, svd_config, svd_method, logger=None) diff --git a/python/tests/cuquantum_tests/cutensornet_tests/test_experimental.py b/python/tests/cuquantum_tests/cutensornet_tests/test_experimental.py index d0da2ec..d8f207d 100644 --- a/python/tests/cuquantum_tests/cutensornet_tests/test_experimental.py +++ b/python/tests/cuquantum_tests/cutensornet_tests/test_experimental.py @@ -19,7 +19,7 @@ from .approxTN_utils import split_contract_decompose, tensor_decompose, verify_split_QR, verify_split_SVD from .data import backend_names, contract_decompose_expr from .test_options import _OptionsBase -from .test_utils import DecomposeFactory, deselect_contract_decompose_algorithm_tests, deselect_decompose_tests, gen_rand_svd_method +from .test_utils import DecomposeFactory, deselect_contract_decompose_algorithm_tests, deselect_decompose_tests, get_svd_methods_for_test from .test_utils import get_stream_for_backend @@ -131,16 +131,14 @@ def test_contract_qr_decompose(self, decompose_expr, xp, dtype, order, stream): def test_contract_svd_decompose(self, decompose_expr, xp, dtype, order, stream): - rng = numpy.random.default_rng(2021) - methods = [tensor.SVDMethod()] + [gen_rand_svd_method(rng) for _ in range(10)] + methods = get_svd_methods_for_test(3, dtype) for svd_method in methods: algorithm = ContractDecomposeAlgorithm(qr_method=False, svd_method=svd_method) self._run_contract_decompose(decompose_expr, xp, dtype, order, stream, algorithm) def test_contract_qr_assisted_svd_decompose(self, decompose_expr, xp, dtype, order, stream): - rng = numpy.random.default_rng(2021) - methods = [tensor.SVDMethod()] + [gen_rand_svd_method(rng) for _ in range(10)] + methods = get_svd_methods_for_test(3, dtype) for svd_method in methods: algorithm = ContractDecomposeAlgorithm(qr_method={}, svd_method=svd_method) self._run_contract_decompose(decompose_expr, xp, dtype, order, stream, algorithm) diff --git a/python/tests/cuquantum_tests/cutensornet_tests/test_network.py b/python/tests/cuquantum_tests/cutensornet_tests/test_network.py index 4b40cbb..8619078 100644 --- a/python/tests/cuquantum_tests/cutensornet_tests/test_network.py +++ b/python/tests/cuquantum_tests/cutensornet_tests/test_network.py @@ -7,11 +7,12 @@ import re import sys -import cupy -import numpy -import opt_einsum +import cupy as cp +import numpy as np +import opt_einsum as oe import pytest +from cuquantum import cutensornet as cutn from cuquantum import Network from cuquantum.cutensornet._internal.utils import infer_object_package @@ -26,6 +27,9 @@ # TODO: parametrize compute type? @pytest.mark.uncollect_if(func=deselect_contract_tests) +@pytest.mark.parametrize( + "gradient", (False, "random", "all") +) @pytest.mark.parametrize( "use_numpy_path", (False, True) ) @@ -51,7 +55,7 @@ class TestNetwork: def test_network( self, einsum_expr_pack, xp, dtype, order, autotune, - stream, use_numpy_path): + stream, use_numpy_path, gradient): einsum_expr = copy.deepcopy(einsum_expr_pack) if isinstance(einsum_expr, list): einsum_expr, network_opts, optimizer_opts, _ = einsum_expr @@ -59,20 +63,23 @@ def test_network( network_opts = optimizer_opts = None assert isinstance(einsum_expr, (str, tuple)) + # prepare operands and other needed test config factory = EinsumFactory(einsum_expr) operands = factory.generate_operands( factory.input_shapes, xp, dtype, order) + qualifiers, picks = factory.generate_qualifiers(xp, gradient) + factory.setup_torch_grads(xp, picks, operands) backend = sys.modules[infer_object_package(operands[0])] data = factory.convert_by_format(operands) if stream: stream = get_stream_for_backend(backend) - tn = Network(*data, options=network_opts) + tn = Network(*data, options=network_opts, qualifiers=qualifiers) # We already test tn as a context manager in the samples, so let's test # explicitly calling tn.free() here. try: if not use_numpy_path: - path, info = tn.contract_path(optimize=optimizer_opts) + path, info = self._setup_path(tn, optimizer_opts) uninit_f_str = re.compile("{.*}") assert uninit_f_str.search(str(info)) is None check_intermediate_modes( @@ -89,7 +96,7 @@ def test_network( else: optimizer_opts = set_path_to_optimizer_options( optimizer_opts, path_ref) - path, _ = tn.contract_path(optimizer_opts) + path, _ = self._setup_path(tn, optimizer_opts) # round-trip test # note that within each pair it could have different order assert all(map(lambda x, y: sorted(x) == sorted(y), path, path_ref)) @@ -97,30 +104,107 @@ def test_network( if autotune: tn.autotune(iterations=autotune, stream=stream) # check the result - self._verify_contract( + out, out_ref = self._verify_contract( tn, operands, backend, data, xp, dtype, stream) + self._verify_gradient( + tn, operands, backend, data, xp, dtype, + gradient, out, out_ref, picks, stream) - # generate new data and bind them to the TN + # generate new data (by picking a nonzero seed) and bind them + # to the TN operands = factory.generate_operands( - factory.input_shapes, xp, dtype, order) + factory.input_shapes, xp, dtype, order, seed=100) + factory.setup_torch_grads(xp, picks, operands) data = factory.convert_by_format(operands) - tn.reset_operands(*operands) + tn.reset_operands(*operands, stream=stream) + # check the result - self._verify_contract( + out, out_ref = self._verify_contract( tn, operands, backend, data, xp, dtype, stream) + self._verify_gradient( + tn, operands, backend, data, xp, dtype, + gradient, out, out_ref, picks, stream) finally: tn.free() + def _setup_path(self, tn, optimizer_opts): + try: + path, info = tn.contract_path(optimize=optimizer_opts) + except cutn.cuTensorNetError as e: + # differentiating some edge TNs is not yet supported + if "NOT_SUPPORTED" in str(e): + pytest.skip("this TN is currently not supported") + else: + raise + return path, info + + def _setup_gradients(self, tn, output_grad, stream): + try: + input_grads = tn.gradients(output_grad, stream=stream) + except cutn.cuTensorNetError as e: + # differentiating some edge TNs is not yet supported + if "NOT_SUPPORTED" in str(e): + pytest.skip("this TN is currently not supported") + else: + raise + return input_grads + def _verify_contract( self, tn, operands, backend, data, xp, dtype, stream): out = tn.contract(stream=stream) if stream: stream.synchronize() - backend_out = sys.modules[infer_object_package(out)] - assert backend_out is backend + + # check contraction result types + assert sys.modules[infer_object_package(out)] is backend assert out.dtype == operands[0].dtype - out_ref = opt_einsum.contract( - *data, backend="torch" if "torch" in xp else xp) + # check contraction + out_ref = oe.contract(*data, backend=("torch" if "torch" in xp else xp)) assert backend.allclose( out, out_ref, atol=atol_mapper[dtype], rtol=rtol_mapper[dtype]) + + return out, out_ref + + def _verify_gradient( + self, tn, operands, backend, data, xp, dtype, + gradient, out, out_ref, picks, stream): + if gradient is False: + return + + # compute gradients + output_grad = backend.ones_like(out) + input_grads = self._setup_gradients(tn, output_grad, stream) + if stream: + stream.synchronize() + + # check gradient result types + assert all((sys.modules[infer_object_package(grad)] is backend) + if grad is not None else True + for grad in input_grads) + assert all((grad.dtype == operands[0].dtype) + if grad is not None else True + for grad in input_grads) + + # given simplicity & CI time constraints we only do grad + # verification with torch tensors + if "torch" in xp: + output_grad = backend.ones_like(out_ref) + out_ref.backward(output_grad) + + # check gradients + try: + is_close = backend.tensor(tuple( + backend.allclose( + cutn_grad, op.grad, + atol=atol_mapper[dtype], rtol=rtol_mapper[dtype]) + if cutn_grad is not None else cutn_grad is op.grad + for cutn_grad, op in zip(input_grads, operands) + )) + assert all(is_close) + except AssertionError as e: + # for easier debugging + print(tuple(op.shape for op in operands)) + print(input_grads) + print(tuple(op.grad for op in operands)) + raise diff --git a/python/tests/cuquantum_tests/cutensornet_tests/test_tensor.py b/python/tests/cuquantum_tests/cutensornet_tests/test_tensor.py index 502ebc6..7851e31 100644 --- a/python/tests/cuquantum_tests/cutensornet_tests/test_tensor.py +++ b/python/tests/cuquantum_tests/cutensornet_tests/test_tensor.py @@ -18,7 +18,7 @@ from .data import backend_names, tensor_decomp_expressions from .test_options import _OptionsBase, TestNetworkOptions from .test_utils import DecomposeFactory -from .test_utils import deselect_decompose_tests, gen_rand_svd_method +from .test_utils import deselect_decompose_tests, get_svd_methods_for_test from .test_utils import get_stream_for_backend @@ -107,8 +107,7 @@ def test_qr(self, decompose_expr, xp, dtype, order, stream, blocking): ) def test_svd( self, decompose_expr, xp, dtype, order, stream, return_info, blocking): - rng = numpy.random.default_rng(2021) - methods = [tensor.SVDMethod()] + [gen_rand_svd_method(rng) for _ in range(10)] + methods = get_svd_methods_for_test(3, dtype) for method in methods: self._run_decompose( decompose_expr, xp, dtype, order, stream, method, @@ -133,6 +132,9 @@ def test_abs_cutoff(self): def test_rel_cutoff(self): self.create_options({'rel_cutoff': 0.1}) + def test_discarded_weight_cutoff(self): + self.create_options({'discarded_weight_cutoff': 0.1}) + @pytest.mark.parametrize( 'partition', [None, 'U', 'V', 'UV'] ) diff --git a/python/tests/cuquantum_tests/cutensornet_tests/test_utils.py b/python/tests/cuquantum_tests/cutensornet_tests/test_utils.py index faaabcd..42b4f81 100644 --- a/python/tests/cuquantum_tests/cutensornet_tests/test_utils.py +++ b/python/tests/cuquantum_tests/cutensornet_tests/test_utils.py @@ -5,9 +5,9 @@ import re import sys -import cupy +import cupy as cp from cupy.testing import shaped_random -import numpy +import numpy as np try: import torch if not torch.cuda.is_available(): @@ -15,19 +15,21 @@ except ImportError: torch = None +from cuquantum import cutensornet as cutn from cuquantum import OptimizerOptions from cuquantum import tensor from cuquantum.cutensornet._internal.circuit_converter_utils import EINSUM_SYMBOLS_BASE +from cuquantum.cutensornet._internal.decomposition_utils import DECOMPOSITION_DTYPE_NAMES from cuquantum.cutensornet._internal.einsum_parser import infer_output_mode_labels from .data import dtype_names -machine_epsilon_values = [numpy.finfo(dtype).eps for dtype in dtype_names] +machine_epsilon_values = [np.finfo(dtype).eps for dtype in dtype_names] rtol_mapper = dict(zip( dtype_names, - [numpy.sqrt(m_eps) for m_eps in machine_epsilon_values] + [np.sqrt(m_eps) for m_eps in machine_epsilon_values] )) atol_mapper = dict(zip( @@ -50,7 +52,7 @@ def set_path_to_optimizer_options(optimizer_opts, path): def compute_and_normalize_numpy_path(data, num_operands): try: # this can fail if the TN is too large (ex: containing unicode) - path, _ = numpy.einsum_path(*data, optimize=True) + path, _ = np.einsum_path(*data, optimize=True) except: raise NotImplementedError path = path[1:] @@ -220,18 +222,18 @@ def input_modes(self): def output_modes(self): return self.modes[self.num_inputs:] - def generate_operands(self, shapes, xp, dtype, order): + def generate_operands(self, shapes, xp, dtype, order, seed=0): # we always generate data from shaped_random as CuPy fixes # the RNG seed for us if xp == "torch-cpu": - _xp = numpy + _xp = np elif xp == "torch-gpu": - _xp = cupy + _xp = cp else: _xp = sys.modules[xp] operands = [ - shaped_random(shape, xp=_xp, dtype=dtype, order=order) + shaped_random(shape, xp=_xp, dtype=dtype, order=order, seed=seed) for shape in shapes ] @@ -272,7 +274,7 @@ def convert_by_format(self, operands, *, dummy=False): if dummy: # create dummy NumPy arrays to bypass the __array_function__ # dispatcher, see numpy/numpy#21379 for discussion - operands = [numpy.broadcast_to(0, arr.shape) for arr in operands] + operands = [np.broadcast_to(0, arr.shape) for arr in operands] if self.expr_format == "subscript": data = [self.expr, *operands] @@ -283,6 +285,44 @@ def convert_by_format(self, operands, *, dummy=False): return data + def generate_qualifiers(self, xp, gradient): + if not gradient: + qualifiers = None + picks = None + elif gradient == "random": + # picks could be all false, and torch would not be happy during + # backprop + while True: + picks = np.random.choice(2, size=self.num_inputs) + if any(picks): + break + if "torch" in xp: + # for torch, test auto-detect, will set up torch operands later + qualifiers = None + else: + qualifiers = np.zeros( + self.num_inputs, dtype=cutn.tensor_qualifiers_dtype) + qualifiers[:]["requires_gradient"] = picks + elif gradient == "all": + # for torch, test overwrite + qualifiers = np.zeros( + self.num_inputs, dtype=cutn.tensor_qualifiers_dtype) + qualifiers[:]["requires_gradient"] = True + picks = tuple(True for i in range(self.num_inputs)) + + return qualifiers, picks + + def setup_torch_grads(self, xp, picks, operands): + if not "torch" in xp or picks is None: + return + + for op, pick in zip(operands, picks): + if pick: + op.requires_grad_(True) + else: + op.requires_grad_(False) + op.grad = None # reset + class DecomposeFactory(ExpressionFactory): @@ -314,26 +354,47 @@ def modes(self): return self._modes -def gen_rand_svd_method(rng): +def gen_rand_svd_method(rng, dtype, fixed=None): + assert dtype in DECOMPOSITION_DTYPE_NAMES, f"dtype {dtype} not supported" method = {"max_extent": rng.choice(range(1, 7)), "abs_cutoff": rng.random() / 2.0, # [0, 0.5) - "rel_cutoff": 0.1 + rng.random() / 2.5 , # [0.1, 0.5) + "rel_cutoff": 0.1 + rng.random() / 2.5, # [0.1, 0.5) "normalization": rng.choice([None, "L1", "L2", "LInf"]), "partition": rng.choice([None, "U", "V", "UV"]), "algorithm": rng.choice(['gesvd', 'gesvdj', 'gesvdp', 'gesvdr'])} - if method["algorithm"] == 'gesvdj': - method["gesvdj_tol"] = rng.choice([0, 1e-14]) + algorithm = method["algorithm"] + if fixed is not None and "algorithm" in fixed: + algorithm = fixed["algorithm"] + if algorithm != 'gesvdr': + # gesvdr + max_extent can't be used with discarded weight truncation + method["discarded_weight_cutoff"] = rng.random() / 10.0 # [0, 0.1) + if algorithm == 'gesvdj': + if dtype in ("float32", "complex64"): + # for single precision, lowered down gesvdj_tol for convergence + method["gesvdj_tol"] = rng.choice([0, 1e-7]) + else: + method["gesvdj_tol"] = rng.choice([0, 1e-14]) method["gesvdj_max_sweeps"] = rng.choice([0, 100]) - elif method["algorithm"] == 'gesvdr': + elif algorithm == 'gesvdr': method["gesvdr_niters"] = rng.choice([0, 40]) # we can't set oversampling as it depends on matrix size here + # updating method again in case svd_params are already + if fixed is not None: + method.update(fixed) return tensor.SVDMethod(**method) +def get_svd_methods_for_test(num, dtype): + # single dw cutoff to verify dw < dw_cutoff + methods = [tensor.SVDMethod(), tensor.SVDMethod(discarded_weight_cutoff=0.05)] + rng = np.random.default_rng(2021) + for _ in range(num): + methods.append(gen_rand_svd_method(rng, dtype)) + return methods # We want to avoid fragmenting the stream-ordered mempools _predefined_streams = { - numpy: cupy.cuda.Stream(), # implementation detail - cupy: cupy.cuda.Stream(), + np: cp.cuda.Stream(), # implementation detail + cp: cp.cuda.Stream(), } if torch is not None: _predefined_streams[torch] = torch.cuda.Stream() @@ -362,6 +423,18 @@ def deselect_contract_tests( return True return False +def deselect_gradient_tests( + einsum_expr_pack, xp, dtype, order, stream, + use_numpy_path, gradient, *args, **kwargs): + if xp.startswith('torch') and torch is None: + return True + assert gradient in (False, "random", "all", "skip") + if gradient == "skip": + return True + if gradient and "torch" not in xp: + return True + return False + def deselect_decompose_tests( decompose_expr, xp, dtype, *args, **kwargs): if xp.startswith('torch') and torch is None: diff --git a/python/tests/requirements.txt b/python/tests/requirements.txt index db5a3d6..51f63c5 100644 --- a/python/tests/requirements.txt +++ b/python/tests/requirements.txt @@ -2,7 +2,9 @@ pytest >=6.2 pytest-xdist opt_einsum cffi >=1.0.0 -nbmake ==1.3.0 +nbformat +nbconvert cirq >=0.6.0 qiskit >=0.24.0 +qiskit-aer pylatexenc diff --git a/python/tests/samples_tests/cutensornet_tests/test_cutensornet_samples.py b/python/tests/samples_tests/cutensornet_tests/test_cutensornet_samples.py index df4b0cb..84da894 100644 --- a/python/tests/samples_tests/cutensornet_tests/test_cutensornet_samples.py +++ b/python/tests/samples_tests/cutensornet_tests/test_cutensornet_samples.py @@ -7,10 +7,6 @@ import re import sys -try: - import nbmake -except ImportError: - nbmake = None # we could use packaging.version.Version too, but NumPy is our required # dependency, packaging is not. from numpy.lib import NumpyVersion as Version @@ -70,10 +66,6 @@ def test_sample(self, sample): notebook_files = glob.glob(samples_path+'/**/*.ipynb', recursive=True) -@pytest.mark.skipif( - nbmake is None, - reason="testing Jupyter notebooks requires nbmake" -) @pytest.mark.parametrize( 'notebook', notebook_files ) @@ -84,6 +76,4 @@ def test_notebook(self, notebook): if circuit_type in notebook_skip_messages: pytest.skip(notebook_skip_messages[circuit_type]) else: - status = pytest.main(['--nbmake', notebook]) - if status != 0: - raise cuQuantumSampleTestError(f'{notebook} failed') + run_sample(samples_path, notebook) diff --git a/python/tests/samples_tests/test_utils.py b/python/tests/samples_tests/test_utils.py index b8fa1ed..786a69d 100644 --- a/python/tests/samples_tests/test_utils.py +++ b/python/tests/samples_tests/test_utils.py @@ -2,9 +2,20 @@ # # SPDX-License-Identifier: BSD-3-Clause +import gc import os import sys +import cupy as cp +try: + import matplotlib +except ImportError: + matplotlib = None +else: + # disable plot windows from popping out when testing locally + matplotlib.use('Agg') +import nbformat +from nbconvert import PythonExporter import pytest @@ -12,19 +23,35 @@ class cuQuantumSampleTestError(Exception): pass +def parse_python_script(filepath): + if filepath.endswith('.py'): + with open(filepath, "r", encoding='utf-8') as f: + script = f.read() + elif filepath.endswith('.ipynb'): + # run all notebooks in the same process to avoid OOM & ABI issues + with open(filepath, "r", encoding="utf-8") as f: + nb = nbformat.reads(f.read(), nbformat.NO_CONVERT) + script = PythonExporter().from_notebook_node(nb)[0] + else: + raise ValueError(f"{filepath} not supported") + return script + + def run_sample(samples_path, filename): fullpath = os.path.join(samples_path, filename) - with open(fullpath, "r", encoding='utf-8') as f: - script = f.read() + script = parse_python_script(fullpath) try: old_argv = sys.argv sys.argv = [fullpath] exec(script, {}) except ImportError as e: - if 'torch' not in str(e): - raise + # for samples/notebooks requiring any of optional dependencies + for m in ('torch', 'qiskit', 'cirq'): + if f"No module named '{m}'" in str(e): + pytest.skip(f'{m} uninstalled, skipping related tests') + break else: - pytest.skip('PyTorch uninstalled, skipping related tests') + raise except Exception as e: msg = "\n" msg += f'Got error ({filename}):\n' @@ -32,3 +59,6 @@ def run_sample(samples_path, filename): raise cuQuantumSampleTestError(msg) from e finally: sys.argv = old_argv + # further reduce the memory watermark + gc.collect() + cp.get_default_memory_pool().free_all_blocks() diff --git a/samples/custatevec/CMakeLists.txt b/samples/custatevec/CMakeLists.txt index 520a447..2e07f89 100644 --- a/samples/custatevec/CMakeLists.txt +++ b/samples/custatevec/CMakeLists.txt @@ -145,3 +145,4 @@ add_custatevec_example(custatevec_examples "cuStateVec.example.swap_index_bits" add_custatevec_example(custatevec_examples "cuStateVec.example.mgpu_swap_index_bits" mgpu_swap_index_bits.cu) add_custatevec_example(custatevec_examples "cuStateVec.example.mgpu_sampler" mgpu_sampler.cu) add_custatevec_example(custatevec_examples "cuStateVec.example.mgpu_batch_measure" mgpu_batch_measure.cu) +add_custatevec_example(custatevec_examples "cuStateVec.example.subsv_migration" subsv_migration.cu) diff --git a/samples/custatevec/Makefile b/samples/custatevec/Makefile index ffc2649..067c279 100644 --- a/samples/custatevec/Makefile +++ b/samples/custatevec/Makefile @@ -41,6 +41,7 @@ all: check-env nvcc mgpu_swap_index_bits.cu -o mgpu_swap_index_bits ${CXX_FLAGS} nvcc mgpu_batch_measure.cu -o mgpu_batch_measure ${CXX_FLAGS} nvcc mgpu_sampler.cu -o mgpu_sampler ${CXX_FLAGS} + nvcc subsv_migration.cu -o subsv_migration ${CXX_FLAGS} check-env: @@ -77,4 +78,5 @@ clean: swap_index_bits \ mgpu_swap_index_bits \ mgpu_batch_measure \ - mgpu_sampler + mgpu_sampler \ + subsv_migration diff --git a/samples/custatevec/subsv_migration.cu b/samples/custatevec/subsv_migration.cu new file mode 100644 index 0000000..f7ce5ad --- /dev/null +++ b/samples/custatevec/subsv_migration.cu @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION & AFFILIATES. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include // cudaMalloc, cudaMemcpy, etc. +#include // cuDoubleComplex +#include // custatevecSubSVMigratorMigrate +#include // printf +#include // EXIT_FAILURE + +#include "helper.hpp" // HANDLE_ERROR, HANDLE_CUDA_ERROR + +int main(void) { + + const cudaDataType_t svDataType = CUDA_C_64F; + + const int nLocalIndexBits = 3; + const int64_t subSvSize = int64_t(1) << nLocalIndexBits; + + // allocate host memory + const int nSubSvs = 2; + cuDoubleComplex* subSvs[nSubSvs]; + const size_t subSvSizeInBytes = subSvSize * sizeof(cuDoubleComplex); + for (int iSv = 0; iSv < nSubSvs; iSv++) { + HANDLE_CUDA_ERROR( cudaMallocHost(&subSvs[iSv], subSvSizeInBytes) ); + } + + // fill subSvs[0] + for (int i = 0; i < subSvSize; i++) { + subSvs[0][i] = {0.25, 0.00}; + } + + // allocate device memory + const int nDeviceSlots = 1; + cuDoubleComplex* deviceSlots; + const size_t deviceSlotSizeInBytes = nDeviceSlots * subSvSize * sizeof(cuDoubleComplex); + HANDLE_CUDA_ERROR( cudaMalloc(&deviceSlots, deviceSlotSizeInBytes) ); + + //---------------------------------------------------------------------------------------------- + + // initialize custatevec handle + custatevecHandle_t handle; + HANDLE_ERROR( custatevecCreate(&handle) ); + + // create migrator + custatevecSubSVMigratorDescriptor_t migrator; + HANDLE_ERROR( custatevecSubSVMigratorCreate(handle, &migrator, deviceSlots, svDataType, + nDeviceSlots, nLocalIndexBits) ); + + int deviceSlotIndex = 0; + cuDoubleComplex* srcSubSv = subSvs[0]; + cuDoubleComplex* dstSubSv = subSvs[1]; + + // migrate subSvs[0] into d_subSvSlots + HANDLE_ERROR( custatevecSubSVMigratorMigrate(handle, migrator, deviceSlotIndex, srcSubSv, + nullptr, 0, subSvSize) ); + + // migrate d_subSvSlots into subSvs[1] + HANDLE_ERROR( custatevecSubSVMigratorMigrate(handle, migrator, deviceSlotIndex, nullptr, + dstSubSv, 0, subSvSize) ); + + // destroy migrator + HANDLE_ERROR( custatevecSubSVMigratorDestroy(handle, migrator)); + + // destroy custatevec handle + HANDLE_ERROR( custatevecDestroy(handle) ); + + //---------------------------------------------------------------------------------------------- + + // check if subSvs[1] has expected values + bool correct = true; + for (int i = 0; i < subSvSize; i++) { + cuDoubleComplex expectedValue = {0.25, 0.00}; + if (!almost_equal(subSvs[1][i], expectedValue)) { + correct = false; + break; + } + } + + // free host memory + for (int iSv = 0; iSv < nSubSvs; iSv++) { + HANDLE_CUDA_ERROR( cudaFreeHost(subSvs[iSv]) ); + } + + // free device memory + HANDLE_CUDA_ERROR( cudaFree(deviceSlots) ); + + if (correct) { + printf("subsv_migration example PASSED\n"); + return EXIT_SUCCESS; + } + else { + printf("subsv_migration example FAILED: wrong result\n"); + return EXIT_FAILURE; + } +} \ No newline at end of file diff --git a/samples/cutensornet/CMakeLists.txt b/samples/cutensornet/CMakeLists.txt index 1bee4d0..65653e8 100644 --- a/samples/cutensornet/CMakeLists.txt +++ b/samples/cutensornet/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES. +# Copyright (c) 2022-2023, NVIDIA CORPORATION & AFFILIATES. # # SPDX-License-Identifier: BSD-3-Clause @@ -6,6 +6,7 @@ cmake_minimum_required(VERSION 3.13.0 FATAL_ERROR) project(cutensornet_example LANGUAGES C CXX CUDA) include(GNUInstallDirs) +find_package(CUDA 11.0 REQUIRED) # ########################################## # cutensornet_example build mode @@ -92,13 +93,19 @@ function(add_cutensornet_example GROUP_TARGET EXAMPLE_NAME EXAMPLE_SOURCES) cutensornet $<$:MPI::MPI_CXX> ) - set_target_properties( - ${EXAMPLE_TARGET} - PROPERTIES - POSITION_INDEPENDENT_CODE ON - CUDA_ARCHITECTURES - "70;75;80" - ) + if((${CUDA_VERSION_MAJOR} GREATER_EQUAL 12) OR (${CUDA_VERSION_MAJOR} EQUAL 11 AND ${CUDA_VERSION_MINOR} GREATER_EQUAL 8)) + set_target_properties( + ${EXAMPLE_TARGET} + PROPERTIES + POSITION_INDEPENDENT_CODE ON + CUDA_ARCHITECTURES "70;75;80;86;90") + else() + set_target_properties( + ${EXAMPLE_TARGET} + PROPERTIES + POSITION_INDEPENDENT_CODE ON + CUDA_ARCHITECTURES "70;75;80;86") + endif() # Install example install( TARGETS ${EXAMPLE_TARGET} @@ -118,8 +125,14 @@ add_custom_target(cutensornet_examples) add_cutensornet_example(cutensornet_examples "cuTENSORNet.example.tensornet" tensornet_example.cu) add_cutensornet_example(cutensornet_examples "cuTENSORNet.example.tensornet.reuse" tensornet_example_reuse.cu) add_cutensornet_example(cutensornet_examples "cuTENSORNet.example.tensornet.gradients" tensornet_example_gradients.cu) +add_cutensornet_example(cutensornet_examples "cuTENSORNet.example.tensornet.amplitudes" high_level/amplitudes_example.cu) +add_cutensornet_example(cutensornet_examples "cuTENSORNet.example.tensornet.expectation" high_level/expectation_example.cu) add_cutensornet_example(cutensornet_examples "cuTENSORNet.example.tensornet.marginal" high_level/marginal_example.cu) add_cutensornet_example(cutensornet_examples "cuTENSORNet.example.tensornet.sampler" high_level/sampling_example.cu) +add_cutensornet_example(cutensornet_examples "cuTENSORNet.example.tensornet.mps_amplitudes" high_level/mps_amplitudes_example.cu) +add_cutensornet_example(cutensornet_examples "cuTENSORNet.example.tensornet.mps_expectation" high_level/mps_expectation_example.cu) +add_cutensornet_example(cutensornet_examples "cuTENSORNet.example.tensornet.mps_marginal" high_level/mps_marginal_example.cu) +add_cutensornet_example(cutensornet_examples "cuTENSORNet.example.tensornet.mps_sampler" high_level/mps_sampling_example.cu) add_cutensornet_example(cutensornet_examples "cuTENSORNet.example.tensornet.svd" approxTN/tensor_svd_example.cu) add_cutensornet_example(cutensornet_examples "cuTENSORNet.example.tensornet.qr" approxTN/tensor_qr_example.cu) add_cutensornet_example(cutensornet_examples "cuTENSORNet.example.tensornet.gate" approxTN/gate_split_example.cu) @@ -129,6 +142,7 @@ find_package(MPI) if (MPI_FOUND) add_cutensornet_example(cutensornet_examples "cuTENSORNet.example.tensornet.mpi" tensornet_example_mpi.cu) add_cutensornet_example(cutensornet_examples "cuTENSORNet.example.tensornet.mpi.auto" tensornet_example_mpi_auto.cu) + add_cutensornet_example(cutensornet_examples "cuTENSORNet.example.tensornet.mpi.sampling" high_level/sampling_mpi_example.cu) else () message(WARNING "An MPI installation was not detected. Please install CUDA-aware MPI if you would like to build the distributed example(s).") endif () diff --git a/samples/cutensornet/Makefile b/samples/cutensornet/Makefile index 3d7f7e7..2a4689f 100644 --- a/samples/cutensornet/Makefile +++ b/samples/cutensornet/Makefile @@ -1,4 +1,4 @@ -# Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES. +# Copyright (c) 2022-2023, NVIDIA CORPORATION & AFFILIATES. # # SPDX-License-Identifier: BSD-3-Clause @@ -28,11 +28,18 @@ all: check-env ${CUDA_PATH}/bin/nvcc approxTN/tensor_qr_example.cu -o tensor_qr_example ${CXX_FLAGS} ${CUDA_PATH}/bin/nvcc approxTN/gate_split_example.cu -o gate_split_example ${CXX_FLAGS} ${CUDA_PATH}/bin/nvcc approxTN/mps_example.cu -o mps_example ${CXX_FLAGS} + ${CUDA_PATH}/bin/nvcc high_level/amplitudes_example.cu -o amplitudes_example ${CXX_FLAGS} + ${CUDA_PATH}/bin/nvcc high_level/expectation_example.cu -o expectation_example ${CXX_FLAGS} ${CUDA_PATH}/bin/nvcc high_level/marginal_example.cu -o marginal_example ${CXX_FLAGS} ${CUDA_PATH}/bin/nvcc high_level/sampling_example.cu -o sampling_example ${CXX_FLAGS} + ${CUDA_PATH}/bin/nvcc high_level/mps_amplitudes_example.cu -o mps_amplitudes_example ${CXX_FLAGS} + ${CUDA_PATH}/bin/nvcc high_level/mps_expectation_example.cu -o mps_expectation_example ${CXX_FLAGS} + ${CUDA_PATH}/bin/nvcc high_level/mps_marginal_example.cu -o mps_marginal_example ${CXX_FLAGS} + ${CUDA_PATH}/bin/nvcc high_level/mps_sampling_example.cu -o mps_sampling_example ${CXX_FLAGS} ifdef MPI_ROOT ${CUDA_PATH}/bin/nvcc tensornet_example_mpi.cu -Xlinker -rpath,${MPI_ROOT}/lib -L${MPI_ROOT}/lib -o tensornet_example_mpi ${CXX_FLAGS} -lmpi ${CUDA_PATH}/bin/nvcc tensornet_example_mpi_auto.cu -Xlinker -rpath,${MPI_ROOT}/lib -L${MPI_ROOT}/lib -o tensornet_example_mpi_auto ${CXX_FLAGS} -lmpi + ${CUDA_PATH}/bin/nvcc high_level/sampling_mpi_example.cu -Xlinker -rpath,${MPI_ROOT}/lib -L${MPI_ROOT}/lib -o sampling_mpi_example ${CXX_FLAGS} -lmpi endif check-env: @@ -62,7 +69,14 @@ clean: rm -f tensor_qr_example tensor_qr_example.o rm -f gate_split_example gate_split_example.o rm -f mps_example mps_example.o + rm -f amplitudes_example amplitudes_example.o + rm -f expectation_example expectation_example.o rm -f marginal_example marginal_example.o rm -f sampling_example sampling_example.o + rm -f mps_amplitudes_example mps_amplitudes_example + rm -f mps_expectation_example mps_expectation_example + rm -f mps_marginal_example mps_marginal_example + rm -f mps_sampling_example mps_sampling_example rm -f tensornet_example_mpi tensornet_example_mpi.o rm -f tensornet_example_mpi_auto tensornet_example_mpi_auto.o + rm -f sampling_mpi_example sampling_mpi_example.o diff --git a/samples/cutensornet/high_level/amplitudes_example.cu b/samples/cutensornet/high_level/amplitudes_example.cu new file mode 100644 index 0000000..3af8147 --- /dev/null +++ b/samples/cutensornet/high_level/amplitudes_example.cu @@ -0,0 +1,204 @@ +/* Copyright (c) 2023, NVIDIA CORPORATION & AFFILIATES. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +// Sphinx: Amplitudes #1 + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + + +#define HANDLE_CUDA_ERROR(x) \ +{ const auto err = x; \ + if( err != cudaSuccess ) \ + { printf("CUDA error %s in line %d\n", cudaGetErrorString(err), __LINE__); fflush(stdout); std::abort(); } \ +}; + +#define HANDLE_CUTN_ERROR(x) \ +{ const auto err = x; \ + if( err != CUTENSORNET_STATUS_SUCCESS ) \ + { printf("cuTensorNet error %s in line %d\n", cutensornetGetErrorString(err), __LINE__); fflush(stdout); std::abort(); } \ +}; + + +int main(int argc, char **argv) +{ + static_assert(sizeof(size_t) == sizeof(int64_t), "Please build this sample on a 64-bit architecture!"); + + constexpr std::size_t fp64size = sizeof(double); + + // Sphinx: Amplitudes #2 + + // Quantum state configuration + constexpr int32_t numQubits = 6; // number of qubits + const std::vector qubitDims(numQubits,2); // qubit dimensions + const std::vector fixedModes({0,1}); // fixed modes in the output amplitude tensor (must be in acsending order) + const std::vector fixedValues({1,1}); // values of the fixed modes in the output amplitude tensor + const int32_t numFixedModes = fixedModes.size(); // number of fixed modes in the output amplitude tensor + std::cout << "Quantum circuit: " << numQubits << " qubits\n"; + + // Sphinx: Amplitudes #3 + + // Initialize the cuTensorNet library + HANDLE_CUDA_ERROR(cudaSetDevice(0)); + cutensornetHandle_t cutnHandle; + HANDLE_CUTN_ERROR(cutensornetCreate(&cutnHandle)); + std::cout << "Initialized cuTensorNet library on GPU 0\n"; + + // Sphinx: Amplitudes #4 + + // Define necessary quantum gate tensors in Host memory + const double invsq2 = 1.0 / std::sqrt(2.0); + // Hadamard gate + const std::vector> h_gateH {{invsq2, 0.0}, {invsq2, 0.0}, + {invsq2, 0.0}, {-invsq2, 0.0}}; + // CX gate + const std::vector> h_gateCX {{1.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, + {0.0, 0.0}, {1.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, + {0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {1.0, 0.0}, + {0.0, 0.0}, {0.0, 0.0}, {1.0, 0.0}, {0.0, 0.0}}; + + // Copy quantum gates to Device memory + void *d_gateH{nullptr}, *d_gateCX{nullptr}; + HANDLE_CUDA_ERROR(cudaMalloc(&d_gateH, 4 * (2 * fp64size))); + HANDLE_CUDA_ERROR(cudaMalloc(&d_gateCX, 16 * (2 * fp64size))); + std::cout << "Allocated quantum gate memory on GPU\n"; + HANDLE_CUDA_ERROR(cudaMemcpy(d_gateH, h_gateH.data(), 4 * (2 * fp64size), cudaMemcpyHostToDevice)); + HANDLE_CUDA_ERROR(cudaMemcpy(d_gateCX, h_gateCX.data(), 16 * (2 * fp64size), cudaMemcpyHostToDevice)); + std::cout << "Copied quantum gates to GPU memory\n"; + + // Sphinx: Amplitudes #5 + + // Allocate Device memory for the specified slice of the quantum circuit amplitudes tensor + void *d_amp{nullptr}; + std::size_t ampSize = 1; + for(const auto & qubitDim: qubitDims) ampSize *= qubitDim; // all state modes (full size) + for(const auto & fixedMode: fixedModes) ampSize /= qubitDims[fixedMode]; // fixed state modes reduce the slice size + HANDLE_CUDA_ERROR(cudaMalloc(&d_amp, ampSize * (2 * fp64size))); + std::cout << "Allocated memory for the specified slice of the quantum circuit amplitude tensor of size " + << ampSize << " elements\n"; + + // Sphinx: Amplitudes #6 + + // Query the free memory on Device + std::size_t freeSize{0}, totalSize{0}; + HANDLE_CUDA_ERROR(cudaMemGetInfo(&freeSize, &totalSize)); + const std::size_t scratchSize = (freeSize - (freeSize % 4096)) / 2; // use half of available memory with alignment + void *d_scratch{nullptr}; + HANDLE_CUDA_ERROR(cudaMalloc(&d_scratch, scratchSize)); + std::cout << "Allocated " << scratchSize << " bytes of scratch memory on GPU\n"; + + // Sphinx: Amplitudes #7 + + // Create the initial quantum state + cutensornetState_t quantumState; + HANDLE_CUTN_ERROR(cutensornetCreateState(cutnHandle, CUTENSORNET_STATE_PURITY_PURE, numQubits, qubitDims.data(), + CUDA_C_64F, &quantumState)); + std::cout << "Created the initial quantum state\n"; + + // Sphinx: Amplitudes #8 + + // Construct the final quantum circuit state (apply quantum gates) for the GHZ circuit + int64_t id; + HANDLE_CUTN_ERROR(cutensornetStateApplyTensor(cutnHandle, quantumState, 1, std::vector{{0}}.data(), + d_gateH, nullptr, 1, 0, 1, &id)); + for(int32_t i = 1; i < numQubits; ++i) { + HANDLE_CUTN_ERROR(cutensornetStateApplyTensor(cutnHandle, quantumState, 2, std::vector{{i-1,i}}.data(), + d_gateCX, nullptr, 1, 0, 1, &id)); + } + std::cout << "Applied quantum gates\n"; + + // Sphinx: Amplitudes #9 + + // Specify the quantum circuit amplitudes accessor + cutensornetStateAccessor_t accessor; + HANDLE_CUTN_ERROR(cutensornetCreateAccessor(cutnHandle, quantumState, numFixedModes, fixedModes.data(), + nullptr, &accessor)); // using default strides + std::cout << "Created the specified quantum circuit amplitudes accessor\n"; + + // Sphinx: Amplitudes #10 + + // Configure the computation of the slice of the specified quantum circuit amplitudes tensor + const int32_t numHyperSamples = 8; // desired number of hyper samples used in the tensor network contraction path finder + HANDLE_CUTN_ERROR(cutensornetAccessorConfigure(cutnHandle, accessor, + CUTENSORNET_ACCESSOR_OPT_NUM_HYPER_SAMPLES, &numHyperSamples, sizeof(numHyperSamples))); + + // Sphinx: Amplitudes #11 + + // Prepare the computation of the specified slice of the quantum circuit amplitudes tensor + cutensornetWorkspaceDescriptor_t workDesc; + HANDLE_CUTN_ERROR(cutensornetCreateWorkspaceDescriptor(cutnHandle, &workDesc)); + std::cout << "Created the workspace descriptor\n"; + HANDLE_CUTN_ERROR(cutensornetAccessorPrepare(cutnHandle, accessor, scratchSize, workDesc, 0x0)); + std::cout << "Prepared the computation of the specified slice of the quantum circuit amplitudes tensor\n"; + + // Sphinx: Amplitudes #12 + + // Attach the workspace buffer + int64_t worksize {0}; + HANDLE_CUTN_ERROR(cutensornetWorkspaceGetMemorySize(cutnHandle, + workDesc, + CUTENSORNET_WORKSIZE_PREF_RECOMMENDED, + CUTENSORNET_MEMSPACE_DEVICE, + CUTENSORNET_WORKSPACE_SCRATCH, + &worksize)); + std::cout << "Required scratch GPU workspace size (bytes) = " << worksize << std::endl; + if(worksize <= scratchSize) { + HANDLE_CUTN_ERROR(cutensornetWorkspaceSetMemory(cutnHandle, workDesc, CUTENSORNET_MEMSPACE_DEVICE, + CUTENSORNET_WORKSPACE_SCRATCH, d_scratch, worksize)); + }else{ + std::cout << "ERROR: Insufficient workspace size on Device!\n"; + std::abort(); + } + std::cout << "Set the workspace buffer\n"; + + // Sphinx: Amplitudes #13 + + // Compute the specified slice of the quantum circuit amplitudes tensor + std::complex stateNorm{0.0,0.0}; + HANDLE_CUTN_ERROR(cutensornetAccessorCompute(cutnHandle, accessor, fixedValues.data(), + workDesc, d_amp, static_cast(&stateNorm), 0x0)); + std::cout << "Computed the specified slice of the quantum circuit amplitudes tensor\n"; + std::vector> h_amp(ampSize); + HANDLE_CUDA_ERROR(cudaMemcpy(h_amp.data(), d_amp, ampSize * (2 * fp64size), cudaMemcpyDeviceToHost)); + std::cout << "Amplitudes slice for " << (numQubits - numFixedModes) << " qubits:\n"; + for(std::size_t i = 0; i < ampSize; ++i) { + std::cout << " " << h_amp[i] << std::endl; + } + std::cout << "State 2-norm = (" << stateNorm.real() << ", " << stateNorm.imag() << ")\n"; + + // Sphinx: Amplitudes #14 + + // Destroy the workspace descriptor + HANDLE_CUTN_ERROR(cutensornetDestroyWorkspaceDescriptor(workDesc)); + std::cout << "Destroyed the workspace descriptor\n"; + + // Destroy the quantum circuit amplitudes accessor + HANDLE_CUTN_ERROR(cutensornetDestroyAccessor(accessor)); + std::cout << "Destroyed the quantum circuit amplitudes accessor\n"; + + // Destroy the quantum circuit state + HANDLE_CUTN_ERROR(cutensornetDestroyState(quantumState)); + std::cout << "Destroyed the quantum circuit state\n"; + + HANDLE_CUDA_ERROR(cudaFree(d_scratch)); + HANDLE_CUDA_ERROR(cudaFree(d_amp)); + HANDLE_CUDA_ERROR(cudaFree(d_gateCX)); + HANDLE_CUDA_ERROR(cudaFree(d_gateH)); + std::cout << "Freed memory on GPU\n"; + + // Finalize the cuTensorNet library + HANDLE_CUTN_ERROR(cutensornetDestroy(cutnHandle)); + std::cout << "Finalized the cuTensorNet library\n"; + + return 0; +} diff --git a/samples/cutensornet/high_level/expectation_example.cu b/samples/cutensornet/high_level/expectation_example.cu new file mode 100644 index 0000000..e3c98af --- /dev/null +++ b/samples/cutensornet/high_level/expectation_example.cu @@ -0,0 +1,242 @@ +/* Copyright (c) 2023, NVIDIA CORPORATION & AFFILIATES. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +// Sphinx: Expectation #1 + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + + +#define HANDLE_CUDA_ERROR(x) \ +{ const auto err = x; \ + if( err != cudaSuccess ) \ + { printf("CUDA error %s in line %d\n", cudaGetErrorString(err), __LINE__); fflush(stdout); std::abort(); } \ +}; + +#define HANDLE_CUTN_ERROR(x) \ +{ const auto err = x; \ + if( err != CUTENSORNET_STATUS_SUCCESS ) \ + { printf("cuTensorNet error %s in line %d\n", cutensornetGetErrorString(err), __LINE__); fflush(stdout); std::abort(); } \ +}; + + +int main(int argc, char **argv) +{ + static_assert(sizeof(size_t) == sizeof(int64_t), "Please build this sample on a 64-bit architecture!"); + + constexpr std::size_t fp64size = sizeof(double); + + // Sphinx: Expectation #2 + + // Quantum state configuration + constexpr int32_t numQubits = 16; // number of qubits + const std::vector qubitDims(numQubits,2); // qubit dimensions + std::cout << "Quantum circuit: " << numQubits << " qubits\n"; + + // Sphinx: Expectation #3 + + // Initialize the cuTensorNet library + HANDLE_CUDA_ERROR(cudaSetDevice(0)); + cutensornetHandle_t cutnHandle; + HANDLE_CUTN_ERROR(cutensornetCreate(&cutnHandle)); + std::cout << "Initialized cuTensorNet library on GPU 0\n"; + + // Sphinx: Expectation #4 + + // Define necessary quantum gate tensors in Host memory + const double invsq2 = 1.0 / std::sqrt(2.0); + // Hadamard gate + const std::vector> h_gateH {{invsq2, 0.0}, {invsq2, 0.0}, + {invsq2, 0.0}, {-invsq2, 0.0}}; + // Pauli X gate + const std::vector> h_gateX {{0.0, 0.0}, {1.0, 0.0}, + {1.0, 0.0}, {0.0, 0.0}}; + // Pauli Y gate + const std::vector> h_gateY {{0.0, 0.0}, {0.0, -1.0}, + {0.0, 1.0}, {0.0, 0.0}}; + // Pauli Z gate + const std::vector> h_gateZ {{1.0, 0.0}, {0.0, 0.0}, + {0.0, 0.0}, {-1.0, 0.0}}; + // CX gate + const std::vector> h_gateCX {{1.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, + {0.0, 0.0}, {1.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, + {0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {1.0, 0.0}, + {0.0, 0.0}, {0.0, 0.0}, {1.0, 0.0}, {0.0, 0.0}}; + + // Copy quantum gates to Device memory + void *d_gateH{nullptr}, *d_gateX{nullptr}, *d_gateY{nullptr}, *d_gateZ{nullptr}, *d_gateCX{nullptr}; + HANDLE_CUDA_ERROR(cudaMalloc(&d_gateH, 4 * (2 * fp64size))); + HANDLE_CUDA_ERROR(cudaMalloc(&d_gateX, 4 * (2 * fp64size))); + HANDLE_CUDA_ERROR(cudaMalloc(&d_gateY, 4 * (2 * fp64size))); + HANDLE_CUDA_ERROR(cudaMalloc(&d_gateZ, 4 * (2 * fp64size))); + HANDLE_CUDA_ERROR(cudaMalloc(&d_gateCX, 16 * (2 * fp64size))); + std::cout << "Allocated quantum gate memory on GPU\n"; + HANDLE_CUDA_ERROR(cudaMemcpy(d_gateH, h_gateH.data(), 4 * (2 * fp64size), cudaMemcpyHostToDevice)); + HANDLE_CUDA_ERROR(cudaMemcpy(d_gateX, h_gateX.data(), 4 * (2 * fp64size), cudaMemcpyHostToDevice)); + HANDLE_CUDA_ERROR(cudaMemcpy(d_gateY, h_gateY.data(), 4 * (2 * fp64size), cudaMemcpyHostToDevice)); + HANDLE_CUDA_ERROR(cudaMemcpy(d_gateZ, h_gateZ.data(), 4 * (2 * fp64size), cudaMemcpyHostToDevice)); + HANDLE_CUDA_ERROR(cudaMemcpy(d_gateCX, h_gateCX.data(), 16 * (2 * fp64size), cudaMemcpyHostToDevice)); + std::cout << "Copied quantum gates to GPU memory\n"; + + // Sphinx: Expectation #5 + + // Query the free memory on Device + std::size_t freeSize{0}, totalSize{0}; + HANDLE_CUDA_ERROR(cudaMemGetInfo(&freeSize, &totalSize)); + const std::size_t scratchSize = (freeSize - (freeSize % 4096)) / 2; // use half of available memory with alignment + void *d_scratch{nullptr}; + HANDLE_CUDA_ERROR(cudaMalloc(&d_scratch, scratchSize)); + std::cout << "Allocated " << scratchSize << " bytes of scratch memory on GPU\n"; + + // Sphinx: Expectation #6 + + // Create the initial quantum state + cutensornetState_t quantumState; + HANDLE_CUTN_ERROR(cutensornetCreateState(cutnHandle, CUTENSORNET_STATE_PURITY_PURE, numQubits, qubitDims.data(), + CUDA_C_64F, &quantumState)); + std::cout << "Created the initial quantum state\n"; + + // Sphinx: Expectation #7 + + // Construct the final quantum circuit state (apply quantum gates) for the GHZ circuit + int64_t id; + HANDLE_CUTN_ERROR(cutensornetStateApplyTensor(cutnHandle, quantumState, 1, std::vector{{0}}.data(), + d_gateH, nullptr, 1, 0, 1, &id)); + for(int32_t i = 1; i < numQubits; ++i) { + HANDLE_CUTN_ERROR(cutensornetStateApplyTensor(cutnHandle, quantumState, 2, std::vector{{i-1,i}}.data(), + d_gateCX, nullptr, 1, 0, 1, &id)); + } + std::cout << "Applied quantum gates\n"; + + // Sphinx: Expectation #8 + + // Create an empty tensor network operator + cutensornetNetworkOperator_t hamiltonian; + HANDLE_CUTN_ERROR(cutensornetCreateNetworkOperator(cutnHandle, numQubits, qubitDims.data(), CUDA_C_64F, &hamiltonian)); + // Append component (0.5 * Z1 * Z2) to the tensor network operator + { + const int32_t numModes[] = {1, 1}; // Z1 acts on 1 mode, Z2 acts on 1 mode + const int32_t modesZ1[] = {1}; // state modes Z1 acts on + const int32_t modesZ2[] = {2}; // state modes Z2 acts on + const int32_t * stateModes[] = {modesZ1, modesZ2}; // state modes (Z1 * Z2) acts on + const void * gateData[] = {d_gateZ, d_gateZ}; // GPU pointers to gate data + HANDLE_CUTN_ERROR(cutensornetNetworkOperatorAppendProduct(cutnHandle, hamiltonian, cuDoubleComplex{0.5,0.0}, + 2, numModes, stateModes, NULL, gateData, &id)); + } + // Append component (0.25 * Y3) to the tensor network operator + { + const int32_t numModes[] = {1}; // Y3 acts on 1 mode + const int32_t modesY3[] = {3}; // state modes Y3 acts on + const int32_t * stateModes[] = {modesY3}; // state modes (Y3) acts on + const void * gateData[] = {d_gateY}; // GPU pointers to gate data + HANDLE_CUTN_ERROR(cutensornetNetworkOperatorAppendProduct(cutnHandle, hamiltonian, cuDoubleComplex{0.25,0.0}, + 1, numModes, stateModes, NULL, gateData, &id)); + } + // Append component (0.13 * Y0 X2 Z3) to the tensor network operator + { + const int32_t numModes[] = {1, 1, 1}; // Y0 acts on 1 mode, X2 acts on 1 mode, Z3 acts on 1 mode + const int32_t modesY0[] = {0}; // state modes Y0 acts on + const int32_t modesX2[] = {2}; // state modes X2 acts on + const int32_t modesZ3[] = {3}; // state modes Z3 acts on + const int32_t * stateModes[] = {modesY0, modesX2, modesZ3}; // state modes (Y0 * X2 * Z3) acts on + const void * gateData[] = {d_gateY, d_gateX, d_gateZ}; // GPU pointers to gate data + HANDLE_CUTN_ERROR(cutensornetNetworkOperatorAppendProduct(cutnHandle, hamiltonian, cuDoubleComplex{0.13,0.0}, + 3, numModes, stateModes, NULL, gateData, &id)); + } + std::cout << "Constructed a tensor network operator: (0.5 * Z1 * Z2) + (0.25 * Y3) + (0.13 * Y0 * X2 * Z3)" << std::endl; + + // Sphinx: Expectation #9 + + // Specify the quantum circuit expectation value + cutensornetStateExpectation_t expectation; + HANDLE_CUTN_ERROR(cutensornetCreateExpectation(cutnHandle, quantumState, hamiltonian, &expectation)); + std::cout << "Created the specified quantum circuit expectation value\n"; + + // Sphinx: Expectation #10 + + // Configure the computation of the specified quantum circuit expectation value + const int32_t numHyperSamples = 8; // desired number of hyper samples used in the tensor network contraction path finder + HANDLE_CUTN_ERROR(cutensornetExpectationConfigure(cutnHandle, expectation, + CUTENSORNET_EXPECTATION_OPT_NUM_HYPER_SAMPLES, &numHyperSamples, sizeof(numHyperSamples))); + + // Sphinx: Expectation #11 + + // Prepare the specified quantum circuit expectation value for computation + cutensornetWorkspaceDescriptor_t workDesc; + HANDLE_CUTN_ERROR(cutensornetCreateWorkspaceDescriptor(cutnHandle, &workDesc)); + std::cout << "Created the workspace descriptor\n"; + HANDLE_CUTN_ERROR(cutensornetExpectationPrepare(cutnHandle, expectation, scratchSize, workDesc, 0x0)); + std::cout << "Prepared the specified quantum circuit expectation value\n"; + + // Sphinx: Expectation #12 + + // Attach the workspace buffer + int64_t worksize {0}; + HANDLE_CUTN_ERROR(cutensornetWorkspaceGetMemorySize(cutnHandle, + workDesc, + CUTENSORNET_WORKSIZE_PREF_RECOMMENDED, + CUTENSORNET_MEMSPACE_DEVICE, + CUTENSORNET_WORKSPACE_SCRATCH, + &worksize)); + std::cout << "Required scratch GPU workspace size (bytes) = " << worksize << std::endl; + if(worksize <= scratchSize) { + HANDLE_CUTN_ERROR(cutensornetWorkspaceSetMemory(cutnHandle, workDesc, CUTENSORNET_MEMSPACE_DEVICE, + CUTENSORNET_WORKSPACE_SCRATCH, d_scratch, worksize)); + }else{ + std::cout << "ERROR: Insufficient workspace size on Device!\n"; + std::abort(); + } + std::cout << "Set the workspace buffer\n"; + + // Sphinx: Expectation #13 + + // Compute the specified quantum circuit expectation value + std::complex expectVal{0.0,0.0}, stateNorm{0.0,0.0}; + HANDLE_CUTN_ERROR(cutensornetExpectationCompute(cutnHandle, expectation, workDesc, + static_cast(&expectVal), static_cast(&stateNorm), 0x0)); + std::cout << "Computed the specified quantum circuit expectation value\n"; + std::cout << "Expectation value = (" << expectVal.real() << ", " << expectVal.imag() << ")\n"; + std::cout << "State 2-norm = (" << stateNorm.real() << ", " << stateNorm.imag() << ")\n"; + + // Sphinx: Expectation #14 + + // Destroy the workspace descriptor + HANDLE_CUTN_ERROR(cutensornetDestroyWorkspaceDescriptor(workDesc)); + std::cout << "Destroyed the workspace descriptor\n"; + + // Destroy the quantum circuit expectation value + HANDLE_CUTN_ERROR(cutensornetDestroyExpectation(expectation)); + std::cout << "Destroyed the quantum circuit state expectation value\n"; + + // Destroy the tensor network operator + HANDLE_CUTN_ERROR(cutensornetDestroyNetworkOperator(hamiltonian)); + std::cout << "Destroyed the tensor network operator\n"; + + // Destroy the quantum circuit state + HANDLE_CUTN_ERROR(cutensornetDestroyState(quantumState)); + std::cout << "Destroyed the quantum circuit state\n"; + + HANDLE_CUDA_ERROR(cudaFree(d_scratch)); + HANDLE_CUDA_ERROR(cudaFree(d_gateCX)); + HANDLE_CUDA_ERROR(cudaFree(d_gateZ)); + HANDLE_CUDA_ERROR(cudaFree(d_gateY)); + HANDLE_CUDA_ERROR(cudaFree(d_gateX)); + HANDLE_CUDA_ERROR(cudaFree(d_gateH)); + std::cout << "Freed memory on GPU\n"; + + // Finalize the cuTensorNet library + HANDLE_CUTN_ERROR(cutensornetDestroy(cutnHandle)); + std::cout << "Finalized the cuTensorNet library\n"; + + return 0; +} diff --git a/samples/cutensornet/high_level/marginal_example.cu b/samples/cutensornet/high_level/marginal_example.cu index 8de8720..a5ebc1f 100644 --- a/samples/cutensornet/high_level/marginal_example.cu +++ b/samples/cutensornet/high_level/marginal_example.cu @@ -32,15 +32,17 @@ int main(int argc, char **argv) { + static_assert(sizeof(size_t) == sizeof(int64_t), "Please build this sample on a 64-bit architecture!"); + constexpr std::size_t fp64size = sizeof(double); // Sphinx: Marginal #2 // Quantum state configuration - constexpr int32_t numQubits = 16; + constexpr int32_t numQubits = 16; // number of qubits const std::vector qubitDims(numQubits,2); // qubit dimensions - constexpr int32_t numMarginalModes = 2; // rank of the marginal (reduced density matrix) - const std::vector marginalModes({0,1}); // open qubits (must be in acsending order) + const std::vector marginalModes({0,1}); // open qubits defining the marginal (must be in acsending order) + const int32_t numMarginalModes = marginalModes.size(); // rank of the marginal (reduced density matrix) std::cout << "Quantum circuit: " << numQubits << " qubits\n"; // Sphinx: Marginal #3 @@ -75,12 +77,14 @@ int main(int argc, char **argv) // Sphinx: Marginal #5 - // Allocate the specified quantum circuit reduced density matrix (marginal) in Device memory + // Allocate Device memory for the specified quantum circuit reduced density matrix (marginal) void *d_rdm{nullptr}; std::size_t rdmDim = 1; for(const auto & mode: marginalModes) rdmDim *= qubitDims[mode]; const std::size_t rdmSize = rdmDim * rdmDim; HANDLE_CUDA_ERROR(cudaMalloc(&d_rdm, rdmSize * (2 * fp64size))); + std::cout << "Allocated memory for the specified quantum circuit reduced density matrix (marginal) of size " + << rdmSize << " elements\n"; // Sphinx: Marginal #6 @@ -114,7 +118,7 @@ int main(int argc, char **argv) // Sphinx: Marginal #9 - // Specify the desired reduced density matrix (marginal) + // Specify the quantum circuit reduced density matrix (marginal) cutensornetStateMarginal_t marginal; HANDLE_CUTN_ERROR(cutensornetCreateMarginal(cutnHandle, quantumState, numMarginalModes, marginalModes.data(), 0, nullptr, std::vector{{1,2,4,8}}.data(), &marginal)); // using explicit strides @@ -129,12 +133,12 @@ int main(int argc, char **argv) // Sphinx: Marginal #11 - // Prepare the specified quantum circuit reduced densitry matrix (marginal) + // Prepare the computation of the specified quantum circuit reduced densitry matrix (marginal) cutensornetWorkspaceDescriptor_t workDesc; HANDLE_CUTN_ERROR(cutensornetCreateWorkspaceDescriptor(cutnHandle, &workDesc)); std::cout << "Created the workspace descriptor\n"; HANDLE_CUTN_ERROR(cutensornetMarginalPrepare(cutnHandle, marginal, scratchSize, workDesc, 0x0)); - std::cout << "Prepared the specified quantum circuit reduced density matrix (marginal)\n"; + std::cout << "Prepared the computation of the specified quantum circuit reduced density matrix (marginal)\n"; // Sphinx: Marginal #12 @@ -159,7 +163,7 @@ int main(int argc, char **argv) // Sphinx: Marginal #13 // Compute the specified quantum circuit reduced densitry matrix (marginal) - HANDLE_CUTN_ERROR(cutensornetMarginalCompute(cutnHandle, marginal, nullptr, workDesc, d_rdm, 0)); + HANDLE_CUTN_ERROR(cutensornetMarginalCompute(cutnHandle, marginal, nullptr, workDesc, d_rdm, 0x0)); std::cout << "Computed the specified quantum circuit reduced density matrix (marginal)\n"; std::vector> h_rdm(rdmSize); HANDLE_CUDA_ERROR(cudaMemcpy(h_rdm.data(), d_rdm, rdmSize * (2 * fp64size), cudaMemcpyDeviceToHost)); diff --git a/samples/cutensornet/high_level/mps_amplitudes_example.cu b/samples/cutensornet/high_level/mps_amplitudes_example.cu new file mode 100644 index 0000000..9125bef --- /dev/null +++ b/samples/cutensornet/high_level/mps_amplitudes_example.cu @@ -0,0 +1,272 @@ +/* Copyright (c) 2023, NVIDIA CORPORATION & AFFILIATES. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +// Sphinx: MPS Amplitudes #1 + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + + +#define HANDLE_CUDA_ERROR(x) \ +{ const auto err = x; \ + if( err != cudaSuccess ) \ + { printf("CUDA error %s in line %d\n", cudaGetErrorString(err), __LINE__); fflush(stdout); std::abort(); } \ +}; + +#define HANDLE_CUTN_ERROR(x) \ +{ const auto err = x; \ + if( err != CUTENSORNET_STATUS_SUCCESS ) \ + { printf("cuTensorNet error %s in line %d\n", cutensornetGetErrorString(err), __LINE__); fflush(stdout); std::abort(); } \ +}; + + +int main(int argc, char **argv) +{ + static_assert(sizeof(size_t) == sizeof(int64_t), "Please build this sample on a 64-bit architecture!"); + + constexpr std::size_t fp64size = sizeof(double); + + // Sphinx: MPS Amplitudes #2 + + // Quantum state configuration + constexpr int32_t numQubits = 6; // number of qubits + const std::vector qubitDims(numQubits,2); // qubit dimensions + const std::vector fixedModes({0,1}); // fixed modes in the output amplitude tensor (must be in acsending order) + const std::vector fixedValues({1,1}); // values of the fixed modes in the output amplitude tensor + const int32_t numFixedModes = fixedModes.size(); // number of fixed modes in the output amplitude tensor + std::cout << "Quantum circuit: " << numQubits << " qubits\n"; + + // Sphinx: MPS Amplitudes #3 + + // Initialize the cuTensorNet library + HANDLE_CUDA_ERROR(cudaSetDevice(0)); + cutensornetHandle_t cutnHandle; + HANDLE_CUTN_ERROR(cutensornetCreate(&cutnHandle)); + std::cout << "Initialized cuTensorNet library on GPU 0\n"; + + // Sphinx: MPS Amplitudes #4 + + // Define necessary quantum gate tensors in Host memory + const double invsq2 = 1.0 / std::sqrt(2.0); + // Hadamard gate + const std::vector> h_gateH {{invsq2, 0.0}, {invsq2, 0.0}, + {invsq2, 0.0}, {-invsq2, 0.0}}; + // CX gate + const std::vector> h_gateCX {{1.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, + {0.0, 0.0}, {1.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, + {0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {1.0, 0.0}, + {0.0, 0.0}, {0.0, 0.0}, {1.0, 0.0}, {0.0, 0.0}}; + + // Copy quantum gates to Device memory + void *d_gateH{nullptr}, *d_gateCX{nullptr}; + HANDLE_CUDA_ERROR(cudaMalloc(&d_gateH, 4 * (2 * fp64size))); + HANDLE_CUDA_ERROR(cudaMalloc(&d_gateCX, 16 * (2 * fp64size))); + std::cout << "Allocated quantum gate memory on GPU\n"; + HANDLE_CUDA_ERROR(cudaMemcpy(d_gateH, h_gateH.data(), 4 * (2 * fp64size), cudaMemcpyHostToDevice)); + HANDLE_CUDA_ERROR(cudaMemcpy(d_gateCX, h_gateCX.data(), 16 * (2 * fp64size), cudaMemcpyHostToDevice)); + std::cout << "Copied quantum gates to GPU memory\n"; + + // Sphinx: MPS Amplitudes #5 + + // Determine the MPS representation and allocate buffers for the MPS tensors + const int64_t maxExtent = 2; // GHZ state can be exactly represented with max bond dimension of 2 + std::vector> extents; + std::vector extentsPtr(numQubits); + std::vector d_mpsTensors(numQubits, nullptr); + for (int32_t i = 0; i < numQubits; i++) { + if (i == 0) { // left boundary MPS tensor + extents.push_back({2, maxExtent}); + HANDLE_CUDA_ERROR(cudaMalloc(&d_mpsTensors[i], 2 * maxExtent * 2 * fp64size)); + } + else if (i == numQubits-1) { // right boundary MPS tensor + extents.push_back({maxExtent, 2}); + HANDLE_CUDA_ERROR(cudaMalloc(&d_mpsTensors[i], 2 * maxExtent * 2 * fp64size)); + } + else { // middle MPS tensors + extents.push_back({maxExtent, 2, maxExtent}); + HANDLE_CUDA_ERROR(cudaMalloc(&d_mpsTensors[i], 2 * maxExtent * maxExtent * 2 * fp64size)); + } + extentsPtr[i] = extents[i].data(); + } + + // Sphinx: MPS Amplitudes #6 + + // Allocate Device memory for the specified slice of the quantum circuit amplitudes tensor + void *d_amp{nullptr}; + std::size_t ampSize = 1; + for(const auto & qubitDim: qubitDims) ampSize *= qubitDim; // all state modes (full size) + for(const auto & fixedMode: fixedModes) ampSize /= qubitDims[fixedMode]; // fixed state modes reduce the slice size + HANDLE_CUDA_ERROR(cudaMalloc(&d_amp, ampSize * (2 * fp64size))); + std::cout << "Allocated memory for the specified slice of the quantum circuit amplitude tensor of size " + << ampSize << " elements\n"; + + // Sphinx: MPS Amplitudes #7 + + // Query the free memory on Device + std::size_t freeSize{0}, totalSize{0}; + HANDLE_CUDA_ERROR(cudaMemGetInfo(&freeSize, &totalSize)); + const std::size_t scratchSize = (freeSize - (freeSize % 4096)) / 2; // use half of available memory with alignment + void *d_scratch{nullptr}; + HANDLE_CUDA_ERROR(cudaMalloc(&d_scratch, scratchSize)); + std::cout << "Allocated " << scratchSize << " bytes of scratch memory on GPU\n"; + + // Sphinx: MPS Amplitudes #8 + + // Create the initial quantum state + cutensornetState_t quantumState; + HANDLE_CUTN_ERROR(cutensornetCreateState(cutnHandle, CUTENSORNET_STATE_PURITY_PURE, numQubits, qubitDims.data(), + CUDA_C_64F, &quantumState)); + std::cout << "Created the initial quantum state\n"; + + // Sphinx: MPS Amplitudes #9 + + // Construct the final quantum circuit state (apply quantum gates) for the GHZ circuit + int64_t id; + HANDLE_CUTN_ERROR(cutensornetStateApplyTensor(cutnHandle, quantumState, 1, std::vector{{0}}.data(), + d_gateH, nullptr, 1, 0, 1, &id)); + for(int32_t i = 1; i < numQubits; ++i) { + HANDLE_CUTN_ERROR(cutensornetStateApplyTensor(cutnHandle, quantumState, 2, std::vector{{i-1,i}}.data(), + d_gateCX, nullptr, 1, 0, 1, &id)); + } + std::cout << "Applied quantum gates\n"; + + // Sphinx: MPS Amplitudes #10 + + // Specify the final target MPS representation (use default fortran strides) + HANDLE_CUTN_ERROR(cutensornetStateFinalizeMPS(cutnHandle, quantumState, + CUTENSORNET_BOUNDARY_CONDITION_OPEN, extentsPtr.data(), /*strides=*/nullptr)); + std::cout << "Requested the final MPS factorization of the quantum circuit state\n"; + + // Sphinx: MPS Amplitudes #11 + + // Optional, set up the SVD method for MPS truncation. + cutensornetTensorSVDAlgo_t algo = CUTENSORNET_TENSOR_SVD_ALGO_GESVDJ; + HANDLE_CUTN_ERROR(cutensornetStateConfigure(cutnHandle, quantumState, + CUTENSORNET_STATE_MPS_SVD_CONFIG_ALGO, &algo, sizeof(algo))); + std::cout << "Configured the MPS factorization computation\n"; + + // Sphinx: MPS Amplitudes #12 + + // Prepare the MPS computation and attach workspace + cutensornetWorkspaceDescriptor_t workDesc; + HANDLE_CUTN_ERROR(cutensornetCreateWorkspaceDescriptor(cutnHandle, &workDesc)); + std::cout << "Created the workspace descriptor\n"; + HANDLE_CUTN_ERROR(cutensornetStatePrepare(cutnHandle, quantumState, scratchSize, workDesc, 0x0)); + int64_t worksize {0}; + HANDLE_CUTN_ERROR(cutensornetWorkspaceGetMemorySize(cutnHandle, + workDesc, + CUTENSORNET_WORKSIZE_PREF_RECOMMENDED, + CUTENSORNET_MEMSPACE_DEVICE, + CUTENSORNET_WORKSPACE_SCRATCH, + &worksize)); + std::cout << "Scratch GPU workspace size (bytes) for MPS computation = " << worksize << std::endl; + if(worksize <= scratchSize) { + HANDLE_CUTN_ERROR(cutensornetWorkspaceSetMemory(cutnHandle, workDesc, CUTENSORNET_MEMSPACE_DEVICE, + CUTENSORNET_WORKSPACE_SCRATCH, d_scratch, worksize)); + }else{ + std::cout << "ERROR: Insufficient workspace size on Device!\n"; + std::abort(); + } + std::cout << "Set the workspace buffer for the MPS factorization computation\n"; + + // Sphinx: MPS Amplitudes #13 + + // Execute MPS computation + HANDLE_CUTN_ERROR(cutensornetStateCompute(cutnHandle, quantumState, + workDesc, extentsPtr.data(), /*strides=*/nullptr, d_mpsTensors.data(), 0)); + std::cout << "Computed the MPS factorization\n"; + + // Sphinx: MPS Amplitudes #14 + + // Specify the quantum circuit amplitudes accessor + cutensornetStateAccessor_t accessor; + HANDLE_CUTN_ERROR(cutensornetCreateAccessor(cutnHandle, quantumState, numFixedModes, fixedModes.data(), + nullptr, &accessor)); // using default strides + std::cout << "Created the specified quantum circuit amplitudes accessor\n"; + + // Sphinx: MPS Amplitudes #15 + + // Configure the computation of the slice of the specified quantum circuit amplitudes tensor + const int32_t numHyperSamples = 8; // desired number of hyper samples used in the tensor network contraction path finder + HANDLE_CUTN_ERROR(cutensornetAccessorConfigure(cutnHandle, accessor, + CUTENSORNET_ACCESSOR_OPT_NUM_HYPER_SAMPLES, &numHyperSamples, sizeof(numHyperSamples))); + + // Sphinx: MPS Amplitudes #16 + + // Prepare the computation of the specified slice of the quantum circuit amplitudes tensor + HANDLE_CUTN_ERROR(cutensornetAccessorPrepare(cutnHandle, accessor, scratchSize, workDesc, 0x0)); + std::cout << "Prepared the computation of the specified slice of the quantum circuit amplitudes tensor\n"; + + // Sphinx: MPS Amplitudes #17 + + // Attach the workspace buffer + HANDLE_CUTN_ERROR(cutensornetWorkspaceGetMemorySize(cutnHandle, + workDesc, + CUTENSORNET_WORKSIZE_PREF_RECOMMENDED, + CUTENSORNET_MEMSPACE_DEVICE, + CUTENSORNET_WORKSPACE_SCRATCH, + &worksize)); + std::cout << "Required scratch GPU workspace size (bytes) = " << worksize << std::endl; + if(worksize <= scratchSize) { + HANDLE_CUTN_ERROR(cutensornetWorkspaceSetMemory(cutnHandle, workDesc, CUTENSORNET_MEMSPACE_DEVICE, + CUTENSORNET_WORKSPACE_SCRATCH, d_scratch, worksize)); + }else{ + std::cout << "ERROR: Insufficient workspace size on Device!\n"; + std::abort(); + } + std::cout << "Set the workspace buffer\n"; + + // Sphinx: MPS Amplitudes #18 + + // Compute the specified slice of the quantum circuit amplitudes tensor + std::complex stateNorm{0.0,0.0}; + HANDLE_CUTN_ERROR(cutensornetAccessorCompute(cutnHandle, accessor, fixedValues.data(), + workDesc, d_amp, static_cast(&stateNorm), 0x0)); + std::cout << "Computed the specified slice of the quantum circuit amplitudes tensor\n"; + std::vector> h_amp(ampSize); + HANDLE_CUDA_ERROR(cudaMemcpy(h_amp.data(), d_amp, ampSize * (2 * fp64size), cudaMemcpyDeviceToHost)); + std::cout << "Amplitudes slice for " << (numQubits - numFixedModes) << " qubits:\n"; + for(std::size_t i = 0; i < ampSize; ++i) { + std::cout << " " << h_amp[i] << std::endl; + } + std::cout << "State 2-norm = (" << stateNorm.real() << ", " << stateNorm.imag() << ")\n"; + + // Sphinx: MPS Amplitudes #19 + + // Destroy the workspace descriptor + HANDLE_CUTN_ERROR(cutensornetDestroyWorkspaceDescriptor(workDesc)); + std::cout << "Destroyed the workspace descriptor\n"; + + // Destroy the quantum circuit amplitudes accessor + HANDLE_CUTN_ERROR(cutensornetDestroyAccessor(accessor)); + std::cout << "Destroyed the quantum circuit amplitudes accessor\n"; + + // Destroy the quantum circuit state + HANDLE_CUTN_ERROR(cutensornetDestroyState(quantumState)); + std::cout << "Destroyed the quantum circuit state\n"; + + for (int32_t i = 0; i < numQubits; i++) { + HANDLE_CUDA_ERROR(cudaFree(d_mpsTensors[i])); + } + HANDLE_CUDA_ERROR(cudaFree(d_scratch)); + HANDLE_CUDA_ERROR(cudaFree(d_amp)); + HANDLE_CUDA_ERROR(cudaFree(d_gateCX)); + HANDLE_CUDA_ERROR(cudaFree(d_gateH)); + std::cout << "Freed memory on GPU\n"; + + // Finalize the cuTensorNet library + HANDLE_CUTN_ERROR(cutensornetDestroy(cutnHandle)); + std::cout << "Finalized the cuTensorNet library\n"; + + return 0; +} diff --git a/samples/cutensornet/high_level/mps_expectation_example.cu b/samples/cutensornet/high_level/mps_expectation_example.cu new file mode 100644 index 0000000..2b5e84b --- /dev/null +++ b/samples/cutensornet/high_level/mps_expectation_example.cu @@ -0,0 +1,308 @@ +/* Copyright (c) 2023, NVIDIA CORPORATION & AFFILIATES. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +// Sphinx: MPS Expectation #1 + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + + +#define HANDLE_CUDA_ERROR(x) \ +{ const auto err = x; \ + if( err != cudaSuccess ) \ + { printf("CUDA error %s in line %d\n", cudaGetErrorString(err), __LINE__); fflush(stdout); std::abort(); } \ +}; + +#define HANDLE_CUTN_ERROR(x) \ +{ const auto err = x; \ + if( err != CUTENSORNET_STATUS_SUCCESS ) \ + { printf("cuTensorNet error %s in line %d\n", cutensornetGetErrorString(err), __LINE__); fflush(stdout); std::abort(); } \ +}; + + +int main(int argc, char **argv) +{ + static_assert(sizeof(size_t) == sizeof(int64_t), "Please build this sample on a 64-bit architecture!"); + + constexpr std::size_t fp64size = sizeof(double); + + // Sphinx: MPS Expectation #2 + + // Quantum state configuration + constexpr int32_t numQubits = 16; // number of qubits + const std::vector qubitDims(numQubits,2); // qubit dimensions + std::cout << "Quantum circuit: " << numQubits << " qubits\n"; + + // Sphinx: MPS Expectation #3 + + // Initialize the cuTensorNet library + HANDLE_CUDA_ERROR(cudaSetDevice(0)); + cutensornetHandle_t cutnHandle; + HANDLE_CUTN_ERROR(cutensornetCreate(&cutnHandle)); + std::cout << "Initialized cuTensorNet library on GPU 0\n"; + + // Sphinx: MPS Expectation #4 + + // Define necessary quantum gate tensors in Host memory + const double invsq2 = 1.0 / std::sqrt(2.0); + // Hadamard gate + const std::vector> h_gateH {{invsq2, 0.0}, {invsq2, 0.0}, + {invsq2, 0.0}, {-invsq2, 0.0}}; + // Pauli X gate + const std::vector> h_gateX {{0.0, 0.0}, {1.0, 0.0}, + {1.0, 0.0}, {0.0, 0.0}}; + // Pauli Y gate + const std::vector> h_gateY {{0.0, 0.0}, {0.0, -1.0}, + {0.0, 1.0}, {0.0, 0.0}}; + // Pauli Z gate + const std::vector> h_gateZ {{1.0, 0.0}, {0.0, 0.0}, + {0.0, 0.0}, {-1.0, 0.0}}; + // CX gate + const std::vector> h_gateCX {{1.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, + {0.0, 0.0}, {1.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, + {0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {1.0, 0.0}, + {0.0, 0.0}, {0.0, 0.0}, {1.0, 0.0}, {0.0, 0.0}}; + + // Copy quantum gates to Device memory + void *d_gateH{nullptr}, *d_gateX{nullptr}, *d_gateY{nullptr}, *d_gateZ{nullptr}, *d_gateCX{nullptr}; + HANDLE_CUDA_ERROR(cudaMalloc(&d_gateH, 4 * (2 * fp64size))); + HANDLE_CUDA_ERROR(cudaMalloc(&d_gateX, 4 * (2 * fp64size))); + HANDLE_CUDA_ERROR(cudaMalloc(&d_gateY, 4 * (2 * fp64size))); + HANDLE_CUDA_ERROR(cudaMalloc(&d_gateZ, 4 * (2 * fp64size))); + HANDLE_CUDA_ERROR(cudaMalloc(&d_gateCX, 16 * (2 * fp64size))); + std::cout << "Allocated quantum gate memory on GPU\n"; + HANDLE_CUDA_ERROR(cudaMemcpy(d_gateH, h_gateH.data(), 4 * (2 * fp64size), cudaMemcpyHostToDevice)); + HANDLE_CUDA_ERROR(cudaMemcpy(d_gateX, h_gateX.data(), 4 * (2 * fp64size), cudaMemcpyHostToDevice)); + HANDLE_CUDA_ERROR(cudaMemcpy(d_gateY, h_gateY.data(), 4 * (2 * fp64size), cudaMemcpyHostToDevice)); + HANDLE_CUDA_ERROR(cudaMemcpy(d_gateZ, h_gateZ.data(), 4 * (2 * fp64size), cudaMemcpyHostToDevice)); + HANDLE_CUDA_ERROR(cudaMemcpy(d_gateCX, h_gateCX.data(), 16 * (2 * fp64size), cudaMemcpyHostToDevice)); + std::cout << "Copied quantum gates to GPU memory\n"; + + // Sphinx: MPS Expectation #5 + + // Determine the MPS representation and allocate buffers for the MPS tensors + const int64_t maxExtent = 2; // GHZ state can be exactly represented with max bond dimension of 2 + std::vector> extents; + std::vector extentsPtr(numQubits); + std::vector d_mpsTensors(numQubits, nullptr); + for (int32_t i = 0; i < numQubits; i++) { + if (i == 0) { // left boundary MPS tensor + extents.push_back({2, maxExtent}); + HANDLE_CUDA_ERROR(cudaMalloc(&d_mpsTensors[i], 2 * maxExtent * 2 * fp64size)); + } + else if (i == numQubits-1) { // right boundary MPS tensor + extents.push_back({maxExtent, 2}); + HANDLE_CUDA_ERROR(cudaMalloc(&d_mpsTensors[i], 2 * maxExtent * 2 * fp64size)); + } + else { // middle MPS tensors + extents.push_back({maxExtent, 2, maxExtent}); + HANDLE_CUDA_ERROR(cudaMalloc(&d_mpsTensors[i], 2 * maxExtent * maxExtent * 2 * fp64size)); + } + extentsPtr[i] = extents[i].data(); + } + + // Sphinx: MPS Expectation #6 + + // Query the free memory on Device + std::size_t freeSize{0}, totalSize{0}; + HANDLE_CUDA_ERROR(cudaMemGetInfo(&freeSize, &totalSize)); + const std::size_t scratchSize = (freeSize - (freeSize % 4096)) / 2; // use half of available memory with alignment + void *d_scratch{nullptr}; + HANDLE_CUDA_ERROR(cudaMalloc(&d_scratch, scratchSize)); + std::cout << "Allocated " << scratchSize << " bytes of scratch memory on GPU\n"; + + // Sphinx: MPS Expectation #7 + + // Create the initial quantum state + cutensornetState_t quantumState; + HANDLE_CUTN_ERROR(cutensornetCreateState(cutnHandle, CUTENSORNET_STATE_PURITY_PURE, numQubits, qubitDims.data(), + CUDA_C_64F, &quantumState)); + std::cout << "Created the initial quantum state\n"; + + // Sphinx: MPS Expectation #8 + + // Construct the final quantum circuit state (apply quantum gates) for the GHZ circuit + int64_t id; + HANDLE_CUTN_ERROR(cutensornetStateApplyTensor(cutnHandle, quantumState, 1, std::vector{{0}}.data(), + d_gateH, nullptr, 1, 0, 1, &id)); + for(int32_t i = 1; i < numQubits; ++i) { + HANDLE_CUTN_ERROR(cutensornetStateApplyTensor(cutnHandle, quantumState, 2, std::vector{{i-1,i}}.data(), + d_gateCX, nullptr, 1, 0, 1, &id)); + } + std::cout << "Applied quantum gates\n"; + + // Sphinx: MPS Expectation #9 + + // Specify the final target MPS representation (use default fortran strides) + HANDLE_CUTN_ERROR(cutensornetStateFinalizeMPS(cutnHandle, quantumState, + CUTENSORNET_BOUNDARY_CONDITION_OPEN, extentsPtr.data(), /*strides=*/nullptr )); + + // Sphinx: MPS Expectation #10 + + // Optional, set up the SVD method for truncation. + cutensornetTensorSVDAlgo_t algo = CUTENSORNET_TENSOR_SVD_ALGO_GESVDJ; + HANDLE_CUTN_ERROR(cutensornetStateConfigure(cutnHandle, quantumState, + CUTENSORNET_STATE_MPS_SVD_CONFIG_ALGO, &algo, sizeof(algo))); + std::cout << "Configured the MPS computation\n"; + + // Sphinx: MPS Expectation #11 + + // Prepare the MPS computation and attach workspace + cutensornetWorkspaceDescriptor_t workDesc; + HANDLE_CUTN_ERROR(cutensornetCreateWorkspaceDescriptor(cutnHandle, &workDesc)); + std::cout << "Created the workspace descriptor\n"; + HANDLE_CUTN_ERROR(cutensornetStatePrepare(cutnHandle, quantumState, scratchSize, workDesc, 0x0)); + int64_t worksize {0}; + HANDLE_CUTN_ERROR(cutensornetWorkspaceGetMemorySize(cutnHandle, + workDesc, + CUTENSORNET_WORKSIZE_PREF_RECOMMENDED, + CUTENSORNET_MEMSPACE_DEVICE, + CUTENSORNET_WORKSPACE_SCRATCH, + &worksize)); + std::cout << "Scratch GPU workspace size (bytes) for MPS computation = " << worksize << std::endl; + if(worksize <= scratchSize) { + HANDLE_CUTN_ERROR(cutensornetWorkspaceSetMemory(cutnHandle, workDesc, CUTENSORNET_MEMSPACE_DEVICE, + CUTENSORNET_WORKSPACE_SCRATCH, d_scratch, worksize)); + }else{ + std::cout << "ERROR: Insufficient workspace size on Device!\n"; + std::abort(); + } + std::cout << "Set the workspace buffer for MPS computation\n"; + + // Sphinx: MPS Expectation #12 + + // Execute MPS computation + HANDLE_CUTN_ERROR(cutensornetStateCompute(cutnHandle, quantumState, + workDesc, extentsPtr.data(), /*strides=*/nullptr, d_mpsTensors.data(), 0)); + + // Sphinx: MPS Expectation #13 + + // Create an empty tensor network operator + cutensornetNetworkOperator_t hamiltonian; + HANDLE_CUTN_ERROR(cutensornetCreateNetworkOperator(cutnHandle, numQubits, qubitDims.data(), CUDA_C_64F, &hamiltonian)); + // Append component (0.5 * Z1 * Z2) to the tensor network operator + { + const int32_t numModes[] = {1, 1}; // Z1 acts on 1 mode, Z2 acts on 1 mode + const int32_t modesZ1[] = {1}; // state modes Z1 acts on + const int32_t modesZ2[] = {2}; // state modes Z2 acts on + const int32_t * stateModes[] = {modesZ1, modesZ2}; // state modes (Z1 * Z2) acts on + const void * gateData[] = {d_gateZ, d_gateZ}; // GPU pointers to gate data + HANDLE_CUTN_ERROR(cutensornetNetworkOperatorAppendProduct(cutnHandle, hamiltonian, cuDoubleComplex{0.5,0.0}, + 2, numModes, stateModes, NULL, gateData, &id)); + } + // Append component (0.25 * Y3) to the tensor network operator + { + const int32_t numModes[] = {1}; // Y3 acts on 1 mode + const int32_t modesY3[] = {3}; // state modes Y3 acts on + const int32_t * stateModes[] = {modesY3}; // state modes (Y3) acts on + const void * gateData[] = {d_gateY}; // GPU pointers to gate data + HANDLE_CUTN_ERROR(cutensornetNetworkOperatorAppendProduct(cutnHandle, hamiltonian, cuDoubleComplex{0.25,0.0}, + 1, numModes, stateModes, NULL, gateData, &id)); + } + // Append component (0.13 * Y0 X2 Z3) to the tensor network operator + { + const int32_t numModes[] = {1, 1, 1}; // Y0 acts on 1 mode, X2 acts on 1 mode, Z3 acts on 1 mode + const int32_t modesY0[] = {0}; // state modes Y0 acts on + const int32_t modesX2[] = {2}; // state modes X2 acts on + const int32_t modesZ3[] = {3}; // state modes Z3 acts on + const int32_t * stateModes[] = {modesY0, modesX2, modesZ3}; // state modes (Y0 * X2 * Z3) acts on + const void * gateData[] = {d_gateY, d_gateX, d_gateZ}; // GPU pointers to gate data + HANDLE_CUTN_ERROR(cutensornetNetworkOperatorAppendProduct(cutnHandle, hamiltonian, cuDoubleComplex{0.13,0.0}, + 3, numModes, stateModes, NULL, gateData, &id)); + } + std::cout << "Constructed a tensor network operator: (0.5 * Z1 * Z2) + (0.25 * Y3) + (0.13 * Y0 * X2 * Z3)" << std::endl; + + // Sphinx: MPS Expectation #14 + + // Specify the quantum circuit expectation value + cutensornetStateExpectation_t expectation; + HANDLE_CUTN_ERROR(cutensornetCreateExpectation(cutnHandle, quantumState, hamiltonian, &expectation)); + std::cout << "Created the specified quantum circuit expectation value\n"; + + // Sphinx: MPS Expectation #15 + + // Configure the computation of the specified quantum circuit expectation value + const int32_t numHyperSamples = 8; // desired number of hyper samples used in the tensor network contraction path finder + HANDLE_CUTN_ERROR(cutensornetExpectationConfigure(cutnHandle, expectation, + CUTENSORNET_EXPECTATION_OPT_NUM_HYPER_SAMPLES, &numHyperSamples, sizeof(numHyperSamples))); + + // Sphinx: MPS Expectation #16 + + // Prepare the specified quantum circuit expectation value for computation + HANDLE_CUTN_ERROR(cutensornetExpectationPrepare(cutnHandle, expectation, scratchSize, workDesc, 0x0)); + std::cout << "Prepared the specified quantum circuit expectation value\n"; + + // Sphinx: MPS Expectation #17 + + // Attach the workspace buffer + HANDLE_CUTN_ERROR(cutensornetWorkspaceGetMemorySize(cutnHandle, + workDesc, + CUTENSORNET_WORKSIZE_PREF_RECOMMENDED, + CUTENSORNET_MEMSPACE_DEVICE, + CUTENSORNET_WORKSPACE_SCRATCH, + &worksize)); + std::cout << "Required scratch GPU workspace size (bytes) = " << worksize << std::endl; + if(worksize <= scratchSize) { + HANDLE_CUTN_ERROR(cutensornetWorkspaceSetMemory(cutnHandle, workDesc, CUTENSORNET_MEMSPACE_DEVICE, + CUTENSORNET_WORKSPACE_SCRATCH, d_scratch, worksize)); + }else{ + std::cout << "ERROR: Insufficient workspace size on Device!\n"; + std::abort(); + } + std::cout << "Set the workspace buffer\n"; + + // Sphinx: MPS Expectation #18 + + // Compute the specified quantum circuit expectation value + std::complex expectVal{0.0,0.0}, stateNorm{0.0,0.0}; + HANDLE_CUTN_ERROR(cutensornetExpectationCompute(cutnHandle, expectation, workDesc, + static_cast(&expectVal), static_cast(&stateNorm), 0x0)); + std::cout << "Computed the specified quantum circuit expectation value\n"; + std::cout << "Expectation value = (" << expectVal.real() << ", " << expectVal.imag() << ")\n"; + std::cout << "State 2-norm = (" << stateNorm.real() << ", " << stateNorm.imag() << ")\n"; + + // Sphinx: MPS Expectation #19 + + // Destroy the workspace descriptor + HANDLE_CUTN_ERROR(cutensornetDestroyWorkspaceDescriptor(workDesc)); + std::cout << "Destroyed the workspace descriptor\n"; + + // Destroy the quantum circuit expectation value + HANDLE_CUTN_ERROR(cutensornetDestroyExpectation(expectation)); + std::cout << "Destroyed the quantum circuit state expectation value\n"; + + // Destroy the tensor network operator + HANDLE_CUTN_ERROR(cutensornetDestroyNetworkOperator(hamiltonian)); + std::cout << "Destroyed the tensor network operator\n"; + + // Destroy the quantum circuit state + HANDLE_CUTN_ERROR(cutensornetDestroyState(quantumState)); + std::cout << "Destroyed the quantum circuit state\n"; + + for (int32_t i = 0; i < numQubits; i++) { + HANDLE_CUDA_ERROR(cudaFree(d_mpsTensors[i])); + } + HANDLE_CUDA_ERROR(cudaFree(d_scratch)); + HANDLE_CUDA_ERROR(cudaFree(d_gateCX)); + HANDLE_CUDA_ERROR(cudaFree(d_gateZ)); + HANDLE_CUDA_ERROR(cudaFree(d_gateY)); + HANDLE_CUDA_ERROR(cudaFree(d_gateX)); + HANDLE_CUDA_ERROR(cudaFree(d_gateH)); + std::cout << "Freed memory on GPU\n"; + + // Finalize the cuTensorNet library + HANDLE_CUTN_ERROR(cutensornetDestroy(cutnHandle)); + std::cout << "Finalized the cuTensorNet library\n"; + + return 0; +} diff --git a/samples/cutensornet/high_level/mps_marginal_example.cu b/samples/cutensornet/high_level/mps_marginal_example.cu new file mode 100644 index 0000000..fbb2941 --- /dev/null +++ b/samples/cutensornet/high_level/mps_marginal_example.cu @@ -0,0 +1,270 @@ +/* Copyright (c) 2023, NVIDIA CORPORATION & AFFILIATES. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +// Sphinx: MPS Marginal #1 + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + + +#define HANDLE_CUDA_ERROR(x) \ +{ const auto err = x; \ + if( err != cudaSuccess ) \ + { printf("CUDA error %s in line %d\n", cudaGetErrorString(err), __LINE__); fflush(stdout); std::abort(); } \ +}; + +#define HANDLE_CUTN_ERROR(x) \ +{ const auto err = x; \ + if( err != CUTENSORNET_STATUS_SUCCESS ) \ + { printf("cuTensorNet error %s in line %d\n", cutensornetGetErrorString(err), __LINE__); fflush(stdout); std::abort(); } \ +}; + + +int main(int argc, char **argv) +{ + static_assert(sizeof(size_t) == sizeof(int64_t), "Please build this sample on a 64-bit architecture!"); + + constexpr std::size_t fp64size = sizeof(double); + + // Sphinx: MPS Marginal #2 + + // Quantum state configuration + constexpr int32_t numQubits = 16; + const std::vector qubitDims(numQubits,2); // qubit dimensions + constexpr int32_t numMarginalModes = 2; // rank of the marginal (reduced density matrix) + const std::vector marginalModes({0,1}); // open qubits (must be in acsending order) + std::cout << "Quantum circuit: " << numQubits << " qubits\n"; + + // Sphinx: MPS Marginal #3 + + // Initialize the cuTensorNet library + HANDLE_CUDA_ERROR(cudaSetDevice(0)); + cutensornetHandle_t cutnHandle; + HANDLE_CUTN_ERROR(cutensornetCreate(&cutnHandle)); + std::cout << "Initialized cuTensorNet library on GPU 0\n"; + + // Sphinx: MPS Marginal #4 + + // Define necessary quantum gate tensors in Host memory + const double invsq2 = 1.0 / std::sqrt(2.0); + // Hadamard gate + const std::vector> h_gateH {{invsq2, 0.0}, {invsq2, 0.0}, + {invsq2, 0.0}, {-invsq2, 0.0}}; + // CX gate + const std::vector> h_gateCX {{1.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, + {0.0, 0.0}, {1.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, + {0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {1.0, 0.0}, + {0.0, 0.0}, {0.0, 0.0}, {1.0, 0.0}, {0.0, 0.0}}; + + // Copy quantum gates to Device memory + void *d_gateH{nullptr}, *d_gateCX{nullptr}; + HANDLE_CUDA_ERROR(cudaMalloc(&d_gateH, 4 * (2 * fp64size))); + HANDLE_CUDA_ERROR(cudaMalloc(&d_gateCX, 16 * (2 * fp64size))); + std::cout << "Allocated quantum gate memory on GPU\n"; + HANDLE_CUDA_ERROR(cudaMemcpy(d_gateH, h_gateH.data(), 4 * (2 * fp64size), cudaMemcpyHostToDevice)); + HANDLE_CUDA_ERROR(cudaMemcpy(d_gateCX, h_gateCX.data(), 16 * (2 * fp64size), cudaMemcpyHostToDevice)); + std::cout << "Copied quantum gates to GPU memory\n"; + + // Sphinx: MPS Marginal #5 + + // Determine the MPS representation and allocate buffers for the MPS tensors + const int64_t maxExtent = 2; // GHZ state can be exactly represented with max bond dimension of 2 + std::vector> extents; + std::vector extentsPtr(numQubits); + std::vector d_mpsTensors(numQubits, nullptr); + for (int32_t i = 0; i < numQubits; i++) { + if (i == 0) { // left boundary MPS tensor + extents.push_back({2, maxExtent}); + HANDLE_CUDA_ERROR(cudaMalloc(&d_mpsTensors[i], 2 * maxExtent * 2 * fp64size)); + } + else if (i == numQubits-1) { // right boundary MPS tensor + extents.push_back({maxExtent, 2}); + HANDLE_CUDA_ERROR(cudaMalloc(&d_mpsTensors[i], 2 * maxExtent * 2 * fp64size)); + } + else { // middle MPS tensors + extents.push_back({maxExtent, 2, maxExtent}); + HANDLE_CUDA_ERROR(cudaMalloc(&d_mpsTensors[i], 2 * maxExtent * maxExtent * 2 * fp64size)); + } + extentsPtr[i] = extents[i].data(); + } + + // Sphinx: MPS Marginal #6 + + // Allocate the specified quantum circuit reduced density matrix (marginal) in Device memory + void *d_rdm{nullptr}; + std::size_t rdmDim = 1; + for(const auto & mode: marginalModes) rdmDim *= qubitDims[mode]; + const std::size_t rdmSize = rdmDim * rdmDim; + HANDLE_CUDA_ERROR(cudaMalloc(&d_rdm, rdmSize * (2 * fp64size))); + + // Sphinx: MPS Marginal #7 + + // Query the free memory on Device + std::size_t freeSize{0}, totalSize{0}; + HANDLE_CUDA_ERROR(cudaMemGetInfo(&freeSize, &totalSize)); + const std::size_t scratchSize = (freeSize - (freeSize % 4096)) / 2; // use half of available memory with alignment + void *d_scratch{nullptr}; + HANDLE_CUDA_ERROR(cudaMalloc(&d_scratch, scratchSize)); + std::cout << "Allocated " << scratchSize << " bytes of scratch memory on GPU\n"; + + // Sphinx: MPS Marginal #8 + + // Create the initial quantum state + cutensornetState_t quantumState; + HANDLE_CUTN_ERROR(cutensornetCreateState(cutnHandle, CUTENSORNET_STATE_PURITY_PURE, numQubits, qubitDims.data(), + CUDA_C_64F, &quantumState)); + std::cout << "Created the initial quantum state\n"; + + // Sphinx: MPS Marginal #9 + + // Construct the final quantum circuit state (apply quantum gates) for the GHZ circuit + int64_t id; + HANDLE_CUTN_ERROR(cutensornetStateApplyTensor(cutnHandle, quantumState, 1, std::vector{{0}}.data(), + d_gateH, nullptr, 1, 0, 1, &id)); + for(int32_t i = 1; i < numQubits; ++i) { + HANDLE_CUTN_ERROR(cutensornetStateApplyTensor(cutnHandle, quantumState, 2, std::vector{{i-1,i}}.data(), + d_gateCX, nullptr, 1, 0, 1, &id)); + } + std::cout << "Applied quantum gates\n"; + + // Sphinx: MPS Marginal #10 + + // Specify the final target MPS representation (use default fortran strides) + HANDLE_CUTN_ERROR(cutensornetStateFinalizeMPS(cutnHandle, quantumState, + CUTENSORNET_BOUNDARY_CONDITION_OPEN, extentsPtr.data(), /*strides=*/nullptr)); + std::cout << "Requested the final MPS factorization of the quantum circuit state\n"; + + // Sphinx: MPS Marginal #11 + + // Optional, set up the SVD method for MPS truncation. + cutensornetTensorSVDAlgo_t algo = CUTENSORNET_TENSOR_SVD_ALGO_GESVDJ; + HANDLE_CUTN_ERROR(cutensornetStateConfigure(cutnHandle, quantumState, + CUTENSORNET_STATE_MPS_SVD_CONFIG_ALGO, &algo, sizeof(algo))); + std::cout << "Configured the MPS factorization computation\n"; + + // Sphinx: MPS Marginal #12 + + // Prepare the MPS computation and attach workspace + cutensornetWorkspaceDescriptor_t workDesc; + HANDLE_CUTN_ERROR(cutensornetCreateWorkspaceDescriptor(cutnHandle, &workDesc)); + std::cout << "Created the workspace descriptor\n"; + HANDLE_CUTN_ERROR(cutensornetStatePrepare(cutnHandle, quantumState, scratchSize, workDesc, 0x0)); + int64_t worksize {0}; + HANDLE_CUTN_ERROR(cutensornetWorkspaceGetMemorySize(cutnHandle, + workDesc, + CUTENSORNET_WORKSIZE_PREF_RECOMMENDED, + CUTENSORNET_MEMSPACE_DEVICE, + CUTENSORNET_WORKSPACE_SCRATCH, + &worksize)); + std::cout << "Scratch GPU workspace size (bytes) for MPS computation = " << worksize << std::endl; + if(worksize <= scratchSize) { + HANDLE_CUTN_ERROR(cutensornetWorkspaceSetMemory(cutnHandle, workDesc, CUTENSORNET_MEMSPACE_DEVICE, + CUTENSORNET_WORKSPACE_SCRATCH, d_scratch, worksize)); + }else{ + std::cout << "ERROR: Insufficient workspace size on Device!\n"; + std::abort(); + } + std::cout << "Set the workspace buffer for the MPS factorization computation\n"; + + // Sphinx: MPS Marginal #13 + + // Execute MPS computation + HANDLE_CUTN_ERROR(cutensornetStateCompute(cutnHandle, quantumState, + workDesc, extentsPtr.data(), /*strides=*/nullptr, d_mpsTensors.data(), 0)); + std::cout << "Computed the MPS factorization\n"; + + // Sphinx: MPS Marginal #14 + + // Specify the desired reduced density matrix (marginal) + cutensornetStateMarginal_t marginal; + HANDLE_CUTN_ERROR(cutensornetCreateMarginal(cutnHandle, quantumState, numMarginalModes, marginalModes.data(), + 0, nullptr, std::vector{{1,2,4,8}}.data(), &marginal)); // using explicit strides + std::cout << "Created the specified quantum circuit reduced densitry matrix (marginal)\n"; + + // Sphinx: MPS Marginal #15 + + // Configure the computation of the specified quantum circuit reduced density matrix (marginal) + const int32_t numHyperSamples = 8; // desired number of hyper samples used in the tensor network contraction path finder + HANDLE_CUTN_ERROR(cutensornetMarginalConfigure(cutnHandle, marginal, + CUTENSORNET_MARGINAL_OPT_NUM_HYPER_SAMPLES, &numHyperSamples, sizeof(numHyperSamples))); + std::cout << "Configured the specified quantum circuit reduced density matrix (marginal) computation\n"; + + // Sphinx: MPS Marginal #16 + + // Prepare the specified quantum circuit reduced densitry matrix (marginal) + HANDLE_CUTN_ERROR(cutensornetMarginalPrepare(cutnHandle, marginal, scratchSize, workDesc, 0x0)); + std::cout << "Prepared the specified quantum circuit reduced density matrix (marginal)\n"; + + // Sphinx: MPS Marginal #17 + + // Attach the workspace buffer + HANDLE_CUTN_ERROR(cutensornetWorkspaceGetMemorySize(cutnHandle, + workDesc, + CUTENSORNET_WORKSIZE_PREF_RECOMMENDED, + CUTENSORNET_MEMSPACE_DEVICE, + CUTENSORNET_WORKSPACE_SCRATCH, + &worksize)); + std::cout << "Required scratch GPU workspace size (bytes) for marginal computation = " << worksize << std::endl; + if(worksize <= scratchSize) { + HANDLE_CUTN_ERROR(cutensornetWorkspaceSetMemory(cutnHandle, workDesc, CUTENSORNET_MEMSPACE_DEVICE, + CUTENSORNET_WORKSPACE_SCRATCH, d_scratch, worksize)); + }else{ + std::cout << "ERROR: Insufficient workspace size on Device!\n"; + std::abort(); + } + std::cout << "Set the workspace buffer\n"; + + // Sphinx: MPS Marginal #18 + + // Compute the specified quantum circuit reduced densitry matrix (marginal) + HANDLE_CUTN_ERROR(cutensornetMarginalCompute(cutnHandle, marginal, nullptr, workDesc, d_rdm, 0)); + std::cout << "Computed the specified quantum circuit reduced density matrix (marginal)\n"; + std::vector> h_rdm(rdmSize); + HANDLE_CUDA_ERROR(cudaMemcpy(h_rdm.data(), d_rdm, rdmSize * (2 * fp64size), cudaMemcpyDeviceToHost)); + std::cout << "Reduced density matrix for " << numMarginalModes << " qubits:\n"; + for(std::size_t i = 0; i < rdmDim; ++i) { + for(std::size_t j = 0; j < rdmDim; ++j) { + std::cout << " " << h_rdm[i + j * rdmDim]; + } + std::cout << std::endl; + } + + // Sphinx: MPS Marginal #19 + + // Destroy the workspace descriptor + HANDLE_CUTN_ERROR(cutensornetDestroyWorkspaceDescriptor(workDesc)); + std::cout << "Destroyed the workspace descriptor\n"; + + // Destroy the quantum circuit reduced density matrix + HANDLE_CUTN_ERROR(cutensornetDestroyMarginal(marginal)); + std::cout << "Destroyed the quantum circuit state reduced density matrix (marginal)\n"; + + // Destroy the quantum circuit state + HANDLE_CUTN_ERROR(cutensornetDestroyState(quantumState)); + std::cout << "Destroyed the quantum circuit state\n"; + + for (int32_t i = 0; i < numQubits; i++) { + HANDLE_CUDA_ERROR(cudaFree(d_mpsTensors[i])); + } + HANDLE_CUDA_ERROR(cudaFree(d_scratch)); + HANDLE_CUDA_ERROR(cudaFree(d_rdm)); + HANDLE_CUDA_ERROR(cudaFree(d_gateCX)); + HANDLE_CUDA_ERROR(cudaFree(d_gateH)); + std::cout << "Freed memory on GPU\n"; + + // Finalize the cuTensorNet library + HANDLE_CUTN_ERROR(cutensornetDestroy(cutnHandle)); + std::cout << "Finalized the cuTensorNet library\n"; + + return 0; +} diff --git a/samples/cutensornet/high_level/mps_sampling_example.cu b/samples/cutensornet/high_level/mps_sampling_example.cu new file mode 100644 index 0000000..ebaf441 --- /dev/null +++ b/samples/cutensornet/high_level/mps_sampling_example.cu @@ -0,0 +1,254 @@ +/* Copyright (c) 2023, NVIDIA CORPORATION & AFFILIATES. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +// Sphinx: MPS Sampler #1 + +#include +#include +#include +#include +#include +#include + +#include +#include + +#define HANDLE_CUDA_ERROR(x) \ +{ const auto err = x; \ + if( err != cudaSuccess ) \ + { printf("CUDA error %s in line %d\n", cudaGetErrorString(err), __LINE__); fflush(stdout); std::abort(); } \ +}; + +#define HANDLE_CUTN_ERROR(x) \ +{ const auto err = x; \ + if( err != CUTENSORNET_STATUS_SUCCESS ) \ + { printf("cuTensorNet error %s in line %d\n", cutensornetGetErrorString(err), __LINE__); fflush(stdout); std::abort(); } \ +}; + + +int main(int argc, char **argv) +{ + static_assert(sizeof(size_t) == sizeof(int64_t), "Please build this sample on a 64-bit architecture!"); + + constexpr std::size_t fp64size = sizeof(double); + + // Sphinx: MPS Sampler #2 + + // Quantum state configuration + const int64_t numSamples = 100; + const int32_t numQubits = 16; + const std::vector qubitDims(numQubits, 2); // qubit size + std::cout << "Quantum circuit: " << numQubits << " qubits; " << numSamples << " samples\n"; + + // Sphinx: MPS Sampler #3 + + // Initialize the cuTensorNet library + HANDLE_CUDA_ERROR(cudaSetDevice(0)); + cutensornetHandle_t cutnHandle; + HANDLE_CUTN_ERROR(cutensornetCreate(&cutnHandle)); + std::cout << "Initialized cuTensorNet library on GPU 0\n"; + + // Sphinx: MPS Sampler #4 + + // Define necessary quantum gate tensors in Host memory + const double invsq2 = 1.0 / std::sqrt(2.0); + // Hadamard gate + const std::vector> h_gateH {{invsq2, 0.0}, {invsq2, 0.0}, + {invsq2, 0.0}, {-invsq2, 0.0}}; + // CX gate + const std::vector> h_gateCX {{1.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, + {0.0, 0.0}, {1.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, + {0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {1.0, 0.0}, + {0.0, 0.0}, {0.0, 0.0}, {1.0, 0.0}, {0.0, 0.0}}; + + // Copy quantum gates to Device memory + void *d_gateH{nullptr}, *d_gateCX{nullptr}; + HANDLE_CUDA_ERROR(cudaMalloc(&d_gateH, 4 * (2 * fp64size))); + std::cout << "H gate buffer allocated on GPU: " << d_gateH << std::endl; //debug + HANDLE_CUDA_ERROR(cudaMalloc(&d_gateCX, 16 * (2 * fp64size))); + std::cout << "CX gate buffer allocated on GPU: " << d_gateCX << std::endl; //debug + std::cout << "Allocated quantum gate memory on GPU\n"; + HANDLE_CUDA_ERROR(cudaMemcpy(d_gateH, h_gateH.data(), 4 * (2 * fp64size), cudaMemcpyHostToDevice)); + HANDLE_CUDA_ERROR(cudaMemcpy(d_gateCX, h_gateCX.data(), 16 * (2 * fp64size), cudaMemcpyHostToDevice)); + std::cout << "Copied quantum gates to GPU memory\n"; + + // Sphinx: MPS Sampler #5 + + // Determine the MPS representation and allocate buffer for the MPS tensors + const int64_t maxExtent = 2; // GHZ state can be exactly represented with max bond dimension of 2 + std::vector> extents; + std::vector extentsPtr(numQubits); + std::vector d_mpsTensors(numQubits, nullptr); + for (int32_t i = 0; i < numQubits; i++) { + if (i == 0) { // left boundary MPS tensor + extents.push_back({2, maxExtent}); + HANDLE_CUDA_ERROR(cudaMalloc(&d_mpsTensors[i], 2 * maxExtent * 2 * fp64size)); + } + else if (i == numQubits-1) { // right boundary MPS tensor + extents.push_back({maxExtent, 2}); + HANDLE_CUDA_ERROR(cudaMalloc(&d_mpsTensors[i], 2 * maxExtent * 2 * fp64size)); + } + else { // middle MPS tensors + extents.push_back({maxExtent, 2, maxExtent}); + HANDLE_CUDA_ERROR(cudaMalloc(&d_mpsTensors[i], 2 * maxExtent * maxExtent * 2 * fp64size)); + } + extentsPtr[i] = extents[i].data(); + } + + // Sphinx: MPS Sampler #6 + + // Query the free memory on Device + std::size_t freeSize {0}, totalSize {0}; + HANDLE_CUDA_ERROR(cudaMemGetInfo(&freeSize, &totalSize)); + const std::size_t scratchSize = (freeSize - (freeSize % 4096)) / 2; // use half of available memory with alignment + void *d_scratch {nullptr}; + HANDLE_CUDA_ERROR(cudaMalloc(&d_scratch, scratchSize)); + std::cout << "Allocated " << scratchSize << " bytes of scratch memory on GPU: " + << "[" << d_scratch << ":" << (void*)(((char*)(d_scratch)) + scratchSize) << ")\n"; + + // Sphinx: MPS Sampler #7 + + // Create the initial quantum state + cutensornetState_t quantumState; + HANDLE_CUTN_ERROR(cutensornetCreateState(cutnHandle, CUTENSORNET_STATE_PURITY_PURE, numQubits, qubitDims.data(), + CUDA_C_64F, &quantumState)); + std::cout << "Created the initial quantum state\n"; + + // Sphinx: MPS Sampler #8 + + // Construct the quantum circuit state (apply quantum gates) + int64_t id; + HANDLE_CUTN_ERROR(cutensornetStateApplyTensor(cutnHandle, quantumState, 1, std::vector{{0}}.data(), + d_gateH, nullptr, 1, 0, 1, &id)); + for(int32_t i = 1; i < numQubits; ++i) { + HANDLE_CUTN_ERROR(cutensornetStateApplyTensor(cutnHandle, quantumState, 2, std::vector{{i-1,i}}.data(), + d_gateCX, nullptr, 1, 0, 1, &id)); + } + std::cout << "Applied quantum gates\n"; + + // Sphinx: MPS Sampler #9 + + // Specify the final target MPS representation (use default fortran strides) + HANDLE_CUTN_ERROR(cutensornetStateFinalizeMPS(cutnHandle, quantumState, + CUTENSORNET_BOUNDARY_CONDITION_OPEN, extentsPtr.data(), /*strides=*/nullptr )); + + // Sphinx: MPS Sampler #10 + + // Optional, set up the SVD method for truncation. + cutensornetTensorSVDAlgo_t algo = CUTENSORNET_TENSOR_SVD_ALGO_GESVDJ; + HANDLE_CUTN_ERROR(cutensornetStateConfigure(cutnHandle, quantumState, + CUTENSORNET_STATE_MPS_SVD_CONFIG_ALGO, &algo, sizeof(algo))); + std::cout << "Configured the MPS computation\n"; + + // Sphinx: MPS Sampler #11 + + // Prepare the MPS computation and attach workspace + cutensornetWorkspaceDescriptor_t workDesc; + HANDLE_CUTN_ERROR(cutensornetCreateWorkspaceDescriptor(cutnHandle, &workDesc)); + std::cout << "Created the workspace descriptor\n"; + HANDLE_CUTN_ERROR(cutensornetStatePrepare(cutnHandle, quantumState, scratchSize, workDesc, 0x0)); + int64_t worksize {0}; + HANDLE_CUTN_ERROR(cutensornetWorkspaceGetMemorySize(cutnHandle, + workDesc, + CUTENSORNET_WORKSIZE_PREF_RECOMMENDED, + CUTENSORNET_MEMSPACE_DEVICE, + CUTENSORNET_WORKSPACE_SCRATCH, + &worksize)); + std::cout << "Scratch GPU workspace size (bytes) for MPS computation = " << worksize << std::endl; + if(worksize <= scratchSize) { + HANDLE_CUTN_ERROR(cutensornetWorkspaceSetMemory(cutnHandle, workDesc, CUTENSORNET_MEMSPACE_DEVICE, + CUTENSORNET_WORKSPACE_SCRATCH, d_scratch, worksize)); + }else{ + std::cout << "ERROR: Insufficient workspace size on Device!\n"; + std::abort(); + } + std::cout << "Set the workspace buffer for MPS computation\n"; + + // Sphinx: MPS Sampler #12 + + // Execute MPS computation + HANDLE_CUTN_ERROR(cutensornetStateCompute(cutnHandle, quantumState, + workDesc, extentsPtr.data(), /*strides=*/nullptr, d_mpsTensors.data(), 0)); + + // Sphinx: MPS Sampler #13 + + // Create the quantum circuit sampler + cutensornetStateSampler_t sampler; + HANDLE_CUTN_ERROR(cutensornetCreateSampler(cutnHandle, quantumState, numQubits, nullptr, &sampler)); + std::cout << "Created the quantum circuit sampler\n"; + + // Sphinx: MPS Sampler #14 + + // Configure the quantum circuit sampler + const int32_t numHyperSamples = 8; // desired number of hyper samples used in the tensor network contraction path finder + HANDLE_CUTN_ERROR(cutensornetSamplerConfigure(cutnHandle, sampler, + CUTENSORNET_SAMPLER_OPT_NUM_HYPER_SAMPLES, &numHyperSamples, sizeof(numHyperSamples))); + + // Sphinx: MPS Sampler #15 + + // Prepare the quantum circuit sampler + HANDLE_CUTN_ERROR(cutensornetSamplerPrepare(cutnHandle, sampler, scratchSize, workDesc, 0x0)); + std::cout << "Prepared the quantum circuit state sampler\n"; + + // Sphinx: MPS Sampler #16 + + // Attach the workspace buffer + HANDLE_CUTN_ERROR(cutensornetWorkspaceGetMemorySize(cutnHandle, + workDesc, + CUTENSORNET_WORKSIZE_PREF_RECOMMENDED, + CUTENSORNET_MEMSPACE_DEVICE, + CUTENSORNET_WORKSPACE_SCRATCH, + &worksize)); + std::cout << "Scratch GPU workspace size (bytes) for MPS Sampling = " << worksize << std::endl; + assert(worksize > 0); + if(worksize <= scratchSize) { + HANDLE_CUTN_ERROR(cutensornetWorkspaceSetMemory(cutnHandle, workDesc, CUTENSORNET_MEMSPACE_DEVICE, + CUTENSORNET_WORKSPACE_SCRATCH, d_scratch, worksize)); + }else{ + std::cout << "ERROR: Insufficient workspace size on Device!\n"; + std::abort(); + } + std::cout << "Set the workspace buffer\n"; + + // Sphinx: MPS Sampler #17 + + // Sample the quantum circuit state + std::vector samples(numQubits * numSamples); // samples[SampleId][QubitId] reside in Host memory + HANDLE_CUTN_ERROR(cutensornetSamplerSample(cutnHandle, sampler, numSamples, workDesc, samples.data(), 0)); + std::cout << "Performed quantum circuit state sampling\n"; + std::cout << "Bit-string samples:\n"; + for(int64_t i = 0; i < numSamples; ++i) { + for(int64_t j = 0; j < numQubits; ++j) std::cout << " " << samples[i * numQubits + j]; + std::cout << std::endl; + } + + // Sphinx: MPS Sampler #18 + + // Destroy the workspace descriptor + HANDLE_CUTN_ERROR(cutensornetDestroyWorkspaceDescriptor(workDesc)); + std::cout << "Destroyed the workspace descriptor\n"; + + // Destroy the quantum circuit sampler + HANDLE_CUTN_ERROR(cutensornetDestroySampler(sampler)); + std::cout << "Destroyed the quantum circuit state sampler\n"; + + // Destroy the quantum circuit state + HANDLE_CUTN_ERROR(cutensornetDestroyState(quantumState)); + std::cout << "Destroyed the quantum circuit state\n"; + + for (int32_t i = 0; i < numQubits; i++) { + HANDLE_CUDA_ERROR(cudaFree(d_mpsTensors[i])); + } + HANDLE_CUDA_ERROR(cudaFree(d_scratch)); + HANDLE_CUDA_ERROR(cudaFree(d_gateCX)); + HANDLE_CUDA_ERROR(cudaFree(d_gateH)); + std::cout << "Freed memory on GPU\n"; + + // Finalize the cuTensorNet library + HANDLE_CUTN_ERROR(cutensornetDestroy(cutnHandle)); + std::cout << "Finalized the cuTensorNet library\n"; + + return 0; +} diff --git a/samples/cutensornet/high_level/sampling_example.cu b/samples/cutensornet/high_level/sampling_example.cu index 3c0c2e0..64376aa 100644 --- a/samples/cutensornet/high_level/sampling_example.cu +++ b/samples/cutensornet/high_level/sampling_example.cu @@ -30,6 +30,8 @@ int main(int argc, char **argv) { + static_assert(sizeof(size_t) == sizeof(int64_t), "Please build this sample on a 64-bit architecture!"); + constexpr std::size_t fp64size = sizeof(double); // Sphinx: Sampler #2 diff --git a/samples/cutensornet/high_level/sampling_mpi_example.cu b/samples/cutensornet/high_level/sampling_mpi_example.cu new file mode 100644 index 0000000..65ba2cc --- /dev/null +++ b/samples/cutensornet/high_level/sampling_mpi_example.cu @@ -0,0 +1,259 @@ +/* Copyright (c) 2023, NVIDIA CORPORATION & AFFILIATES. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +// Sphinx: Sampler #1 + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#define HANDLE_CUDA_ERROR(x) \ +{ const auto err = x; \ + if( err != cudaSuccess ) \ + { printf("CUDA error %s in line %d\n", cudaGetErrorString(err), __LINE__); \ + fflush(stdout); \ + std::abort(); \ + } \ +}; + +#define HANDLE_CUTN_ERROR(x) \ +{ const auto err = x; \ + if( err != CUTENSORNET_STATUS_SUCCESS ) \ + { printf("cuTensorNet error %s in line %d\n", cutensornetGetErrorString(err), __LINE__); \ + fflush(stdout); \ + std::abort(); \ + } \ +}; + +#define HANDLE_MPI_ERROR(x) \ +{ const auto err = x; \ + if( err != MPI_SUCCESS ) \ + { char error[MPI_MAX_ERROR_STRING]; \ + int len; \ + MPI_Error_string(err, error, &len); \ + printf("MPI Error: %s in line %d\n", error, __LINE__); \ + fflush(stdout); \ + MPI_Abort(MPI_COMM_WORLD, err); \ + } \ +}; + + +int main(int argc, char **argv) +{ + static_assert(sizeof(size_t) == sizeof(int64_t), "Please build this sample on a 64-bit architecture!"); + + constexpr std::size_t fp64size = sizeof(double); + + // Sphinx: Sampler #2 + + // Initialize MPI library + HANDLE_MPI_ERROR(MPI_Init(&argc, &argv)); + int rank {-1}; + HANDLE_MPI_ERROR(MPI_Comm_rank(MPI_COMM_WORLD, &rank)); + int numProcs {0}; + HANDLE_MPI_ERROR(MPI_Comm_size(MPI_COMM_WORLD, &numProcs)); + + bool verbose = (rank == 0) ? true : false; + if (verbose) + { + std::cout << "*** Printing is done only from the root MPI process to prevent jumbled messages ***\n"; + std::cout << "The number of MPI processes is " << numProcs << std::endl; + } + + // Sphinx: Sampler #3 + + // Quantum state configuration + const int64_t numSamples = 100; + const int32_t numQubits = 16; + const std::vector qubitDims(numQubits, 2); // qubit size + if (verbose) + std::cout << "Quantum circuit: " << numQubits << " qubits; " << numSamples << " samples\n"; + + // Sphinx: Sampler #4 + + // Initialize the cuTensorNet library + int numDevices {0}; + HANDLE_CUDA_ERROR(cudaGetDeviceCount(&numDevices)); + const int deviceId = rank % numDevices; // we assume that the processes are mapped to nodes in contiguous chunks + HANDLE_CUDA_ERROR(cudaSetDevice(deviceId)); + + cutensornetHandle_t cutnHandle; + HANDLE_CUTN_ERROR(cutensornetCreate(&cutnHandle)); + if (verbose) + std::cout << "Initialized cuTensorNet library on GPU 0\n"; + + // Sphinx: Sampler #5 + + // Activate distributed (parallel) execution + // HANDLE_CUTN_ERROR(cutensornetDistributedResetConfiguration(handle, NULL, 0)); // reset back to serial execution + MPI_Comm cutnComm; + HANDLE_MPI_ERROR(MPI_Comm_dup(MPI_COMM_WORLD, &cutnComm)); // duplicate MPI communicator to dedicate it to cuTensorNet + HANDLE_CUTN_ERROR(cutensornetDistributedResetConfiguration(cutnHandle, &cutnComm, sizeof(cutnComm))); + if(verbose) + printf("Reset cuTensorNet distributed MPI configuration\n"); + + // Sphinx: Sampler #6 + + // Define necessary quantum gate tensors in Host memory + const double invsq2 = 1.0 / std::sqrt(2.0); + // Hadamard gate + const std::vector> h_gateH {{invsq2, 0.0}, {invsq2, 0.0}, + {invsq2, 0.0}, {-invsq2, 0.0}}; + // CX gate + const std::vector> h_gateCX {{1.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, + {0.0, 0.0}, {1.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, + {0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {1.0, 0.0}, + {0.0, 0.0}, {0.0, 0.0}, {1.0, 0.0}, {0.0, 0.0}}; + + // Copy quantum gates to Device memory + void *d_gateH{nullptr}, *d_gateCX{nullptr}; + HANDLE_CUDA_ERROR(cudaMalloc(&d_gateH, 4 * (2 * fp64size))); + if (verbose) + std::cout << "H gate buffer allocated on GPU: " << d_gateH << std::endl; //debug + HANDLE_CUDA_ERROR(cudaMalloc(&d_gateCX, 16 * (2 * fp64size))); + if (verbose) + std::cout << "CX gate buffer allocated on GPU: " << d_gateCX << std::endl; //debug + if (verbose) + std::cout << "Allocated quantum gate memory on GPU\n"; + HANDLE_CUDA_ERROR(cudaMemcpy(d_gateH, h_gateH.data(), 4 * (2 * fp64size), cudaMemcpyHostToDevice)); + HANDLE_CUDA_ERROR(cudaMemcpy(d_gateCX, h_gateCX.data(), 16 * (2 * fp64size), cudaMemcpyHostToDevice)); + if (verbose) + std::cout << "Copied quantum gates to GPU memory\n"; + + // Sphinx: Sampler #7 + + // Create the initial quantum state + cutensornetState_t quantumState; + HANDLE_CUTN_ERROR(cutensornetCreateState(cutnHandle, CUTENSORNET_STATE_PURITY_PURE, numQubits, qubitDims.data(), + CUDA_C_64F, &quantumState)); + if (verbose) + std::cout << "Created the initial quantum state\n"; + + // Sphinx: Sampler #8 + + // Construct the quantum circuit state (apply quantum gates) + int64_t id; + HANDLE_CUTN_ERROR(cutensornetStateApplyTensor(cutnHandle, quantumState, 1, std::vector{{0}}.data(), + d_gateH, nullptr, 1, 0, 1, &id)); + for(int32_t i = 1; i < numQubits; ++i) { + HANDLE_CUTN_ERROR(cutensornetStateApplyTensor(cutnHandle, quantumState, 2, std::vector{{i-1,i}}.data(), + d_gateCX, nullptr, 1, 0, 1, &id)); + } + if (verbose) + std::cout << "Applied quantum gates\n"; + + // Sphinx: Sampler #9 + + // Create the quantum circuit sampler + cutensornetStateSampler_t sampler; + HANDLE_CUTN_ERROR(cutensornetCreateSampler(cutnHandle, quantumState, numQubits, nullptr, &sampler)); + if (verbose) + std::cout << "Created the quantum circuit sampler\n"; + + // Sphinx: Sampler #10 + + // Query the free memory on Device + std::size_t freeSize {0}, totalSize {0}; + HANDLE_CUDA_ERROR(cudaMemGetInfo(&freeSize, &totalSize)); + std::size_t scratchSize = size_t(double(freeSize) * 0.9) / 8; // assume max of 8 GPUs per node + scratchSize -= scratchSize % 4096; + void *d_scratch {nullptr}; + HANDLE_CUDA_ERROR(cudaMalloc(&d_scratch, scratchSize)); + if (verbose) + std::cout << "Allocated " << scratchSize << " bytes of scratch memory on GPU: " + << "[" << d_scratch << ":" << (void*)(((char*)(d_scratch)) + scratchSize) << ")\n"; + + // Sphinx: Sampler #11 + + // Configure the quantum circuit sampler + const int32_t numHyperSamples = 8; // desired number of hyper samples used in the tensor network contraction path finder + HANDLE_CUTN_ERROR(cutensornetSamplerConfigure(cutnHandle, sampler, + CUTENSORNET_SAMPLER_OPT_NUM_HYPER_SAMPLES, &numHyperSamples, sizeof(numHyperSamples))); + + // Sphinx: Sampler #12 + + // Prepare the quantum circuit sampler + cutensornetWorkspaceDescriptor_t workDesc; + HANDLE_CUTN_ERROR(cutensornetCreateWorkspaceDescriptor(cutnHandle, &workDesc)); + HANDLE_CUTN_ERROR(cutensornetSamplerPrepare(cutnHandle, sampler, scratchSize, workDesc, 0x0)); + if (verbose) + std::cout << "Prepared the quantum circuit state sampler\n"; + + // Sphinx: Sampler #13 + + // Attach the workspace buffer + int64_t worksize {0}; + HANDLE_CUTN_ERROR(cutensornetWorkspaceGetMemorySize(cutnHandle, + workDesc, + CUTENSORNET_WORKSIZE_PREF_RECOMMENDED, + CUTENSORNET_MEMSPACE_DEVICE, + CUTENSORNET_WORKSPACE_SCRATCH, + &worksize)); + assert(worksize > 0); + if(worksize <= scratchSize) { + HANDLE_CUTN_ERROR(cutensornetWorkspaceSetMemory(cutnHandle, workDesc, CUTENSORNET_MEMSPACE_DEVICE, + CUTENSORNET_WORKSPACE_SCRATCH, d_scratch, worksize)); + }else{ + std::cout << "ERROR: Insufficient workspace size on Device!\n"; + std::abort(); + } + if (verbose) + std::cout << "Set the workspace buffer\n"; + + // Sphinx: Sampler #14 + + // Sample the quantum circuit state + std::vector samples(numQubits * numSamples); // samples[SampleId][QubitId] reside in Host memory + HANDLE_CUTN_ERROR(cutensornetSamplerSample(cutnHandle, sampler, numSamples, workDesc, samples.data(), 0)); + if (verbose) { + std::cout << "Performed quantum circuit state sampling\n"; + std::cout << "Bit-string samples:\n"; + for(int64_t i = 0; i < numSamples; ++i) { + for(int64_t j = 0; j < numQubits; ++j) std::cout << " " << samples[i * numQubits + j]; + std::cout << std::endl; + } + } + + // Sphinx: Sampler #15 + + // Destroy the workspace descriptor + HANDLE_CUTN_ERROR(cutensornetDestroyWorkspaceDescriptor(workDesc)); + if (verbose) + std::cout << "Destroyed the workspace descriptor\n"; + + // Destroy the quantum circuit sampler + HANDLE_CUTN_ERROR(cutensornetDestroySampler(sampler)); + if (verbose) + std::cout << "Destroyed the quantum circuit state sampler\n"; + + // Destroy the quantum circuit state + HANDLE_CUTN_ERROR(cutensornetDestroyState(quantumState)); + if (verbose) + std::cout << "Destroyed the quantum circuit state\n"; + + HANDLE_CUDA_ERROR(cudaFree(d_scratch)); + HANDLE_CUDA_ERROR(cudaFree(d_gateCX)); + HANDLE_CUDA_ERROR(cudaFree(d_gateH)); + if (verbose) + std::cout << "Freed memory on GPU\n"; + + // Finalize the cuTensorNet library + HANDLE_CUTN_ERROR(cutensornetDestroy(cutnHandle)); + if (verbose) + std::cout << "Finalized the cuTensorNet library\n"; + + // Finalize the MPI library + HANDLE_MPI_ERROR(MPI_Finalize()); + + return 0; +}