diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f1a8d15..a4b805e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,12 +1,14 @@ # Contributing -Thank you for your interest in contributing to cuQuantum Python! Based on the type of contribution, it will fall into two categories: +Thank you for your interest in contributing to cuQuantum Python! Based on the type of contribution, it will fall into three categories: 1. You want to report a bug, feature request, or documentation issue - - File an [issue](https://github.com/NVIDIA/cuQuantum/issues/new) + - File an [issue](https://github.com/NVIDIA/cuQuantum/issues) describing what you encountered or what you want to see changed. - The NVIDIA team will evaluate the issues and triage them, scheduling them for a release. If you believe the issue needs priority attention comment on the issue to notify the team. 2. You want to implement a feature or bug-fix - At this time we do not accept code contributions. +3. You want to share your nice work built upon cuQuantum: + - We would love to hear more about your work! Please share with us on [NVIDIA/cuQuantum GitHub Discussions](https://github.com/NVIDIA/cuQuantum/discussions>)! We also take any cuQuantum-related questions on this forum. diff --git a/python/CONTRIBUTING.md b/python/CONTRIBUTING.md index f1a8d15..a4b805e 100644 --- a/python/CONTRIBUTING.md +++ b/python/CONTRIBUTING.md @@ -1,12 +1,14 @@ # Contributing -Thank you for your interest in contributing to cuQuantum Python! Based on the type of contribution, it will fall into two categories: +Thank you for your interest in contributing to cuQuantum Python! Based on the type of contribution, it will fall into three categories: 1. You want to report a bug, feature request, or documentation issue - - File an [issue](https://github.com/NVIDIA/cuQuantum/issues/new) + - File an [issue](https://github.com/NVIDIA/cuQuantum/issues) describing what you encountered or what you want to see changed. - The NVIDIA team will evaluate the issues and triage them, scheduling them for a release. If you believe the issue needs priority attention comment on the issue to notify the team. 2. You want to implement a feature or bug-fix - At this time we do not accept code contributions. +3. You want to share your nice work built upon cuQuantum: + - We would love to hear more about your work! Please share with us on [NVIDIA/cuQuantum GitHub Discussions](https://github.com/NVIDIA/cuQuantum/discussions>)! We also take any cuQuantum-related questions on this forum. diff --git a/python/README.md b/python/README.md index d723737..7c146ad 100644 --- a/python/README.md +++ b/python/README.md @@ -5,19 +5,7 @@ Please visit the [NVIDIA cuQuantum Python documentation](https://docs.nvidia.com/cuda/cuquantum/python). -## Building - -### Requirements - -Build-time dependencies of the cuQuantum Python package and some versions that -are known to work are as follows: - -* CUDA Toolkit 11.x -* cuQuantum 22.07+ -* cuTENSOR 1.5.0+ -* Python 3.8+ -* Cython - e.g. 0.29.21 -* [packaging](https://packaging.pypa.io/en/latest/) +## Installation ### Install cuQuantum Python from conda-forge @@ -33,26 +21,53 @@ Alternatively, assuming you already have a Python environment set up (it doesn't you can also install cuQuantum Python this way: ``` -pip install cuquantum-python +pip install cuquantum-python-cu11 ``` -The `pip` solver will also install both cuTENSOR and cuQuantum for you. +The `pip` solver will also install all dependencies for you (including both cuTENSOR and cuQuantum wheels). + +Notes: -Note: To properly install the wheels the environment variable `CUQUANTUM_ROOT` must not be set. +- User can still install cuQuantum Python using `pip install cuquantum-python`, which currently points to the `cuquantum-python-cu11` wheel that is subject to change in the future. Installing wheels with the `-cuXX` suffix is encouraged. +- To manually manage all Python dependencies, append `--no-deps` to `pip install` to bypass the `pip` solver, see below. -### Install cuQuantum Python from source +### Building and installing cuQuantum Python from source + +#### Requirements + +The build-time dependencies of the cuQuantum Python package include: + +* CUDA Toolkit 11.x +* cuStateVec 1.1.0+ +* cuTensorNet 2.0.0+ +* cuTENSOR 1.5.0+ +* Python 3.8+ +* Cython >=0.29.22,<3 +* pip 21.3.1+ +* [packaging](https://packaging.pypa.io/en/latest/) +* setuptools 61.0.0+ +* wheel 0.34.0+ + +Except for CUDA and Python, the rest of the build-time dependencies are handled by the new PEP-517-based build system (see Step 7 below). To compile and install cuQuantum Python from source, please follow the steps below: -1. Set `CUDA_PATH` to point to your CUDA installation -2. Set `CUQUANTUM_ROOT` to point to your cuQuantum installation -3. Set `CUTENSOR_ROOT` to point to your cuTENSOR installation -4. Make sure CUDA, cuQuantum and cuTENSOR are visible in your `LD_LIBRARY_PATH` -5. Run `pip install -v .` +1. Clone the [NVIDIA/cuQuantum](https://github.com/NVIDIA/cuQuantum) repository: `git clone https://github.com/NVIDIA/cuQuantum` +2. Set `CUDA_PATH` to point to your CUDA installation +3. [optional] Set `CUQUANTUM_ROOT` to point to your cuQuantum installation +4. [optional] Set `CUTENSOR_ROOT` to point to your cuTENSOR installation +5. [optional] Make sure cuQuantum and cuTENSOR are visible in your `LD_LIBRARY_PATH` +6. Switch to the directory containing the Python implementation: `cd cuQuantum/python` +7. Build and install: + - Run `pip install .` if you skip Step 3-5 above + - Run `pip install -v --no-deps --no-build-isolation .` otherwise (advanced) Notes: -- For the `pip install` step, adding the `-e` flag after `-v` would allow installing the package in-place (i.e., in "editable mode" for testing/developing). -- If `CUSTATEVEC_ROOT` and `CUTENSORNET_ROOT` are set (for the cuStateVec and the cuTensorNet libraries, respectively), they overwrite `CUQUANTUM_ROOT`. -- For local development, set `CUQUANTUM_IGNORE_SOLVER=1` to ignore the dependency on the `cuquantum` wheel. +- For Step 7, if you are building from source for testing/developing purposes you'd likely want to insert a `-e` flag before the last period (so `pip ... .` becomes `pip ... -e .`): + * `-e`: use the "editable" (in-place) mode + * `-v`: enable more verbose output + * `--no-deps`: avoid installing the *run-time* dependencies + * `--no-build-isolation`: reuse the current Python environment instead of creating a new one for building the package (this avoids installing any *build-time* dependencies) +- As an alternative to setting `CUQUANTUM_ROOT`, `CUSTATEVEC_ROOT` and `CUTENSORNET_ROOT` can be set to point to the cuStateVec and the cuTensorNet libraries, respectively. The latter two environment variables take precedence if defined. ## Running @@ -64,14 +79,16 @@ Runtime dependencies of the cuQuantum Python package include: * An NVIDIA GPU with compute capability 7.0+ * Driver: Linux (450.80.02+) * CUDA Toolkit 11.x -* cuQuantum 22.07+ -* cuTENSOR 1.5.0+ +* cuStateVec 1.1.0+ +* cuTensorNet 2.0.0+ +* cuTENSOR 1.6.1+ * Python 3.8+ * NumPy v1.19+ * CuPy v9.5.0+ (see [installation guide](https://docs.cupy.dev/en/stable/install.html)) * PyTorch v1.10+ (optional, see [installation guide](https://pytorch.org/get-started/locally/)) * Qiskit v0.24.0+ (optional, see [installation guide](https://qiskit.org/documentation/getting_started.html)) * Cirq v0.6.0+ (optional, see [installation guide](https://quantumai.google/cirq/install)) +* mpi4py v3.1.0+ (optional, see [installation guide](https://mpi4py.readthedocs.io/en/stable/install.html)) If you install everything from conda-forge, the dependencies are taken care for you (except for the driver). @@ -102,4 +119,4 @@ variable `CUDA_PATH` is not set. ## Citing cuQuantum -Pleae click this Zenodo badge to see the citation format: [![DOI](https://zenodo.org/badge/435003852.svg)](https://zenodo.org/badge/latestdoi/435003852) +Pleae click this Zenodo badge to see the citation format: [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.6385574.svg)](https://doi.org/10.5281/zenodo.6385574) diff --git a/python/builder/__init__.py b/python/builder/__init__.py new file mode 100644 index 0000000..ab19887 --- /dev/null +++ b/python/builder/__init__.py @@ -0,0 +1,39 @@ +# Copyright (c) 2021-2022, NVIDIA CORPORATION & AFFILIATES +# +# SPDX-License-Identifier: BSD-3-Clause + + +# How does the build system for cuquantum-python work? +# +# - When building a wheel ("pip wheel", "pip install .", or "python setup.py +# bdist_wheel" (discouraged!)), we want to build against the cutensor & +# cuquantum wheels that would be installed to site-packages, so we need +# two things: +# 1. make them the *build-time* dependencies +# 2. set up linker flags to modify rpaths +# +# - For 1. we opt in to use PEP-517, as setup_requires is known to not work +# automatically for users. This is the "price" we pay (by design of +# PEP-517), as it creates a new, "isolated" environment (referred to as +# build isolation) to which all build-time dependencies that live on PyPI +# are installed. Another "price" (also by design) is in the non-editable +# mode (without the "-e" flag) it always builds a wheel for installation. +# +# - For 2. the solution is to create our own bdist_wheel (called first) and +# build_ext (called later) commands. The former would inform the latter +# whether we are building a wheel. +# +# - There is an escape hatch for 1. which is to set "--no-build-isolation". +# Then, users are expected to set CUQUANTUM_ROOT (or CUSTATEVEC_ROOT & +# CUTENSORNET_ROOT) and manage all build-time dependencies themselves. +# This, together with "-e", would not produce any wheel, which is the old +# behavior offered by the environment variable CUQUANTUM_IGNORE_SOLVER=1 +# that we removed and no longer works. +# +# - In any case, the custom build_ext command is in use, which would compute +# the needed compiler flags (depending on it's building a wheel or not) +# and overwrite the incoming Extension instances. +# +# - In any case, the dependencies (on PyPI wheels) are set up by default, +# and "--no-deps" can be passed as usual to tell pip to ignore the +# *run-time* dependencies. diff --git a/python/builder/pep517.py b/python/builder/pep517.py new file mode 100644 index 0000000..276a456 --- /dev/null +++ b/python/builder/pep517.py @@ -0,0 +1,44 @@ +# Copyright (c) 2021-2022, NVIDIA CORPORATION & AFFILIATES +# +# SPDX-License-Identifier: BSD-3-Clause + +# This module implements basic PEP 517 backend support, see e.g. +# - https://peps.python.org/pep-0517/ +# - https://setuptools.pypa.io/en/latest/build_meta.html#dynamic-build-dependencies-and-other-build-meta-tweaks +# Specifically, there are 5 APIs required to create a proper build backend, see below. +# For now it's mostly a pass-through to setuptools, except that we need to determine +# some dependencies at build time. +# +# Note that we purposely do not implement the PEP-660 API hooks so that "pip install ... +# --no-build-isolation -e ." behaves as expected (in-place build/installation without +# creating a wheel). This may require pip>21.3.0. + +from packaging.version import Version +from setuptools import build_meta as _build_meta + +import utils # this is builder.utils (the build system has sys.path set up) + + +prepare_metadata_for_build_wheel = _build_meta.prepare_metadata_for_build_wheel +build_wheel = _build_meta.build_wheel +build_sdist = _build_meta.build_sdist + + +# Note: this function returns a list of *build-time* dependencies, so it's not affected +# by "--no-deps" based on the PEP-517 design. +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.1', # ">=1.1.0,<2" + f'cutensornet-cu{utils.cuda_major_ver}~=2.0', # ">=2.0.0,<3" + ] + + return _build_meta.get_requires_for_build_wheel(config_settings) + cuqnt_require + + +# Note: We have never promised to support sdist (CUQNT-514). We really cannot +# care less about the correctness here. If we are lucky, setuptools would do +# the right thing for us, but even if it's wrong let's not worry about it. +def get_requires_for_build_sdist(config_settings=None): + return _build_meta.get_requires_for_build_sdist(config_settings) diff --git a/python/builder/utils.py b/python/builder/utils.py new file mode 100644 index 0000000..b028a2b --- /dev/null +++ b/python/builder/utils.py @@ -0,0 +1,205 @@ +# Copyright (c) 2021-2022, NVIDIA CORPORATION & AFFILIATES +# +# SPDX-License-Identifier: BSD-3-Clause + +import os +import re +import site +import sys + +from packaging.version import Version +from setuptools.command.build_ext import build_ext as _build_ext +from wheel.bdist_wheel import bdist_wheel as _bdist_wheel + + +# Get __version__ variable +source_root = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(source_root, '..', 'cuquantum', '_version.py')) as f: + exec(f.read()) +cuqnt_py_ver = __version__ +cuqnt_py_ver_obj = Version(cuqnt_py_ver) +cuqnt_ver_major_minor = f"{cuqnt_py_ver_obj.major}.{cuqnt_py_ver_obj.minor}" + +del __version__, cuqnt_py_ver_obj, source_root + + +# We can't assume users to have CTK installed via pip, so we really need this... +# TODO(leofang): try /usr/local/cuda? +try: + cuda_path = os.environ['CUDA_PATH'] +except KeyError as e: + raise RuntimeError('CUDA is not found, please set $CUDA_PATH') from e + + +def check_cuda_version(): + try: + # We cannot do a dlopen and call cudaRuntimeGetVersion, because it + # requires GPUs. We also do not want to rely on the compiler utility + # provided in distutils (deprecated) or setuptools, as this is a very + # simple string parsing task. + # TODO: switch to cudaRuntimeGetVersion once it's fixed (nvbugs 3624208) + cuda_h = os.path.join(cuda_path, 'include', 'cuda.h') + with open(cuda_h, 'r') as f: + cuda_h = f.read() + m = re.search('#define CUDA_VERSION ([0-9]*)', cuda_h) + if m: + ver = int(m.group(1)) + else: + raise RuntimeError("cannot parse CUDA_VERSION") + except: + raise + else: + # 11020 -> "11.2" + return str(ver // 1000) + '.' + str((ver % 100) // 10) + + +# We only support CUDA 11 in v22.11 +cuda_ver = check_cuda_version() +if cuda_ver == '11.0': + cutensor_ver = cuda_ver + cuda_major_ver = '11' +elif '11.0' < cuda_ver < '12.0': + cutensor_ver = '11' + cuda_major_ver = '11' +else: + raise RuntimeError(f"Unsupported CUDA version: {cuda_ver}") + + +building_wheel = False + + +class bdist_wheel(_bdist_wheel): + + def run(self): + global building_wheel + building_wheel = True + super().run() + + +class build_ext(_build_ext): + + def _set_library_roots(self): + custatevec_root = cutensornet_root = cutensor_root = None + # Note that we need sys.path because of build isolation (since PEP 517) + py_paths = sys.path + [site.getusersitepackages()] + site.getsitepackages() + + # search order: + # 1. installed "cuquantum" package + # 2. env var + for path in py_paths: + path = os.path.join(path, 'cuquantum') + if os.path.isdir(os.path.join(path, 'include')): + custatevec_root = cutensornet_root = path + break + else: + # We allow setting CUSTATEVEC_ROOT and CUTENSORNET_ROOT separately for the ease + # of development, but users are encouraged to either install cuquantum from PyPI + # or conda, or set CUQUANTUM_ROOT to the existing installation. + cuquantum_root = os.environ.get('CUQUANTUM_ROOT') + try: + custatevec_root = os.environ['CUSTATEVEC_ROOT'] + except KeyError as e: + if cuquantum_root is None: + raise RuntimeError('cuStateVec is not found, please set $CUQUANTUM_ROOT ' + 'or $CUSTATEVEC_ROOT') from e + else: + custatevec_root = cuquantum_root + try: + cutensornet_root = os.environ['CUTENSORNET_ROOT'] + except KeyError as e: + if cuquantum_root is None: + raise RuntimeError('cuTensorNet is not found, please set $CUQUANTUM_ROOT ' + 'or $CUTENSORNET_ROOT') from e + else: + cutensornet_root = cuquantum_root + + # search order: + # 1. installed "cutensor" package + # 2. env var + for path in py_paths: + path = os.path.join(path, 'cutensor') + if os.path.isdir(os.path.join(path, 'include')): + cutensor_root = path + break + else: + try: + cutensor_root = os.environ['CUTENSOR_ROOT'] + except KeyError as e: + raise RuntimeError('cuTENSOR is not found, please set $CUTENSOR_ROOT') from e + + return custatevec_root, cutensornet_root, cutensor_root + + def _prep_includes_libs_rpaths(self): + """ + Set global vars cusv_incl_dir, cutn_incl_dir, cusv_lib_dir, cutn_lib_dir, + cusv_lib, cutn_lib, and extra_linker_flags. + """ + custatevec_root, cutensornet_root, cutensor_root = self._set_library_roots() + + global cusv_incl_dir, cutn_incl_dir + cusv_incl_dir = [os.path.join(cuda_path, 'include'), + os.path.join(custatevec_root, 'include')] + cutn_incl_dir = [os.path.join(cuda_path, 'include'), + os.path.join(cutensornet_root, 'include')] + + global cusv_lib_dir, cutn_lib_dir + # we include both lib64 and lib to accommodate all possible sources + cusv_lib_dir = [os.path.join(custatevec_root, 'lib'), + os.path.join(custatevec_root, 'lib64')] + cutn_lib_dir = [os.path.join(cutensornet_root, 'lib'), + os.path.join(cutensornet_root, 'lib64'), + os.path.join(cutensor_root, 'lib'), # wheel + os.path.join(cutensor_root, 'lib', cutensor_ver)] # tarball + + global cusv_lib, cutn_lib, extra_linker_flags + if not building_wheel: + # Note: with PEP-517 the editable mode would not build a wheel for installation + # (and we purposely do not support PEP-660). + cusv_lib = ['custatevec'] + cutn_lib = ['cutensornet', 'cutensor'] + extra_linker_flags = [] + else: + # Note: soname = library major version + # We don't need to link to cuBLAS/cuSOLVER at build time (TODO: perhaps cuTENSOR too...?) + cusv_lib = [':libcustatevec.so.1'] + cutn_lib = [':libcutensornet.so.2', ':libcutensor.so.1'] + # The rpaths must be adjusted given the following full-wheel installation: + # - cuquantum-python: site-packages/cuquantum/{custatevec, cutensornet}/ [=$ORIGIN] + # - cusv & cutn: site-packages/cuquantum/lib/ + # - cutensor: site-packages/cutensor/lib/ + # - cublas: site-packages/nvidia/cublas/lib/ + # - cusolver: site-packages/nvidia/cusolver/lib/ + # (Note that starting v22.11 we use the new wheel format, so all lib wheels have suffix -cuXX, + # and cuBLAS/cuSOLVER additionally have prefix nvidia-.) + ldflag = "-Wl,--disable-new-dtags," + ldflag += "-rpath,$ORIGIN/../lib," + ldflag += "-rpath,$ORIGIN/../../cutensor/lib," + ldflag += "-rpath,$ORIGIN/../../nvidia/cublas/lib," + ldflag += "-rpath,$ORIGIN/../../nvidia/cusolver/lib" + extra_linker_flags = [ldflag] + + print("\n"+"*"*80) + print("CUDA version:", cuda_ver) + print("CUDA path:", cuda_path) + print("cuStateVec path:", custatevec_root) + print("cuTensorNet path:", cutensornet_root) + print("cuTENSOR path:", cutensor_root) + print("*"*80+"\n") + + def build_extension(self, ext): + if ext.name.endswith("custatevec"): + ext.include_dirs = cusv_incl_dir + ext.library_dirs = cusv_lib_dir + ext.libraries = cusv_lib + ext.extra_link_args = extra_linker_flags + elif ext.name.endswith("cutensornet"): + ext.include_dirs = cutn_incl_dir + ext.library_dirs = cutn_lib_dir + ext.libraries = cutn_lib + ext.extra_link_args = extra_linker_flags + + super().build_extension(ext) + + def build_extensions(self): + self._prep_includes_libs_rpaths() + super().build_extensions() diff --git a/python/cuquantum/__init__.py b/python/cuquantum/__init__.py index 2ba5f96..6576381 100644 --- a/python/cuquantum/__init__.py +++ b/python/cuquantum/__init__.py @@ -5,7 +5,7 @@ from cuquantum import custatevec from cuquantum import cutensornet from cuquantum.cutensornet import ( - contract, contract_path, einsum, einsum_path, Network, BaseCUDAMemoryManager, MemoryPointer, + contract, contract_path, einsum, einsum_path, tensor_qualifiers_dtype, Network, BaseCUDAMemoryManager, MemoryPointer, NetworkOptions, OptimizerInfo, OptimizerOptions, PathFinderOptions, ReconfigOptions, SlicerOptions, CircuitToEinsum) from cuquantum.utils import ComputeType, cudaDataType, libraryPropertyType from cuquantum._version import __version__ @@ -27,6 +27,11 @@ cutensornet.GraphAlgo, cutensornet.MemoryModel, cutensornet.OptimizerCost, + cutensornet.TensorSVDConfigAttribute, + cutensornet.TensorSVDNormalization, + cutensornet.TensorSVDPartition, + cutensornet.TensorSVDInfoAttribute, + cutensornet.GateSplitAlgo, ): cutensornet._internal.enum_utils.add_enum_class_doc(enum, chomp="_ATTRIBUTE|_PREFERENCE_ATTRIBUTE") diff --git a/python/cuquantum/__main__.py b/python/cuquantum/__main__.py new file mode 100644 index 0000000..6b063dc --- /dev/null +++ b/python/cuquantum/__main__.py @@ -0,0 +1,107 @@ +# Copyright (c) 2021-2022, NVIDIA CORPORATION & AFFILIATES +# +# SPDX-License-Identifier: BSD-3-Clause + +import argparse +import os +import site +import sys + +import cuquantum # get the shared libraries loaded + + +def get_lib_path(name): + """Get the loaded shared library path.""" + # Ideally we should call dl_iterate_phdr or dladdr to do the job, but this + # is simpler and not bad; the former two are not strictly portable anyway + # (not part of POSIX). Obviously this only works on Linux! + try: + with open('/proc/self/maps') as f: + lib_map = f.read() + except FileNotFoundError as e: + raise NotImplementedError("This utility is available only on Linux.") from e + lib = set() + for line in lib_map.split('\n'): + if name in line: + fields = line.split() + lib.add(fields[-1]) # pathname is the last field, check "man proc" + if len(lib) == 0: + raise ValueError(f"library {name} is not loaded") + elif len(lib) > 1: + # This could happen when, e.g., a library exists in both the user env + # and LD_LIBRARY_PATH, and somehow both copies get loaded. This is a + # messy problem, but let's work around it by assuming the one in the + # user env is preferred. + lib2 = set() + for s in [site.getusersitepackages()] + site.getsitepackages(): + for path in lib: + if path.startswith(s): + lib2.add(path) + if len(lib2) != 1: + raise RuntimeError(f"cannot find the unique copy of {name}: {lib}") + else: + lib = lib2 + return lib.pop() + + +def _get_cuquantum_libs(): + paths = set() + for lib in ('custatevec', 'cutensornet', 'cutensor'): + path = os.path.normpath(get_lib_path(f"lib{lib}.so")) + paths.add(path) + return tuple(paths) + + +def _get_cuquantum_includes(): + paths = set() + for path in _get_cuquantum_libs(): + path = os.path.normpath(os.path.join(os.path.dirname(path), '..')) + if not os.path.isdir(os.path.join(path, 'include')): + path = os.path.normpath(os.path.join(path, '../include')) + else: + path = os.path.join(path, 'include') + assert os.path.isdir(path), f"path={path} is invalid" + paths.add(path) + return tuple(paths) + + +def _get_cuquantum_target(target): + target = f"lib{target}.so" + libs = [os.path.basename(lib) for lib in _get_cuquantum_libs()] + for lib in libs: + if target in lib: + lib = '.'.join(lib.split('.')[:3]) # keep SONAME + flag = f"-l:{lib} " + break + else: + assert False + return flag + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--includes', action='store_true', + help='get cuQuantum include flags') + parser.add_argument('--libs', action='store_true', + help='get cuQuantum linker flags') + parser.add_argument('--target', action='append', default=[], + choices=('custatevec', 'cutensornet'), + help='get the linker flag for the target cuQuantum component') + args = parser.parse_args() + + if not sys.argv[1:]: + parser.print_help() + sys.exit(1) + if args.includes: + out = ' '.join(f"-I{path}" for path in _get_cuquantum_includes()) + print(out, end=' ') + if args.libs: + paths = set([os.path.dirname(path) for path in _get_cuquantum_libs()]) + out = ' '.join(f"-L{path}" for path in paths) + print(out, end=' ') + flag = '' + for target in args.target: + flag += _get_cuquantum_target(target) + if target == 'cutensornet': + flag += _get_cuquantum_target('cutensor') + print(flag) diff --git a/python/cuquantum/_version.py b/python/cuquantum/_version.py index bb5b86c..732ec2b 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__ = '22.07.1' # the last digit is for cuQuantum Python only +__version__ = '22.11.0' diff --git a/python/cuquantum/cutensornet/__init__.py b/python/cuquantum/cutensornet/__init__.py index 3f85e3a..162bf55 100644 --- a/python/cuquantum/cutensornet/__init__.py +++ b/python/cuquantum/cutensornet/__init__.py @@ -7,3 +7,4 @@ from cuquantum.cutensornet.memory import * from cuquantum.cutensornet.tensor_network import * from cuquantum.cutensornet.circuit_converter import * +from cuquantum.cutensornet._internal.utils import get_mpi_comm_pointer diff --git a/python/cuquantum/cutensornet/_internal/circuit_converter_utils.py b/python/cuquantum/cutensornet/_internal/circuit_converter_utils.py index a293bde..f427e72 100644 --- a/python/cuquantum/cutensornet/_internal/circuit_converter_utils.py +++ b/python/cuquantum/cutensornet/_internal/circuit_converter_utils.py @@ -4,15 +4,15 @@ try: import cirq - from . import cirq_parser_utils + from . import circuit_parser_utils_cirq except ImportError: - cirq = cirq_parser_utils = None + cirq = circuit_parser_utils_cirq = None import cupy as cp try: import qiskit - from . import qiskit_parser_utils + from . import circuit_parser_utils_qiskit except ImportError: - qiskit = qiskit_parser_utils = None + qiskit = circuit_parser_utils_qiskit = None from .tensor_wrapper import _get_backend_asarray_func @@ -26,7 +26,7 @@ def check_version(package_name, version, minimum_version): """ - Check if the current version of a package is above the required minimum + Check if the current version of a package is above the required minimum. """ version_numbers = [int(i) for i in version.split('.')] minimum_version_numbers = [int(i) for i in minimum_version.split('.')] @@ -52,11 +52,11 @@ def infer_parser(circuit): if qiskit and isinstance(circuit, qiskit.QuantumCircuit): qiskit_version = qiskit.__qiskit_version__['qiskit'] # qiskit metapackage version check_version('qiskit', qiskit_version, QISKIT_MIN_VERSION) - return qiskit_parser_utils + return circuit_parser_utils_qiskit elif cirq and isinstance(circuit, cirq.Circuit): cirq_version = cirq.__version__ check_version('cirq', cirq_version, CIRQ_MIN_VERSION) - return cirq_parser_utils + return circuit_parser_utils_cirq else: base = circuit.__module__.split('.')[0] raise NotImplementedError(f'circuit from {base} not supported') @@ -91,7 +91,7 @@ def parse_bitstring(bitstring, n_qubits=None): def parse_fixed_qubits(fixed): """ - Given a set of qubits with fixed states, return the output bitstring and corresponding qubits order + Given a set of qubits with fixed states, return the output bitstring and corresponding qubits order. """ if fixed: fixed_qubits, fixed_bitstring = zip(*fixed.items()) @@ -133,19 +133,49 @@ def get_bitstring_tensors(bitstring, dtype='complex128', backend=cp): def convert_mode_labels_to_expression(input_mode_labels, output_mode_labels): """ - Create an Einsum expression from input and output index labels + Create an Einsum expression from input and output index labels. Args: input_mode_labels: A sequence of mode labels for each input tensor. output_mode_labels: The desired mode labels for the output tensor. Returns: - An Einsum expression in explicit form + An Einsum expression in explicit form. """ input_symbols = [''.join(map(_get_symbol, idx)) for idx in input_mode_labels] expression = ','.join(input_symbols) + '->' + ''.join(map(_get_symbol, output_mode_labels)) return expression +def get_pauli_gates(pauli_map, dtype='complex128', backend=cp): + """ + Populate the gates for all pauli operators. + + Args: + pauli_map: A dictionary mapping qubits to pauli operators. + dtype: Data type for the tensor operands. + backend: The package the tensor operands belong to. + + Returns: + A sequence of pauli gates. + """ + asarray = _get_backend_asarray_func(backend) + pauli_i = asarray([[1,0], [0,1]], dtype=dtype) + pauli_x = asarray([[0,1], [1,0]], dtype=dtype) + pauli_y = asarray([[0,-1j], [1j,0]], dtype=dtype) + pauli_z = asarray([[1,0], [0,-1]], dtype=dtype) + + operand_map = {'I': pauli_i, + 'X': pauli_x, + 'Y': pauli_y, + 'Z': pauli_z} + gates = [] + for qubit, pauli_char in pauli_map.items(): + operand = operand_map.get(pauli_char) + if operand is None: + raise ValueError('pauli string character must be one of I/X/Y/Z') + gates.append((operand, (qubit,))) + return gates + def parse_gates_to_mode_labels_operands( gates, qubits_frontier, diff --git a/python/cuquantum/cutensornet/_internal/cirq_parser_utils.py b/python/cuquantum/cutensornet/_internal/circuit_parser_utils_cirq.py similarity index 71% rename from python/cuquantum/cutensornet/_internal/cirq_parser_utils.py rename to python/cuquantum/cutensornet/_internal/circuit_parser_utils_cirq.py index e8342c0..68c7c6b 100644 --- a/python/cuquantum/cutensornet/_internal/cirq_parser_utils.py +++ b/python/cuquantum/cutensornet/_internal/circuit_parser_utils_cirq.py @@ -49,7 +49,7 @@ def unfold_circuit(circuit, dtype='complex128', backend=cp): gate_qubits = operation.qubits tensor = unitary(operation).reshape((2,) * 2 * len(gate_qubits)) tensor = asarray(tensor, dtype=dtype) - gates.append([tensor, operation.qubits]) + gates.append((tensor, operation.qubits)) return qubits, gates def get_lightcone_circuit(circuit, coned_qubits): @@ -64,25 +64,16 @@ def get_lightcone_circuit(circuit, coned_qubits): A :class:`cirq.Circuit` object that potentially contains less number of gates """ coned_qubits = set(coned_qubits) + all_operations = list(circuit.all_operations()) n_qubits = len(circuit.all_qubits()) - moments = [] - reversed_moments = circuit.moments[::-1] - n_moments = len(reversed_moments) - for ix, moment in enumerate(reversed_moments): - if len(coned_qubits) == n_qubits: - moments.extend(reversed_moments[ix:]) - break - reduced_moment = [] - reversed_operations = moment.operations[::-1] - n_operations = len(reversed_operations) - for iy, operation in enumerate(reversed_operations): - if len(coned_qubits) == n_qubits: - reduced_moment.extend(reversed_operations[iy:]) - break - qubit_set = set(operation.qubits) - if qubit_set & coned_qubits: - reduced_moment.append(operation) - coned_qubits |= qubit_set - moments.append(Moment(reduced_moment[::-1])) - newqc = Circuit(moments[::-1]) + ix = len(all_operations) + tail_operations = [] + while len(coned_qubits) != n_qubits and ix>0: + ix -= 1 + operation = all_operations[ix] + qubit_set = set(operation.qubits) + if qubit_set & coned_qubits: + tail_operations.append(operation) + coned_qubits |= qubit_set + newqc = Circuit(all_operations[:ix]+tail_operations[::-1]) return newqc diff --git a/python/cuquantum/cutensornet/_internal/qiskit_parser_utils.py b/python/cuquantum/cutensornet/_internal/circuit_parser_utils_qiskit.py similarity index 63% rename from python/cuquantum/cutensornet/_internal/qiskit_parser_utils.py rename to python/cuquantum/cutensornet/_internal/circuit_parser_utils_qiskit.py index 5294263..eb0caad 100644 --- a/python/cuquantum/cutensornet/_internal/qiskit_parser_utils.py +++ b/python/cuquantum/cutensornet/_internal/circuit_parser_utils_qiskit.py @@ -4,14 +4,14 @@ import cupy as cp from qiskit import QuantumCircuit -from qiskit.circuit import Barrier, ControlledGate, Delay, Gate, Instruction, Measure +from qiskit.circuit import Barrier, ControlledGate, Delay, Gate, Measure from qiskit.extensions import UnitaryGate from .tensor_wrapper import _get_backend_asarray_func def remove_measurements(circuit): """ - Return a circuit with final measurement operations removed + Return a circuit with final measurement operations removed. """ circuit = circuit.copy() circuit.remove_final_measurements() @@ -22,39 +22,26 @@ def remove_measurements(circuit): def get_inverse_circuit(circuit): """ - Return a circuit with all gate operations inversed + Return a circuit with all gate operations inversed. """ return circuit.inverse() -def unfold_circuit(circuit, dtype='complex128', qubit_map=None, gates=None, backend=cp): +def get_decomposed_gates(circuit, qubit_map=None, gates=None, gate_process_func=None): """ - Unfold the circuit to obtain the qubits and all gate tensors. All :class:`qiskit.circuit.Gate` and - :class:`qiskit.circuit.Instruction` in the circuit will be decomposed into either standard gates or customized unitary gates. - Barrier and delay operations will be discarded. - - Args: - circuit: A :class:`qiskit.QuantumCircuit` object. All parameters in the circuit must be binded. - dtype: Data type for the tensor operands. - backend: The package the tensor operands belong to. - - Returns: - All qubits and gate operations from the input circuit + Return the gate sequence for the given circuit. Compound gates/instructions will be decomposed + to either standard gates or customized unitary gates. """ if gates is None: gates = [] - asarray = _get_backend_asarray_func(backend) - qubits = circuit.qubits for operation, gate_qubits, _ in circuit: if qubit_map: gate_qubits = [qubit_map[q] for q in gate_qubits] if isinstance(operation, Gate): if 'standard_gate' in str(type(operation)) or isinstance(operation, UnitaryGate): - tensor = operation.to_matrix().reshape((2,2)*len(gate_qubits)) - tensor = asarray(tensor, dtype=dtype) - if isinstance(operation, ControlledGate): - # in qiskit notation, qubit at high index is the target qubit - gate_qubits = gate_qubits[::-1] - gates.append([tensor, gate_qubits]) + if callable(gate_process_func): + gates.append(gate_process_func(operation, gate_qubits)) + else: + gates.append((operation, gate_qubits)) continue else: if isinstance(operation, (Barrier, Delay)): @@ -63,10 +50,37 @@ def unfold_circuit(circuit, dtype='complex128', qubit_map=None, gates=None, back elif not isinstance(operation.definition, QuantumCircuit): # Instruction as composite gate raise ValueError(f'operation type {type(operation)} not supported') - # for composite gate, must provide a map from the sub circuit to the original circuit next_qubit_map = dict(zip(operation.definition.qubits, gate_qubits)) - _, gates = unfold_circuit(operation.definition, dtype=dtype, qubit_map=next_qubit_map, gates=gates, backend=backend) + gates = get_decomposed_gates(operation.definition, qubit_map=next_qubit_map, gates=gates, gate_process_func=gate_process_func) + return gates + +def unfold_circuit(circuit, dtype='complex128', backend=cp): + """ + Unfold the circuit to obtain the qubits and all gate tensors. All :class:`qiskit.circuit.Gate` and + :class:`qiskit.circuit.Instruction` in the circuit will be decomposed into either standard gates or customized unitary gates. + Barrier and delay operations will be discarded. + + Args: + circuit: A :class:`qiskit.QuantumCircuit` object. All parameters in the circuit must be binded. + dtype: Data type for the tensor operands. + backend: The package the tensor operands belong to. + + Returns: + All qubits and gate operations from the input circuit + """ + asarray = _get_backend_asarray_func(backend) + qubits = circuit.qubits + + def gate_process_func(operation, gate_qubits): + tensor = operation.to_matrix().reshape((2,2)*len(gate_qubits)) + tensor = asarray(tensor, dtype=dtype) + if isinstance(operation, ControlledGate): + # in qiskit notation, qubit at high index is the target qubit + gate_qubits = gate_qubits[::-1] + return tensor, gate_qubits + + gates = get_decomposed_gates(circuit, gate_process_func=gate_process_func) return qubits, gates @@ -82,18 +96,17 @@ def get_lightcone_circuit(circuit, coned_qubits): A :class:`qiskit.QuantumCircuit` object that potentially contains less number of gates """ coned_qubits = set(coned_qubits) - reverse_coned_operations = [] - newqc = circuit.copy() - newqc.data = [] - for ix, (operation, gate_qubits, _) in enumerate(circuit[::-1]): - if len(coned_qubits) == circuit.num_qubits: - # when all qubits are coned, all inner gates are preserved - newqc.data = circuit.data[:len(circuit)-ix] - break + gates = get_decomposed_gates(circuit) + newqc = QuantumCircuit(circuit.qubits) + ix = len(gates) + tail_operations = [] + while len(coned_qubits) != circuit.num_qubits and ix>0: + ix -= 1 + operation, gate_qubits = gates[ix] qubit_set = set(gate_qubits) if qubit_set & coned_qubits: - reverse_coned_operations.append([operation, gate_qubits, _]) + tail_operations.append([operation, gate_qubits]) coned_qubits |= qubit_set - - newqc.data.extend(reverse_coned_operations[::-1]) + for operation, gate_qubits in gates[:ix] + tail_operations[::-1]: + newqc.append(operation, gate_qubits) return newqc diff --git a/python/cuquantum/cutensornet/_internal/einsum_parser.py b/python/cuquantum/cutensornet/_internal/einsum_parser.py index 3b58e05..ad00880 100644 --- a/python/cuquantum/cutensornet/_internal/einsum_parser.py +++ b/python/cuquantum/cutensornet/_internal/einsum_parser.py @@ -8,6 +8,7 @@ from collections import Counter from itertools import chain +import string import numpy as np @@ -59,7 +60,7 @@ def parse_single(single): """ Parse single operand mode labels considering ellipsis. Leading or trailing whitespace, if present, is removed. """ - subexpr = single.strip().split('...') + subexpr = single.strip(string.whitespace).split('...') n = len(subexpr) expr = [[Ellipsis]] * (2*n - 1) expr[::2] = subexpr @@ -73,7 +74,7 @@ def check_single(single): for s in single: if s is Ellipsis: continue - if s.isspace() or s in disallowed_labels: + if s in string.whitespace or s in disallowed_labels: return False return True diff --git a/python/cuquantum/cutensornet/_internal/enum_utils.py b/python/cuquantum/cutensornet/_internal/enum_utils.py index cfb0856..25adda7 100644 --- a/python/cuquantum/cutensornet/_internal/enum_utils.py +++ b/python/cuquantum/cutensornet/_internal/enum_utils.py @@ -72,6 +72,17 @@ def create_options_class_from_enum(options_class_name: str, enum_class: IntEnum, return options_class +def snake_to_camel(names): + name = "" + for i, sub_name in enumerate(names): + if i == 0: + name += sub_name.lower() + else: + name += sub_name[0].upper() + sub_name[1:] + name += "_t" + return name + + def camel_to_snake(name, upper=True): """ Convert string from camel case to snake style. diff --git a/python/cuquantum/cutensornet/_internal/optimizer_ifc.py b/python/cuquantum/cutensornet/_internal/optimizer_ifc.py index fa9cf48..84710e4 100644 --- a/python/cuquantum/cutensornet/_internal/optimizer_ifc.py +++ b/python/cuquantum/cutensornet/_internal/optimizer_ifc.py @@ -9,6 +9,7 @@ __all__ = ['OptimizerInfoInterface'] from collections.abc import Sequence +import itertools import operator import numpy as np @@ -16,14 +17,17 @@ from cuquantum import cutensornet as cutn -def _parse_and_map_sliced_modes(sliced_modes, mode_map_user_to_ord, size_dict, dtype_mode=np.int32, dtype_extent=np.int64): +def _parse_and_map_sliced_modes(sliced_modes, mode_map_user_to_ord, size_dict): """ - Parse user-provided sliced modes and create individual, contiguous sliced_modes and sliced extents array. + Parse user-provided sliced modes, create and return a contiguous (sliced mode, slide extent) array of + type `cutn.cutensornet.slice_info_pair_dtype`. """ num_sliced_modes = len(sliced_modes) + slice_info_array = np.empty((num_sliced_modes,), dtype=cutn.cutensornet.slice_info_pair_dtype) + if num_sliced_modes == 0: - return num_sliced_modes, np.zeros((num_sliced_modes,), dtype=dtype_mode), np.zeros((num_sliced_modes,), dtype=dtype_extent) + return slice_info_array # The sliced modes have already passed basic checks when creating the OptimizerOptions dataclass. @@ -31,7 +35,7 @@ def _parse_and_map_sliced_modes(sliced_modes, mode_map_user_to_ord, size_dict, d if pairs: sliced_modes, sliced_extents = zip(*sliced_modes) else: - sliced_extents = np.ones((num_sliced_modes,), dtype=dtype_extent) + sliced_extents = (1,) # Check for invalid mode labels. invalid_modes = tuple(filter(lambda k: k not in mode_map_user_to_ord, sliced_modes)) @@ -39,19 +43,20 @@ def _parse_and_map_sliced_modes(sliced_modes, mode_map_user_to_ord, size_dict, d message = f"Invalid sliced mode labels: {invalid_modes}" raise ValueError(message) - sliced_modes = np.asarray([mode_map_user_to_ord[m] for m in sliced_modes], dtype=dtype_mode) - remainder = tuple(size_dict[m] % e for m, e in zip(sliced_modes, sliced_extents)) - if any(remainder): + slice_info_array["sliced_mode"] = sliced_modes = [mode_map_user_to_ord[m] for m in sliced_modes] + remainder = any(size_dict[m] % e for m, e in itertools.zip_longest(sliced_modes, sliced_extents, fillvalue=1)) + if remainder: raise ValueError("The sliced extents must evenly divide the original extents of the corresponding mode.") + slice_info_array["sliced_extent"] = sliced_extents - return num_sliced_modes, sliced_modes, np.asanyarray(sliced_extents, dtype=dtype_extent) + return slice_info_array InfoEnum = cutn.ContractionOptimizerInfoAttribute -class OptimizerInfoInterface(object): - """ - """ + +class OptimizerInfoInterface: + def __init__(self, network): """ """ @@ -63,10 +68,11 @@ def __init__(self, network): self._largest_tensor = np.zeros((1,), dtype=get_dtype(InfoEnum.LARGEST_TENSOR)) self._num_slices = np.zeros((1,), dtype=get_dtype(InfoEnum.NUM_SLICES)) self._num_sliced_modes = np.zeros((1,), dtype=get_dtype(InfoEnum.NUM_SLICED_MODES)) + self._slicing_config = np.zeros((1,), dtype=get_dtype(InfoEnum.SLICING_CONFIG)) self._slicing_overhead = np.zeros((1,), dtype=get_dtype(InfoEnum.SLICING_OVERHEAD)) self.num_contraction = len(self.network.operands) - 1 - self._path = np.zeros((2*self.num_contraction, ), dtype=np.int32) + self._path = np.zeros((1,), dtype=get_dtype(InfoEnum.PATH)) @staticmethod def _get_scalar_attribute(network, name, attribute): @@ -97,13 +103,6 @@ def num_slices(self): return int(self._num_slices) - @num_slices.setter - def num_slices(self, number): - """ - Set the number of slices in the network. - """ - OptimizerInfoInterface._set_scalar_attribute(network, InfoEnum.NUM_SLICES, self._num_slices, number) - @property def flop_count(self): """ @@ -137,16 +136,11 @@ def path(self): """ Return the contraction path in linear format. """ + path = np.empty((2*self.num_contraction,), dtype=np.int32) + self._path["data"] = path.ctypes.data + OptimizerInfoInterface._get_scalar_attribute(self.network, InfoEnum.PATH, self._path) - network = self.network - - path_wrapper = cutn.ContractionPath(self.num_contraction, self._path.ctypes.data) - size = path_wrapper.get_size() - cutn.contraction_optimizer_info_get_attribute(network.handle, network.optimizer_info_ptr, InfoEnum.PATH, path_wrapper.get_path(), size) - - path = list(zip(*[iter(self._path)]*2)) - - return path + return list(zip(*[iter(path)]*2)) @path.setter def path(self, path): @@ -164,10 +158,13 @@ def path(self, path): 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) - self._path = np.array(path, dtype=np.int32) - path_wrapper = cutn.ContractionPath(num_contraction, self._path.ctypes.data) - size = path_wrapper.get_size() - cutn.contraction_optimizer_info_set_attribute(network.handle, network.optimizer_info_ptr, InfoEnum.PATH, path_wrapper.get_path(), size) + path_array = np.asarray(path, dtype=np.int32) + + # Construct the path type. + path = np.array((num_contraction, path_array.ctypes.data), dtype=get_dtype(InfoEnum.PATH)) + + # Set the attribute. + OptimizerInfoInterface._set_scalar_attribute(self.network, InfoEnum.PATH, self._path, path) @property def num_sliced_modes(self): @@ -178,13 +175,6 @@ def num_sliced_modes(self): return int(self._num_sliced_modes) - @num_sliced_modes.setter - def num_sliced_modes(self, number): - """ - Set the number of sliced_modes in the network. - """ - OptimizerInfoInterface._set_scalar_attribute(self.network, InfoEnum.NUM_SLICED_MODES, self._num_sliced_modes, number) - @property def sliced_mode_extent(self): """ @@ -197,14 +187,15 @@ def sliced_mode_extent(self): num_sliced_modes = self.num_sliced_modes - sliced_modes = np.zeros((num_sliced_modes,), dtype=get_dtype(InfoEnum.SLICED_MODE)) - size = num_sliced_modes * sliced_modes.dtype.itemsize - cutn.contraction_optimizer_info_get_attribute(network.handle, network.optimizer_info_ptr, InfoEnum.SLICED_MODE, sliced_modes.ctypes.data, size) - sliced_modes = tuple(network.mode_map_ord_to_user[m] for m in sliced_modes) # Convert to user mode labels + slice_info_array = np.empty((num_sliced_modes,), dtype=cutn.cutensornet.slice_info_pair_dtype) - sliced_extents = np.zeros((num_sliced_modes,), dtype=get_dtype(InfoEnum.SLICED_EXTENT)) - size = num_sliced_modes * sliced_extents.dtype.itemsize - cutn.contraction_optimizer_info_get_attribute(network.handle, network.optimizer_info_ptr, InfoEnum.SLICED_EXTENT, sliced_extents.ctypes.data, size) + slicing_config = self._slicing_config + slicing_config["num_sliced_modes"] = num_sliced_modes + slicing_config["data"] = slice_info_array.ctypes.data + OptimizerInfoInterface._get_scalar_attribute(self.network, InfoEnum.SLICING_CONFIG, slicing_config) + + sliced_modes = tuple(network.mode_map_ord_to_user[m] for m in slice_info_array["sliced_mode"]) # Convert to user mode labels + sliced_extents = slice_info_array["sliced_extent"] return tuple(zip(sliced_modes, sliced_extents)) @@ -216,18 +207,16 @@ def sliced_mode_extent(self, sliced_modes): sliced_mode = sequence of sliced modes, or sequence of (sliced mode, sliced extent) pairs """ - network = self.network - - num_sliced_modes, sliced_modes, sliced_extents = _parse_and_map_sliced_modes(sliced_modes, network.mode_map_user_to_ord, network.size_dict) + get_dtype = cutn.contraction_optimizer_info_get_attribute_dtype - # Set the number of sliced modes first - self.num_sliced_modes = num_sliced_modes + network = self.network - size = num_sliced_modes * sliced_modes.dtype.itemsize - cutn.contraction_optimizer_info_set_attribute(network.handle, network.optimizer_info_ptr, InfoEnum.SLICED_MODE, sliced_modes.ctypes.data, size) + # Construct the slicing config type. + slice_info_array = _parse_and_map_sliced_modes(sliced_modes, network.mode_map_user_to_ord, network.size_dict) + slicing_config = np.array((len(slice_info_array), slice_info_array.ctypes.data), dtype=get_dtype(InfoEnum.SLICING_CONFIG)) - size = num_sliced_modes * sliced_extents.dtype.itemsize - cutn.contraction_optimizer_info_set_attribute(network.handle, network.optimizer_info_ptr, InfoEnum.SLICED_EXTENT, sliced_extents.ctypes.data, size) + # Set the attribute. + OptimizerInfoInterface._set_scalar_attribute(network, InfoEnum.SLICING_CONFIG, self._slicing_config, slicing_config) @property def intermediate_modes(self): diff --git a/python/cuquantum/cutensornet/_internal/package_ifc_cupy.py b/python/cuquantum/cutensornet/_internal/package_ifc_cupy.py index bfc0f67..d6025a8 100644 --- a/python/cuquantum/cutensornet/_internal/package_ifc_cupy.py +++ b/python/cuquantum/cutensornet/_internal/package_ifc_cupy.py @@ -10,6 +10,7 @@ import cupy as cp +from . import utils from .package_ifc import Package @@ -17,7 +18,7 @@ class CupyPackage(Package): @staticmethod def get_current_stream(device_id): - with cp.cuda.Device(device_id): + with utils.device_ctx(device_id): stream = cp.cuda.get_current_stream() return stream @@ -35,6 +36,6 @@ def create_external_stream(device_id, stream_ptr): @staticmethod def create_stream(device_id): - with cp.cuda.Device(device_id): + with utils.device_ctx(device_id): stream = cp.cuda.Stream(null=False, non_blocking=False, ptds=False) return stream diff --git a/python/cuquantum/cutensornet/_internal/tensor_ifc_cupy.py b/python/cuquantum/cutensornet/_internal/tensor_ifc_cupy.py index c284f20..e607790 100644 --- a/python/cuquantum/cutensornet/_internal/tensor_ifc_cupy.py +++ b/python/cuquantum/cutensornet/_internal/tensor_ifc_cupy.py @@ -11,6 +11,7 @@ import cupy import numpy +from . import utils from .tensor_ifc import Tensor @@ -61,7 +62,15 @@ def empty(cls, shape, **context): name = context.get('dtype', 'float32') dtype = CupyTensor.name_to_dtype[name] device = context.get('device', None) - with cupy.cuda.Device(device=device): + + if isinstance(device, cupy.cuda.Device): + device_id = device.id + elif isinstance(device, int): + device_id = device + else: + 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) return tensor @@ -77,7 +86,7 @@ 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 cupy.cuda.Device(device): + with utils.device_ctx(device): tensor_device = cupy.asarray(self.tensor) return tensor_device diff --git a/python/cuquantum/cutensornet/_internal/tensor_ifc_numpy.py b/python/cuquantum/cutensornet/_internal/tensor_ifc_numpy.py index 8d2843d..ea218d6 100644 --- a/python/cuquantum/cutensornet/_internal/tensor_ifc_numpy.py +++ b/python/cuquantum/cutensornet/_internal/tensor_ifc_numpy.py @@ -11,8 +11,10 @@ import cupy import numpy +from . import utils from .tensor_ifc import Tensor + class NumpyTensor(Tensor): """ Tensor wrapper for numpy ndarrays. @@ -73,7 +75,7 @@ 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 cupy.cuda.Device(device): + with utils.device_ctx(device): tensor_device = cupy.asarray(self.tensor) return tensor_device diff --git a/python/cuquantum/cutensornet/_internal/utils.py b/python/cuquantum/cutensornet/_internal/utils.py index 930b938..b4ef794 100644 --- a/python/cuquantum/cutensornet/_internal/utils.py +++ b/python/cuquantum/cutensornet/_internal/utils.py @@ -6,8 +6,10 @@ A collection of (internal use) helper functions. """ +import contextlib +import ctypes import functools -from typing import Callable, Dict, Optional +from typing import Callable, Dict, Mapping, Optional import cupy as cp import numpy as np @@ -17,6 +19,7 @@ from . import package_wrapper from . import tensor_wrapper + def infer_object_package(obj): """ Infer the package that defines this object. @@ -55,13 +58,42 @@ def _create_stream_ctx_ptr_cupy_stream(package_ifc, stream): return stream, stream_ctx, stream_ptr -def get_or_create_stream(device, stream, op_package): +@contextlib.contextmanager +def device_ctx(new_device_id): + """ + Semantics: + + 1. The device context manager makes the specified device current from the point of entry until the point of exit. + + 2. When the context manager exits, the current device is reset to what it was when the context manager was entered. + + 3. Any explicit setting of the device within the context manager (using cupy.cuda.Device().use(), torch.cuda.set_device(), + etc) will overrule the device set by the context manager from that point onwards till the context manager exits. In + other words, the context manager provides a local device scope and the current device can be explicitly reset for the + remainder of that scope. + + Corollary: if any library function resets the device globally and this is an undesired side-effect, such functions must be + called from within the device context manager. + + Device context managers can be arbitrarily nested. + """ + old_device_id = cp.cuda.runtime.getDevice() + try: + if old_device_id != new_device_id: + cp.cuda.runtime.setDevice(new_device_id) + yield + finally: + # We should always restore the old device at exit. + cp.cuda.runtime.setDevice(old_device_id) + + +def get_or_create_stream(device_id, stream, op_package): """ Create a stream object from a stream pointer or extract the stream pointer from a stream object, or use the current stream. Args: - device: The device (CuPy object) for the stream. + device_id: The device ID. stream: A stream object, stream pointer, or None. op_package: The package the tensor network operands belong to. @@ -69,7 +101,6 @@ def get_or_create_stream(device, stream, op_package): tuple: CuPy stream object, package stream context, stream pointer. """ - device_id = device.id op_package_ifc = package_wrapper.PACKAGE[op_package] if stream is None: stream = op_package_ifc.get_current_stream(device_id) @@ -134,11 +165,10 @@ def get_memory_limit(memory_limit, device): def get_operands_data(operands): """ - Get the raw data pointer of the input operands and their alignment for cutensornet. + Get the raw data pointer of the input operands for cuTensorNet. """ op_data = tuple(o.data_ptr for o in operands) - alignments = tuple(get_maximal_alignment(p) for p in op_data) - return op_data, alignments + return op_data def create_empty_tensor(cls, extents, dtype, device_id, stream_ctx): @@ -155,30 +185,27 @@ def create_empty_tensor(cls, extents, dtype, device_id, stream_ctx): return tensor -def create_output_tensor(cls, package, output, size_dict, device, data_type): +def create_output_tensor(cls, package, output, size_dict, device_id, data_type): """ - Create output tensor and associated data (modes, extents, strides, alignment). This operation is + Create output tensor and associated data (modes, extents, strides). This operation is blocking and is safe to use with asynchronous memory pools. """ modes = tuple(m for m in output) extents = tuple(size_dict[m] for m in output) package_ifc = package_wrapper.PACKAGE[package] - device_id = device.id stream = package_ifc.create_stream(device_id) stream, stream_ctx, _ = _create_stream_ctx_ptr_cupy_stream(package_ifc, stream) - with device: + with device_ctx(device_id): start = stream.record() output = create_empty_tensor(cls, extents, data_type, device_id, stream_ctx) end = stream.record() end.synchronize() strides = output.strides - alignment = get_maximal_alignment(output.data_ptr) - - return output, modes, extents, strides, alignment + return output, modes, extents, strides def get_network_device_id(operands): @@ -204,6 +231,7 @@ def get_operands_dtype(operands): return dtype +# Unused since cuQuantum 22.11 def get_maximal_alignment(address): """ Calculate the maximal alignment of the provided memory location. @@ -242,6 +270,7 @@ def check_operands_match(orig_operands, new_operands, attribute, description): raise ValueError(message) +# Unused since cuQuantum 22.11 def check_alignments_match(orig_alignments, new_alignments): """ Check if alignment matches between the corresponding new and old operands, and raise an exception if it doesn't. @@ -257,6 +286,30 @@ def check_alignments_match(orig_alignments, new_alignments): raise ValueError(message) +def check_tensor_qualifiers(qualifiers, dtype, num_inputs): + """ + Check if the tensor qualifiers array is valid. + """ + + if qualifiers is None: + return 0 + + prolog = f"The tensor qualifiers must be specified as an one-dimensional NumPy ndarray of 'tensor_qualifiers_dtype' objects." + if not isinstance(qualifiers, np.ndarray): + raise ValueError(prolog) + elif qualifiers.dtype != dtype: + message = prolog + f" The dtype of the ndarray is '{qualifiers.dtype}'." + raise ValueError(message) + elif qualifiers.ndim != 1: + message = prolog + f" The shape of the ndarray is {qualifiers.shape}." + raise ValueError(message) + elif len(qualifiers) != num_inputs: + message = prolog + f" The length of the ndarray is {len(qualifiers)}, while the expected length is {num_inputs}." + raise ValueError(message) + + return qualifiers + + def check_autotune_params(iterations): """ Check if the autotune parameters are of the correct type and within range. @@ -285,6 +338,77 @@ def get_ptr_from_memory_pointer(mem_ptr): raise AttributeError(message) +class Value: + """ + A simple value wrapper holding a default value. + """ + def __init__(self, default, *, validator: Callable[[object], bool]): + """ + Args: + default: The default value to use. + validator: A callable that validates the provided value. + """ + self.validator = validator + self._data = default + + @property + def data(self): + return self._data + + @data.setter + def data(self, value): + self._data = self._validate(value) + + def _validate(self, value): + if self.validator(value): + return value + raise ValueError(f"Internal Error: value '{value}' is not valid.") + + +def check_and_set_options(required: Mapping[str, Value], provided: Mapping[str, object]): + """ + Update each option specified in 'required' by getting the value from 'provided' if it exists or using a default. + """ + for option, value in required.items(): + try: + value.data = provided.pop(option) + except KeyError: + pass + required[option] = value.data + + assert not provided, "Unrecognized options." + + +@contextlib.contextmanager +def cuda_call_ctx(stream, 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. + + An `end` event is recorded after the CUDA call for use in establishing stream ordering for non-blocking calls. This + 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. + """ + if blocking: + start = cp.cuda.Event(disable_timing = False if timing else True) + stream.record(start) + + end = cp.cuda.Event(disable_timing = False if timing and blocking else True) + + time = Value(None, validator=lambda v: True) + yield end, time + + stream.record(end) + + if not blocking: + return + + end.synchronize() + + if timing: + time.data = cp.cuda.get_elapsed_time(start, end) + + # Decorator definitions def atomic(handler: Callable[[Optional[object]], None], method: bool = False) -> Callable: @@ -361,3 +485,28 @@ def inner(*args, **kwargs): return outer +def get_mpi_comm_pointer(comm): + """Simple helper to get the address to and size of a ``MPI_Comm`` handle. + + Args: + comm (mpi4py.MPI.Comm): An MPI communicator. + + Returns: + tuple: A pair of int values representing the address and the size. + """ + # We won't initialize MPI for users in any case + try: + import mpi4py + init = mpi4py.rc.initialize + mpi4py.rc.initialize = False + from mpi4py import MPI + except ImportError as e: + raise RuntimeError("please install mpi4py") from e + finally: + mpi4py.rc.initialize = init + + if not isinstance(comm, MPI.Comm): + raise ValueError("invalid MPI communicator") + comm_ptr = MPI._addressof(comm) # = MPI_Comm* + mpi_comm_size = MPI._sizeof(MPI.Comm) + return comm_ptr, mpi_comm_size diff --git a/python/cuquantum/cutensornet/circuit_converter.py b/python/cuquantum/cutensornet/circuit_converter.py index 8cbf653..d359c18 100644 --- a/python/cuquantum/cutensornet/circuit_converter.py +++ b/python/cuquantum/cutensornet/circuit_converter.py @@ -8,7 +8,9 @@ __all__ = ['CircuitToEinsum'] +import collections.abc import importlib +import warnings import numpy as np @@ -120,7 +122,33 @@ def state_vector(self, fixed=EMPTY_DICT): The Einstein summation expression and a list of tensor operands. The order of the output mode labels is consistent with :attr:`CircuitToEinsum.qubits`. For :class:`cirq.Circuit`, this order corresponds to all qubits in the circuit sorted in ascending order. For :class:`qiskit.QuantumCircuit`, this order is the same as :attr:`qiskit.QuantumCircuit.qubits`. + + .. note:: + + The kwargs "fixed" is deprecated and will be removed in the future; please switch to :meth:`CircuitToEinsum.batched_amplitudes` for the same functionality. + """ + if fixed: + warnings.warn("The kwargs \"fixed\" is deprecated and will be removed in the future; please " + "switch to CircuitToEinsum.batched_amplitudes() for the same functionality.") + elif fixed is None: + fixed = dict() + + return self.batched_amplitudes(fixed) + + def batched_amplitudes(self, fixed): """ + Generate the Einstein summation expression and tensor operands to compute a batch of bitstring amplitudes for the input circuit. + + Args: + fixed: A dictionary that maps certain qubits to the corresponding fixed states 0 or 1. + + Returns: + The Einstein summation expression and a list of tensor operands. The order of the output mode labels is consistent with :attr:`CircuitToEinsum.qubits`. + For :class:`cirq.Circuit`, this order corresponds to all qubits in the circuit sorted in ascending order. + For :class:`qiskit.QuantumCircuit`, this order is the same as :attr:`qiskit.QuantumCircuit.qubits`. + """ + if not isinstance(fixed, collections.abc.Mapping): + raise TypeError('fixed must be a dictionary') input_mode_labels, input_operands, qubits_frontier = self._get_inputs() fixed_qubits, fixed_bitstring = circ_utils.parse_fixed_qubits(fixed) @@ -130,7 +158,7 @@ def state_vector(self, fixed=EMPTY_DICT): operands = input_operands + circ_utils.get_bitstring_tensors(fixed_bitstring, dtype=self.dtype, backend=self.backend) output_mode_labels = [qubits_frontier[q] for q in self.qubits if q not in fixed] - expression = circ_utils.convert_mode_labels_to_expression(mode_labels, output_mode_labels=output_mode_labels) + expression = circ_utils.convert_mode_labels_to_expression(mode_labels, output_mode_labels) return expression, operands def amplitude(self, bitstring): @@ -151,7 +179,7 @@ def amplitude(self, bitstring): mode_labels = input_mode_labels + [[qubits_frontier[q]] for q in self.qubits] output_mode_labels = [] - expression = circ_utils.convert_mode_labels_to_expression(mode_labels, output_mode_labels=output_mode_labels) + expression = circ_utils.convert_mode_labels_to_expression(mode_labels, output_mode_labels) operands = input_operands + circ_utils.get_bitstring_tensors(bitstring, dtype=self.dtype, backend=self.backend) return expression, operands @@ -179,23 +207,9 @@ def reduced_density_matrix(self, where, fixed=EMPTY_DICT, lightcone=True): .. seealso:: `unitary reverse lightcone cancellation `_ """ - parser = self.parser n_qubits = self.n_qubits - - if lightcone: - coned_qubits = list(where) + list(fixed.keys()) - circuit = parser.get_lightcone_circuit(self.circuit, coned_qubits) - _, gates = parser.unfold_circuit(circuit, dtype=self.dtype, backend=self.backend) - # in cirq, the lightcone circuit may only contain a subset of the original qubits - # It's imperative to use qubits=self.qubits to generate the input tensors - input_mode_labels, input_operands, qubits_frontier = circ_utils.parse_inputs(self.qubits, gates, self.dtype, self.backend) - else: - circuit = self.circuit - input_mode_labels, input_operands, qubits_frontier = self._get_inputs() - # avoid inplace modification on metadata - qubits_frontier = qubits_frontier.copy() - - next_frontier = max(qubits_frontier.values()) + 1 + coned_qubits = list(where) + list(fixed.keys()) + input_mode_labels, input_operands, qubits_frontier, next_frontier, inverse_gates = self._get_forward_inverse_metadata(lightcone, coned_qubits) # handle tensors/mode labels for qubits with fixed state fixed_qubits, fixed_bitstring = circ_utils.parse_fixed_qubits(fixed) @@ -214,9 +228,6 @@ def reduced_density_matrix(self, where, fixed=EMPTY_DICT, lightcone=True): qubits_frontier[iqubit] = next_frontier next_frontier += 1 - # inverse circuit - inverse_circuit = parser.get_inverse_circuit(circuit) - _, inverse_gates = parser.unfold_circuit(inverse_circuit, dtype=self.dtype, backend=self.backend) igate_mode_labels, igate_operands = circ_utils.parse_gates_to_mode_labels_operands(inverse_gates, qubits_frontier, next_frontier) @@ -232,7 +243,67 @@ def reduced_density_matrix(self, where, fixed=EMPTY_DICT, lightcone=True): output_left_mode_labels.append(left_mode_labels) output_right_mode_labels.append(right_mode_labels) output_mode_labels = output_left_mode_labels + output_right_mode_labels - expression = circ_utils.convert_mode_labels_to_expression(mode_labels, output_mode_labels=output_mode_labels) + expression = circ_utils.convert_mode_labels_to_expression(mode_labels, output_mode_labels) + return expression, operands + + def expectation(self, pauli_string, lightcone=True): + """ + Generate the Einstein summation expression and tensor operands to compute the expectation value of a Pauli + string for the input circuit. + + Unitary reverse lightcone cancellation refers to removing the identity formed by a unitary gate (from + the ket state) and its inverse (from the bra state) when there exists no additional operators + in-between. One can take advantage of this technique to reduce the effective network size by + only including the *causal* gates (gates residing in the lightcone). + + Args: + pauli_string: The Pauli string for expectation value computation. It can be: + + - a sequence of characters ``'I'``/``'X'``/``'Y'``/``'Z'``. The length must be equal to the number of qubits. + - a dictionary mapping the selected qubits to Pauli characters. Qubits not specified are + assumed to be applied with the identity operator ``'I'``. + + lightcone: Whether to apply the unitary reverse lightcone cancellation technique to reduce the number of tensors in expectation value computation. + + Returns: + The Einstein summation expression and a list of tensor operands. + + .. note:: + + When ``lightcone=True``, the identity Pauli operators will be omitted in the output operands. The unitary reverse lightcone cancellation technique is then + applied based on the remaining causal qubits to further reduce the size of the network. The reduction effect depends on the circuit topology and the input Pauli string + (so the contraction path cannot be reused for the contraction of different Pauli strings). When ``lightcone=False``, the identity Pauli operators are preserved in the output operands such that the output tensor network has the identical topology for different Pauli strings, and the contraction path only needs to be computed once and can be reused for all Pauli strings. + + .. seealso:: `unitary reverse lightcone cancellation `_ + """ + if isinstance(pauli_string, collections.abc.Sequence): + if len(pauli_string) != self.n_qubits: + raise ValueError('pauli_string must be of equal size as the number of qubits in the circuit') + pauli_string = dict(zip(self.qubits, pauli_string)) + else: + if not isinstance(pauli_string, collections.abc.Mapping): + raise TypeError('pauli_string must be either a sequence of pauli characters or a dictionary') + + n_qubits = self.n_qubits + if lightcone: + pauli_map = {qubit: pauli_char for qubit, pauli_char in pauli_string.items() if pauli_char!='I'} + else: + pauli_map = pauli_string + coned_qubits = pauli_map.keys() + input_mode_labels, input_operands, qubits_frontier, next_frontier, inverse_gates = self._get_forward_inverse_metadata(lightcone, coned_qubits) + + pauli_gates = circ_utils.get_pauli_gates(pauli_map, dtype=self.dtype, backend=self.backend) + gates = pauli_gates + inverse_gates + + gate_mode_labels, gate_operands = circ_utils.parse_gates_to_mode_labels_operands(gates, + qubits_frontier, + next_frontier) + + mode_labels = input_mode_labels + gate_mode_labels + [[qubits_frontier[ix]] for ix in self.qubits] + operands = input_operands + gate_operands + input_operands[:n_qubits] + + output_mode_labels = [] + expression = circ_utils.convert_mode_labels_to_expression(mode_labels, output_mode_labels) return expression, operands def _get_inputs(self): @@ -248,3 +319,38 @@ def _get_inputs(self): if self._metadata is None: self._metadata = circ_utils.parse_inputs(self.qubits, self.gates, self.dtype, self.backend) return self._metadata + + def _get_forward_inverse_metadata(self, lightcone, coned_qubits): + """parse the metadata for forward and inverse circuit. + + Args: + lightcone: Whether to apply the unitary reverse lightcone cancellation technique to reduce the number of tensors in expectation value computation. + coned_qubits: An iterable of qubits to be coned. + + Returns: + tuple: A 5-tuple (``input_mode_labels``, ``input_operands``, ``qubits_frontier``, ``next_frontier``, ``inverse_gates``): + + - ``input_mode_labels`` : A sequence of mode labels for initial states and gate tensors. + - ``input_operands`` : A sequence of operands for initial states and gate tensors. + - ``qubits_frontier``: A dictionary mapping all qubits to their current mode labels. + - ``next_frontier``: The next mode label to use. + - ``inverse_gates``: A sequence of (operand, qubits) for the inverse circuit. + """ + parser = self.parser + if lightcone: + circuit = parser.get_lightcone_circuit(self.circuit, coned_qubits) + _, gates = parser.unfold_circuit(circuit, dtype=self.dtype, backend=self.backend) + # in cirq, the lightcone circuit may only contain a subset of the original qubits + # It's imperative to use qubits=self.qubits to generate the input tensors + input_mode_labels, input_operands, qubits_frontier = circ_utils.parse_inputs(self.qubits, gates, self.dtype, self.backend) + else: + circuit = self.circuit + input_mode_labels, input_operands, qubits_frontier = self._get_inputs() + # avoid inplace modification on metadata + qubits_frontier = qubits_frontier.copy() + + next_frontier = max(qubits_frontier.values()) + 1 + # inverse circuit + inverse_circuit = parser.get_inverse_circuit(circuit) + _, inverse_gates = parser.unfold_circuit(inverse_circuit, dtype=self.dtype, backend=self.backend) + return input_mode_labels, input_operands, qubits_frontier, next_frontier, inverse_gates \ No newline at end of file diff --git a/python/cuquantum/cutensornet/configuration.py b/python/cuquantum/cutensornet/configuration.py index 98f0527..ed18084 100644 --- a/python/cuquantum/cutensornet/configuration.py +++ b/python/cuquantum/cutensornet/configuration.py @@ -11,7 +11,7 @@ import collections from dataclasses import dataclass from logging import Logger -from typing import Dict, Hashable, Iterable, Mapping, Optional, Tuple, Union +from typing import Dict, Hashable, Iterable, Literal, Mapping, Optional, Tuple, Union import cupy as cp @@ -34,6 +34,10 @@ class NetworkOptions(object): logger (logging.Logger): Python Logger object. The root logger will be used if a logger object is not provided. memory_limit: Maximum memory available to cuTensorNet. It can be specified as a value (with optional suffix like K[iB], M[iB], G[iB]) or as a percentage. The default is 80%. + blocking: A flag specifying the behavior of the execution methods :meth:`Network.autotune` and :meth:`Network.contract`. + When ``blocking`` is ``True``, these methods do not return until the operation is complete. When blocking is ``"auto"``, + the methods return immediately when the input tensors are on the GPU. The execution methods always block when the + input tensors are on the CPU. The default is ``True``. allocator: An object that supports the :class:`BaseCUDAMemoryManager` protocol, used to draw device memory. If an allocator is not provided, a memory allocator from the library package will be used (:func:`torch.cuda.caching_allocator_alloc` for PyTorch operands, :func:`cupy.cuda.alloc` otherwise). @@ -43,6 +47,7 @@ class NetworkOptions(object): handle : Optional[int] = None logger : Optional[Logger] = None memory_limit : Optional[Union[int, str]] = r'80%' + blocking : Literal[True, "auto"] = True allocator : Optional[BaseCUDAMemoryManager] = None def __post_init__(self): @@ -64,6 +69,9 @@ def __post_init__(self): if not (m1 or m2): raise ValueError(MEM_LIMIT_DOC % self.memory_limit) + if self.blocking != True and self.blocking != "auto": + raise ValueError("The value specified for blocking must be either True or 'auto'.") + if self.allocator is not None and not isinstance(self.allocator, BaseCUDAMemoryManager): raise TypeError("The allocator must be an object of type that fulfils the BaseCUDAMemoryManager protocol.") @@ -104,6 +112,8 @@ class OptimizerOptions(object): reconfiguration: Options for the reconfiguration algorithm as a :class:`~cuquantum.ReconfigOptions` object or dict containing the ``(parameter, value)`` items for ``ReconfigOptions``. seed: Optional seed for the random number generator. See `CUTENSORNET_CONTRACTION_OPTIMIZER_CONFIG_SEED`. + cost_function: The objective function to use for finding the optimal contraction path. + See `CUTENSORNET_CONTRACTION_OPTIMIZER_CONFIG_COST_FUNCTION_OBJECTIVE`. """ samples : Optional[int] = None threads : Optional[int] = None @@ -111,6 +121,7 @@ class OptimizerOptions(object): slicing : Optional[Union[SlicerOptions, ModeSequenceType, ModeExtentSequenceType]] = None reconfiguration : Optional[ReconfigOptions] = None seed : Optional[int] = None + cost_function: Optional[int] = None def _check_option(self, option, option_class, checker=None): if isinstance(option, option_class): @@ -160,6 +171,8 @@ def __post_init__(self): self.slicing = self._check_option(self.slicing, SlicerOptions, self._check_specified_slices) self.reconfiguration = self._check_option(self.reconfiguration, ReconfigOptions, None) self._check_int(self.seed, "seed") + if self.cost_function is not None: + self.cost_function = cuquantum.cutensornet.OptimizerCost(self.cost_function) @dataclass diff --git a/python/cuquantum/cutensornet/cutensornet.pxd b/python/cuquantum/cutensornet/cutensornet.pxd index a4bd024..2dbee35 100644 --- a/python/cuquantum/cutensornet/cutensornet.pxd +++ b/python/cuquantum/cutensornet/cutensornet.pxd @@ -7,7 +7,7 @@ # Once we switch over the names would be prettier (in the Cython # layer). -from libc.stdint cimport int32_t +from libc.stdint cimport int32_t, int64_t, uint32_t from cuquantum.utils cimport DataType, DeviceAllocType, DeviceFreeType, Stream @@ -23,14 +23,27 @@ cdef extern from '' nogil: ctypedef void* _ContractionAutotunePreference 'cutensornetContractionAutotunePreference_t' ctypedef void* _WorkspaceDescriptor 'cutensornetWorkspaceDescriptor_t' ctypedef void* _SliceGroup 'cutensornetSliceGroup_t' + ctypedef void* _TensorDescriptor 'cutensornetTensorDescriptor_t' + ctypedef void* _TensorSVDConfig 'cutensornetTensorSVDConfig_t' + ctypedef void* _TensorSVDInfo 'cutensornetTensorSVDInfo_t' # cuTensorNet structs ctypedef struct _NodePair 'cutensornetNodePair_t': int first int second + ctypedef struct _ContractionPath 'cutensornetContractionPath_t': int numContractions _NodePair *data + + ctypedef struct _SliceInfoPair 'cutensornetSliceInfoPair_t': + int32_t slicedMode + int64_t slicedExtent + + ctypedef struct _SlicingConfig 'cutensornetSlicingConfig_t': + uint32_t numSlicedModes + _SliceInfoPair* data + ctypedef struct _DeviceMemHandler 'cutensornetDeviceMemHandler_t': void* ctx DeviceAllocType device_alloc @@ -38,6 +51,10 @@ cdef extern from '' nogil: # Cython limitation: cannot use C defines in declaring a static array, # so we just have to hard-code CUTENSORNET_ALLOCATOR_NAME_LEN here... char name[64] + + ctypedef struct _TensorQualifiers 'cutensornetTensorQualifiers_t': + int32_t isConjugate # cannot assign default value to fields in cdef structs + ctypedef void(*LoggerCallbackData 'cutensornetLoggerCallbackData_t')( int32_t logLevel, const char* functionName, @@ -95,6 +112,7 @@ cdef extern from '' nogil: CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_NUM_INTERMEDIATE_MODES CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_EFFECTIVE_FLOPS_EST CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_RUNTIME_EST + CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_SLICING_CONFIG ctypedef enum _ContractionAutotunePreferenceAttribute 'cutensornetContractionAutotunePreferenceAttributes_t': CUTENSORNET_CONTRACTION_AUTOTUNE_MAX_ITERATIONS @@ -107,6 +125,34 @@ cdef extern from '' nogil: ctypedef enum _Memspace 'cutensornetMemspace_t': CUTENSORNET_MEMSPACE_DEVICE + CUTENSORNET_MEMSPACE_HOST + + ctypedef enum _TensorSVDConfigAttribute 'cutensornetTensorSVDConfigAttributes_t': + CUTENSORNET_TENSOR_SVD_CONFIG_ABS_CUTOFF + CUTENSORNET_TENSOR_SVD_CONFIG_REL_CUTOFF + CUTENSORNET_TENSOR_SVD_CONFIG_S_NORMALIZATION + CUTENSORNET_TENSOR_SVD_CONFIG_S_PARTITION + + ctypedef enum _TensorSVDNormalization 'cutensornetTensorSVDNormalization_t': + CUTENSORNET_TENSOR_SVD_NORMALIZATION_NONE + CUTENSORNET_TENSOR_SVD_NORMALIZATION_L1 + CUTENSORNET_TENSOR_SVD_NORMALIZATION_L2 + CUTENSORNET_TENSOR_SVD_NORMALIZATION_LINF + + ctypedef enum _TensorSVDPartition 'cutensornetTensorSVDPartition_t': + CUTENSORNET_TENSOR_SVD_PARTITION_NONE + CUTENSORNET_TENSOR_SVD_PARTITION_US + CUTENSORNET_TENSOR_SVD_PARTITION_SV + CUTENSORNET_TENSOR_SVD_PARTITION_UV_EQUAL + + ctypedef enum _TensorSVDInfoAttribute 'cutensornetTensorSVDInfoAttributes_t': + CUTENSORNET_TENSOR_SVD_INFO_FULL_EXTENT + CUTENSORNET_TENSOR_SVD_INFO_REDUCED_EXTENT + CUTENSORNET_TENSOR_SVD_INFO_DISCARDED_WEIGHT + + ctypedef enum _GateSplitAlgo 'cutensornetGateSplitAlgo_t': + CUTENSORNET_GATE_SPLIT_ALGO_DIRECT + CUTENSORNET_GATE_SPLIT_ALGO_REDUCED # cuTensorNet consts int CUTENSORNET_MAJOR diff --git a/python/cuquantum/cutensornet/cutensornet.pyx b/python/cuquantum/cutensornet/cutensornet.pyx index d05f5fd..315d530 100644 --- a/python/cuquantum/cutensornet/cutensornet.pyx +++ b/python/cuquantum/cutensornet/cutensornet.pyx @@ -34,13 +34,19 @@ cdef extern from * nogil: # network descriptor int cutensornetCreateNetworkDescriptor( _Handle, int32_t, const int32_t[], const int64_t* const[], - const int64_t* const[], const int32_t* const[], const uint32_t[], + const int64_t* const[], const int32_t* const[], const _TensorQualifiers[], int32_t, const int64_t[], const int64_t[], const int32_t[], - uint32_t, DataType, _ComputeType, _NetworkDescriptor*) + DataType, _ComputeType, _NetworkDescriptor*) int cutensornetDestroyNetworkDescriptor(_NetworkDescriptor) int cutensornetGetOutputTensorDetails( const _Handle, const _NetworkDescriptor, int32_t*, size_t*, int32_t*, int64_t*, int64_t*) + int cutensornetGetOutputTensorDescriptor( + const _Handle, const _NetworkDescriptor, + _TensorDescriptor*) + int cutensornetGetTensorDetails( + const _Handle, const _TensorDescriptor, + int32_t*, size_t*, int32_t*, int64_t*, int64_t*) # workspace descriptor int cutensornetCreateWorkspaceDescriptor( @@ -48,6 +54,9 @@ cdef extern from * nogil: int cutensornetWorkspaceComputeSizes( const _Handle, const _NetworkDescriptor, const _ContractionOptimizerInfo, _WorkspaceDescriptor) + int cutensornetWorkspaceComputeContractionSizes( + const _Handle, const _NetworkDescriptor, + const _ContractionOptimizerInfo, _WorkspaceDescriptor) int cutensornetWorkspaceGetSize( const _Handle, const _WorkspaceDescriptor, _WorksizePref, _Memspace, uint64_t*) @@ -150,6 +159,60 @@ cdef extern from * nogil: int cutensornetLoggerSetMask(int32_t) int cutensornetLoggerForceDisable() + # tensor descriptor + int cutensornetCreateTensorDescriptor( + _Handle, int32_t, const int64_t[], const int64_t[], const int32_t[], + DataType, _TensorDescriptor*) + int cutensornetDestroyTensorDescriptor(_TensorDescriptor) + + # svdConfig + int cutensornetCreateTensorSVDConfig(_Handle, _TensorSVDConfig*) + int cutensornetDestroyTensorSVDConfig(_TensorSVDConfig) + int cutensornetTensorSVDConfigGetAttribute( + _Handle, _TensorSVDConfig, _TensorSVDConfigAttribute, void*, size_t) + int cutensornetTensorSVDConfigSetAttribute( + _Handle, _TensorSVDConfig, _TensorSVDConfigAttribute, void*, size_t) + + # svdInfo + int cutensornetCreateTensorSVDInfo(_Handle, _TensorSVDInfo*) + int cutensornetDestroyTensorSVDInfo(_TensorSVDInfo) + int cutensornetTensorSVDInfoGetAttribute( + _Handle, _TensorSVDInfo, _TensorSVDInfoAttribute, void*, size_t) + + # tensorSVD + int cutensornetWorkspaceComputeSVDSizes( + _Handle, _TensorDescriptor, _TensorDescriptor, _TensorDescriptor, + _TensorSVDConfig, _WorkspaceDescriptor) + int cutensornetTensorSVD( + _Handle, _TensorDescriptor, void*, _TensorDescriptor, void*, void*, + _TensorDescriptor, void*, _TensorSVDConfig, _TensorSVDInfo, + _WorkspaceDescriptor, Stream) + + # tensorQR + int cutensornetWorkspaceComputeQRSizes( + _Handle, _TensorDescriptor, _TensorDescriptor, _TensorDescriptor, + _WorkspaceDescriptor) + int cutensornetTensorQR( + _Handle, _TensorDescriptor, void*, _TensorDescriptor, void*, + _TensorDescriptor, void*, _WorkspaceDescriptor, Stream) + + # gate split + int cutensornetWorkspaceComputeGateSplitSizes( + _Handle, _TensorDescriptor, _TensorDescriptor, _TensorDescriptor, + _TensorDescriptor, _TensorDescriptor, _GateSplitAlgo, + _TensorSVDConfig, _ComputeType, _WorkspaceDescriptor) + int cutensornetGateSplit( + _Handle, _TensorDescriptor, void*, _TensorDescriptor, void*, + _TensorDescriptor, void*, _TensorDescriptor, void*, void*, + _TensorDescriptor, void*, _GateSplitAlgo, _TensorSVDConfig, + _ComputeType, _TensorSVDInfo, _WorkspaceDescriptor, Stream) + + # distributed + int cutensornetDistributedResetConfiguration(_Handle, void*, size_t) + int cutensornetDistributedGetNumRanks(_Handle, int*) + int cutensornetDistributedGetProcRank(_Handle, int*) + int cutensornetDistributedSynchronize(_Handle) + class cuTensorNetError(RuntimeError): def __init__(self, status): @@ -225,9 +288,9 @@ cpdef size_t get_cudart_version() except*: cpdef intptr_t create_network_descriptor( intptr_t handle, int32_t n_inputs, n_modes_in, extents_in, - strides_in, modes_in, alignments_in, + strides_in, modes_in, qualifiers_in, int32_t n_modes_out, extents_out, - strides_out, modes_out, uint32_t alignment_out, + strides_out, modes_out, int data_type, int compute_type) except*: """Create a tensor network descriptor. @@ -261,11 +324,11 @@ cpdef intptr_t create_network_descriptor( to the corresponding tensor's modes - a nested Python sequence of :class:`int` - alignments_in: A host array of alignments for each input tensor. It can + qualifiers_in: A host array of qualifiers for each input tensor. It can be - - an :class:`int` as the pointer address to the array - - a Python sequence of :class:`int` + - an :class:`int` as the pointer address to the numpy array with dtype `tensor_qualifiers_dtype` + - a numpy array with dtype `tensor_qualifiers_dtype` n_modes_out (int32_t): The number of modes of the output tensor. If this is set to -1 and ``modes_out`` is set to 0 (not provided), @@ -286,7 +349,6 @@ cpdef intptr_t create_network_descriptor( - an :class:`int` as the pointer address to the array - a Python sequence of :class:`int` - alignment_out (uint32_t): The alignment for the output tensor. data_type (cuquantum.cudaDataType): The data type of the input and output tensors. compute_type (cuquantum.ComputeType): The compute type of the tensor @@ -388,14 +450,13 @@ cpdef intptr_t create_network_descriptor( # a pointer address, take it as is modesInPtr = modes_in - # alignments_in can be a pointer address, or a Python sequence - cdef vector[uint32_t] alignmentsInData - cdef uint32_t* alignmentsInPtr - if cpython.PySequence_Check(alignments_in): - alignmentsInData = alignments_in - alignmentsInPtr = alignmentsInData.data() - else: # a pointer address - alignmentsInPtr = alignments_in + # qualifiers_in can be a pointer address or a numpy array + cdef _TensorQualifiers* qualifiersInPtr + if isinstance(qualifiers_in, _numpy.ndarray): + assert qualifiers_in.dtype == tensor_qualifiers_dtype + qualifiersInPtr = <_TensorQualifiers*>qualifiers_in.ctypes.data + else: + qualifiersInPtr = <_TensorQualifiers*> qualifiers_in # extents_out can be a pointer address, or a Python sequence cdef vector[int64_t] extentsOutData @@ -427,8 +488,8 @@ cpdef intptr_t create_network_descriptor( cdef _NetworkDescriptor tn_desc with nogil: status = cutensornetCreateNetworkDescriptor(<_Handle>handle, - n_inputs, numModesInPtr, extentsInPtr, stridesInPtr, modesInPtr, alignmentsInPtr, - n_modes_out, extentsOutPtr, stridesOutPtr, modesOutPtr, alignment_out, + n_inputs, numModesInPtr, extentsInPtr, stridesInPtr, modesInPtr, qualifiersInPtr, + n_modes_out, extentsOutPtr, stridesOutPtr, modesOutPtr, data_type, <_ComputeType>compute_type, &tn_desc) check_status(status) return tn_desc @@ -461,6 +522,10 @@ cpdef tuple get_output_tensor_details(intptr_t handle, intptr_t tn_desc): .. seealso:: `cutensornetGetOutputTensorDetails` """ + warnings.warn("cuquantum.cutensornet.get_output_tensor_details() is " + "deprecated and will be removed in a future release; please " + "switch to cuquantum.cutensornet.get_output_tensor_descriptor() " + "instead", DeprecationWarning, 2) cdef int32_t numModesOut = 0 with nogil: status = cutensornetGetOutputTensorDetails( @@ -480,6 +545,63 @@ cpdef tuple get_output_tensor_details(intptr_t handle, intptr_t tn_desc): check_status(status) return (numModesOut, modes, extents, strides) +cpdef intptr_t get_output_tensor_descriptor( + intptr_t handle, intptr_t tn_desc) except*: + """Get the networks output tensor descriptor. + + Args: + handle (intptr_t): The library handle. + tn_desc (intptr_t): The tensor network descriptor. + + Returns: + intptr_t: An opaque descriptor handle (as Python :class:`int`). + Users are responsible to call :func:`destroy_tensor_descriptor` to + clean it up. + + .. seealso:: `cutensornetGetOutputTensorDescriptor` + """ + cdef _TensorDescriptor desc + with nogil: + status = cutensornetGetOutputTensorDescriptor( + <_Handle>handle, <_NetworkDescriptor>tn_desc, &desc) + check_status(status) + return desc + + +cpdef tuple get_tensor_details(intptr_t handle, intptr_t desc): + """Get the tensor's metadata. + + Args: + handle (intptr_t): The library handle. + desc (intptr_t): A tensor descriptor. + + Returns: + tuple: + The metadata of the tensor: ``(num_modes, modes, extents, + strides)``. + + .. seealso:: `cutensornetGetTensorDetails` + + """ + cdef int32_t numModesOut = 0 + with nogil: + status = cutensornetGetTensorDetails( + <_Handle>handle, <_TensorDescriptor>desc, + &numModesOut, NULL, NULL, NULL, NULL) + check_status(status) + modes = _numpy.empty(numModesOut, dtype=_numpy.int32) + extents = _numpy.empty(numModesOut, dtype=_numpy.int64) + strides = _numpy.empty(numModesOut, dtype=_numpy.int64) + cdef int32_t* mPtr = modes.ctypes.data + cdef int64_t* ePtr = extents.ctypes.data + cdef int64_t* sPtr = strides.ctypes.data + with nogil: + status = cutensornetGetTensorDetails( + <_Handle>handle, <_TensorDescriptor>desc, + &numModesOut, NULL, mPtr, ePtr, sPtr) + check_status(status) + return (numModesOut, modes, extents, strides) + cpdef intptr_t create_workspace_descriptor(intptr_t handle) except*: """Create a workspace descriptor. @@ -523,9 +645,18 @@ cpdef workspace_compute_sizes( tn_desc (intptr_t): The tensor network descriptor. info (intptr_t): The optimizer info handle. workspace (intptr_t): The workspace descriptor. + + .. warning:: + + This function is deprecated and will be removed in a future release. + Use :func:`workspace_compute_contraction_sizes` instead. .. seealso:: `cutensornetWorkspaceComputeSizes` """ + warnings.warn("cuquantum.cutensornet.workspace_compute_sizes() is deprecated and will " + "be removed in the future; please switch to " + "cuquantum.cutensornet.workspace_compute_contraction_sizes() instead", + DeprecationWarning, 2) with nogil: status = cutensornetWorkspaceComputeSizes( <_Handle>handle, <_NetworkDescriptor>tn_desc, @@ -534,6 +665,26 @@ cpdef workspace_compute_sizes( check_status(status) +cpdef workspace_compute_contraction_sizes( + intptr_t handle, intptr_t tn_desc, intptr_t info, intptr_t workspace): + """Compute the required workspace sizes for tensor network contraction. + + Args: + handle (intptr_t): The library handle. + tn_desc (intptr_t): The tensor network descriptor. + info (intptr_t): The optimizer info handle. + workspace (intptr_t): The workspace descriptor. + + .. seealso:: `cutensornetWorkspaceComputeContractionSizes` + """ + with nogil: + status = cutensornetWorkspaceComputeContractionSizes( + <_Handle>handle, <_NetworkDescriptor>tn_desc, + <_ContractionOptimizerInfo>info, + <_WorkspaceDescriptor>workspace) + check_status(status) + + cpdef uint64_t workspace_get_size( intptr_t handle, intptr_t workspace, int pref, int mem_space) except*: """Get the workspace size for the corresponding preference and memory @@ -713,12 +864,34 @@ cpdef destroy_contraction_optimizer_info(intptr_t info): ######################### Python specific utility ######################### +contraction_path_dtype = _numpy.dtype( + {'names':['num_contractions','data'], + 'formats': (_numpy.uint32, _numpy.intp), + 'itemsize': sizeof(_ContractionPath), + }, align=True +) + +# We need this dtype because its members are not of the same type... +slice_info_pair_dtype = _numpy.dtype( + {'names': ('sliced_mode','sliced_extent'), + 'formats': (_numpy.int32, _numpy.int64), + 'itemsize': sizeof(_SliceInfoPair), + }, align=True +) + +slicing_config_dtype = _numpy.dtype( + {'names': ('num_sliced_modes','data'), + 'formats': (_numpy.uint32, _numpy.intp), + 'itemsize': sizeof(_SlicingConfig), + }, align=True +) + cdef dict contract_opti_info_sizes = { CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_NUM_SLICES: _numpy.int64, CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_NUM_SLICED_MODES: _numpy.int32, CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_SLICED_MODE: _numpy.int32, CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_SLICED_EXTENT: _numpy.int64, - CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_PATH: ContractionPath, + CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_PATH: contraction_path_dtype, CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_PHASE1_FLOP_COUNT: _numpy.float64, CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_FLOP_COUNT: _numpy.float64, CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_LARGEST_TENSOR: _numpy.float64, @@ -727,6 +900,7 @@ cdef dict contract_opti_info_sizes = { CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_NUM_INTERMEDIATE_MODES: _numpy.int32, CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_EFFECTIVE_FLOPS_EST: _numpy.float64, CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_RUNTIME_EST: _numpy.float64, + CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_SLICING_CONFIG: slicing_config_dtype, } cpdef contraction_optimizer_info_get_attribute_dtype(int attr): @@ -736,7 +910,8 @@ cpdef contraction_optimizer_info_get_attribute_dtype(int attr): attr (ContractionOptimizerInfoAttribute): The attribute to query. Returns: - The data type of the queried attribute. + 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:`contraction_optimizer_info_get_attribute` @@ -750,23 +925,26 @@ cpdef contraction_optimizer_info_get_attribute_dtype(int attr): val = ContractionOptimizerInfoAttribute.PATH dtype = contraction_optimizer_info_get_attribute_dtype(val) - # setter + # for setting a path path = np.asarray([(1, 3), (1, 2), (0, 1)], dtype=np.int32) - path_obj = dtype(path.size//2, path.ctypes.data) + # ... or for getting a path; note that num_contractions is the number of + # input tensors minus one + path = np.empty(2*num_contractions, dtype=np.int32) + + path_obj = np.zeros((1,), dtype=dtype) + path_obj["num_contractions"] = path.size // 2 + path_obj["node_pair"] = path.ctypes.ptr + + # for setting a path contraction_optimizer_info_set_attribute( - handle, info, val, path_obj.get_data(), path_obj.get_size()) + handle, info, val, path_obj.ctypes.data, path_obj.dtype.itemsize) - # getter - # num_contractions is the number of input tensors minus one - path = np.empty(2*num_contractions, dtype=np.int32) - path_obj = dtype(num_contractions, path.ctypes.data) + # for getting a path contraction_optimizer_info_get_attribute( - handle, info, val, path_obj.get_data(), path_obj.get_size()) + handle, info, val, path_obj.ctypes.data, path_obj.dtype.itemsize) # now path is filled print(path) - See also the documentation of :class:`ContractionPath`. This design is subject - to change in a future release. """ return contract_opti_info_sizes[attr] @@ -789,9 +967,6 @@ cpdef contraction_optimizer_info_get_attribute( .. note:: To compute ``size``, use the itemsize of the corresponding data type, which can be queried using :func:`contraction_optimizer_info_get_attribute_dtype`. - .. note:: For getting the :data:`ContractionOptimizerInfoAttribute.PATH` attribute - please see :func:`contraction_optimizer_info_get_attribute_dtype`. - .. seealso:: `cutensornetContractionOptimizerInfoGetAttribute` """ with nogil: @@ -817,9 +992,6 @@ cpdef contraction_optimizer_info_set_attribute( .. note:: To compute ``size``, use the itemsize of the corresponding data type, which can be queried using :func:`contraction_optimizer_info_get_attribute_dtype`. - .. note:: For setting the :data:`ContractionOptimizerInfoAttribute.PATH` attribute - please see :func:`contraction_optimizer_info_get_attribute_dtype`. - .. seealso:: `cutensornetContractionOptimizerInfoSetAttribute` """ with nogil: @@ -1272,7 +1444,7 @@ cpdef contraction( stream (intptr_t): The CUDA stream handle (``cudaStream_t`` as Python :class:`int`). - .. note:: + .. warning:: This function is deprecated and will be removed in a future release. Use :func:`contract_slices` instead. @@ -1623,70 +1795,582 @@ cpdef logger_force_disable(): check_status(status) -cdef class ContractionPath: - """A proxy object to hold a `cutensornetContractionPath_t` struct. +cpdef intptr_t create_tensor_descriptor( + intptr_t handle, int32_t n_modes, extents, strides, modes, + int data_type) except*: + """Create a tensor descriptor. - Users provide the number of contractions and a pointer address to the actual - contraction path, and this object creates an `cutensornetContractionPath_t` - instance and fills in the provided information. + Args: + handle (intptr_t): The library handle. + n_modes (int32_t): The number of modes of the tensor. + extents: The extents of the tensor (on host). It can be - Example: + - an :class:`int` as the pointer address to the array + - a Python sequence of :class:`int` - .. code-block:: python + strides: The strides of the tensor (on host). It can be - # the pairwise contraction order is stored as C int - path = np.asarray([(1, 3), (1, 2), (0, 1)], dtype=np.int32) - path_obj = ContractionPath(path.size//2, path.ctypes.data) + - an :class:`int` as the pointer address to the array + - a Python sequence of :class:`int` + + modes: The modes of the tensor (on host). 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 tensor. + + Returns: + intptr_t: An opaque descriptor handle (as Python :class:`int`). + + .. note:: + If ``strides`` is set to 0 (``NULL``), it means the tensor is in + the Fortran layout (F-contiguous). + + .. seealso:: `cutensornetCreateTensorDescriptor` + """ + # extents can be a pointer address, or a Python sequence + cdef vector[int64_t] extentsData + cdef int64_t* extentsPtr + if cpython.PySequence_Check(extents): + extentsData = extents + extentsPtr = extentsData.data() + else: # a pointer address + extentsPtr = extents + + # strides can be a pointer address, or a Python sequence + cdef vector[int64_t] stridesData + cdef int64_t* stridesPtr + if cpython.PySequence_Check(strides): + stridesData = strides + stridesPtr = stridesData.data() + else: # a pointer address + stridesPtr = strides + + # modes can be a pointer address, or a Python sequence + cdef vector[int32_t] modesData + cdef int32_t* modesPtr + if cpython.PySequence_Check(modes): + modesData = modes + modesPtr = modesData.data() + else: # a pointer address + modesPtr = modes + + cdef _TensorDescriptor desc + with nogil: + status = cutensornetCreateTensorDescriptor( + <_Handle>handle, n_modes, extentsPtr, stridesPtr, modesPtr, + data_type, &desc) + check_status(status) + return desc + + +cpdef destroy_tensor_descriptor(intptr_t desc): + """Destroy a tensor descriptor. + + Args: + desc (intptr_t): The tensor descriptor. + + .. seealso:: `cutensornetDestroyTensorDescriptor` + """ + with nogil: + status = cutensornetDestroyTensorDescriptor(<_TensorDescriptor>desc) + check_status(status) + + +cpdef intptr_t create_tensor_svd_config( + intptr_t handle) except*: + """Create a tensor SVD config object. + + Args: + handle (intptr_t): The library handle. + + Returns: + intptr_t: An opaque tensor SVD config handle (as Python :class:`int`). + + .. seealso:: `cutensornetCreateTensorSVDConfig` + """ + cdef _TensorSVDConfig config + with nogil: + status = cutensornetCreateTensorSVDConfig( + <_Handle>handle, &config) + check_status(status) + return config + + +cpdef destroy_tensor_svd_config(intptr_t config): + """Destroy a tensor SVD config object. + + Args: + config (intptr_t): The tensor SVD config handle. + + .. seealso:: `cutensornetDestroyTensorSVDConfig` + """ + with nogil: + status = cutensornetDestroyTensorSVDConfig( + <_TensorSVDConfig>config) + check_status(status) + + +######################### Python specific utility ######################### + +cdef dict tensor_svd_cfg_sizes = { + CUTENSORNET_TENSOR_SVD_CONFIG_ABS_CUTOFF: _numpy.float64, + CUTENSORNET_TENSOR_SVD_CONFIG_REL_CUTOFF: _numpy.float64, + CUTENSORNET_TENSOR_SVD_CONFIG_S_NORMALIZATION: _numpy.int32, # = sizeof(enum value) + CUTENSORNET_TENSOR_SVD_CONFIG_S_PARTITION: _numpy.int32, # = sizeof(enum value) +} + +cpdef tensor_svd_config_get_attribute_dtype(int attr): + """Get the Python data type of the corresponding tensor SVD config attribute. + + Args: + attr (TensorSVDConfigAttribute): The attribute to query. + + Returns: + The data type of the queried attribute. + + .. note:: This API has no C counterpart and is a convenient helper for + allocating memory for :func:`tensor_svd_config_get_attribute` + and :func:`tensor_svd_config_set_attribute`. + """ + dtype = tensor_svd_cfg_sizes[attr] + if attr == CUTENSORNET_TENSOR_SVD_CONFIG_S_NORMALIZATION: + if _numpy.dtype(dtype).itemsize != sizeof(_TensorSVDNormalization): + warnings.warn("binary size may be incompatible") + elif attr == CUTENSORNET_TENSOR_SVD_CONFIG_S_PARTITION: + if _numpy.dtype(dtype).itemsize != sizeof(_TensorSVDPartition): + warnings.warn("binary size may be incompatible") + return dtype + +########################################################################### + + +cpdef tensor_svd_config_get_attribute( + intptr_t handle, intptr_t config, int attr, + intptr_t buf, size_t size): + """Get the tensor SVD config attribute. + + Args: + handle (intptr_t): The library handle. + config (intptr_t): The tensor SVD config handle. + attr (TensorSVDConfigAttribute): The attribute to set. + 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:`tensor_svd_config_get_attribute_dtype`. + + .. seealso:: `cutensornetTensorSVDConfigGetAttribute` + """ + with nogil: + status = cutensornetTensorSVDConfigGetAttribute( + <_Handle>handle, <_TensorSVDConfig>config, + <_TensorSVDConfigAttribute>attr, + buf, size) + check_status(status) - # get the pointer address to the underlying `cutensornetContractionPath_t` - my_func(..., path_obj.get_data(), ...) - # path must outlive path_obj! - del path_obj - del path +cpdef tensor_svd_config_set_attribute( + intptr_t handle, intptr_t config, int attr, + intptr_t buf, size_t size): + """Set the tensor SVD config attribute. + + Args: + handle (intptr_t): The library handle. + config (intptr_t): The tensor SVD config handle. + attr (TensorSVDConfigAttribute): The attribute to set. + buf (intptr_t): The pointer address (as Python :class:`int`) to the attribute data. + 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:`tensor_svd_config_get_attribute_dtype`. + + .. seealso:: `cutensornetTensorSVDConfigSetAttribute` + """ + with nogil: + status = cutensornetTensorSVDConfigSetAttribute( + <_Handle>handle, <_TensorSVDConfig>config, + <_TensorSVDConfigAttribute>attr, + buf, size) + check_status(status) + + +cpdef intptr_t create_tensor_svd_info(intptr_t handle) except*: + """Create a tensor SVD info object. Args: - num_contractions (int): The number of contractions in the provided path. - data (uintptr_t): The pointer address (as Python :class:`int`) to the provided path. + handle (intptr_t): The library handle. + + Returns: + intptr_t: An opaque tensor SVD info handle (as Python :class:`int`). + + .. seealso:: `cutensornetCreateTensorSVDInfo` + """ + cdef _TensorSVDInfo info + with nogil: + status = cutensornetCreateTensorSVDInfo( + <_Handle>handle, &info) + check_status(status) + return info + + +cpdef destroy_tensor_svd_info(intptr_t info): + """Destroy a tensor SVD info object. + + Args: + info (intptr_t): The tensor SVD info handle. + + .. seealso:: `cutensornetDestroyTensorSVDInfo` + """ + with nogil: + status = cutensornetDestroyTensorSVDInfo( + <_TensorSVDInfo>info) + check_status(status) + + +######################### Python specific utility ######################### + +cdef dict tensor_svd_info_sizes = { + CUTENSORNET_TENSOR_SVD_INFO_FULL_EXTENT: _numpy.int64, + CUTENSORNET_TENSOR_SVD_INFO_REDUCED_EXTENT: _numpy.int64, + CUTENSORNET_TENSOR_SVD_INFO_DISCARDED_WEIGHT: _numpy.float64, +} + +cpdef tensor_svd_info_get_attribute_dtype(int attr): + """Get the Python data type of the corresponding tensor SVD info attribute. + + Args: + attr (TensorSVDInfoAttribute): 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:`tensor_svd_info_get_attribute`. + + """ + return tensor_svd_info_sizes[attr] + +########################################################################### + + +cpdef tensor_svd_info_get_attribute( + intptr_t handle, intptr_t info, int attr, + intptr_t buf, size_t size): + """Get the tensor SVD info attribute. + + Args: + handle (intptr_t): The library handle. + info (intptr_t): The tensor SVD info handle. + attr (TensorSVDInfoAttribute): The attribute to query. + 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:`tensor_svd_info_get_attribute_dtype`. + + .. seealso:: `cutensornetTensorSVDInfoGetAttribute` + """ + with nogil: + status = cutensornetTensorSVDInfoGetAttribute( + <_Handle>handle, <_TensorSVDInfo>info, + <_TensorSVDInfoAttribute>attr, + buf, size) + check_status(status) + + +cpdef workspace_compute_svd_sizes( + intptr_t handle, intptr_t tensor_in, intptr_t tensor_u, + intptr_t tensor_v, intptr_t config, intptr_t workspace): + """Compute the required workspace sizes for :func:`tensor_svd`. + + Args: + handle (intptr_t): The library handle. + tensor_in (intptr_t): The input tensor descriptor. + tensor_u (intptr_t): The tensor descriptor for the output U. + tensor_v (intptr_t): The tensor descriptor for the output V. + config (intptr_t): The tensor SVD config handle. + workspace (intptr_t): The workspace descriptor. + + .. seealso:: `cutensornetWorkspaceComputeSVDSizes` + """ + with nogil: + status = cutensornetWorkspaceComputeSVDSizes( + <_Handle>handle, <_TensorDescriptor>tensor_in, + <_TensorDescriptor>tensor_u, <_TensorDescriptor>tensor_v, + <_TensorSVDConfig>config, + <_WorkspaceDescriptor>workspace) + check_status(status) + + +cpdef tensor_svd( + intptr_t handle, intptr_t tensor_in, intptr_t raw_data_in, + intptr_t tensor_u, intptr_t u, + intptr_t s, + intptr_t tensor_v, intptr_t v, + intptr_t config, intptr_t info, + intptr_t workspace, intptr_t stream): + """Perform SVD decomposition of a tensor. + + Args: + handle (intptr_t): The library handle. + tensor_in (intptr_t): The input tensor descriptor. + raw_data_in (intptr_t): The pointer address (as Python :class:`int`) to the + input tensor (on device). + tensor_u (intptr_t): The tensor descriptor for the output U. + u (intptr_t): The pointer address (as Python :class:`int`) to the output + tensor U (on device). + s (intptr_t): The pointer address (as Python :class:`int`) to the output + array S (on device). + tensor_v (intptr_t): The tensor descriptor for the output V. + v (intptr_t): The pointer address (as Python :class:`int`) to the output + tensor V (on device). + config (intptr_t): The tensor SVD config handle. + info (intptr_t): The tensor SVD info handle. + workspace (intptr_t): The workspace descriptor. + stream (intptr_t): The CUDA stream handle (``cudaStream_t`` as Python + :class:`int`). .. note:: - Users are responsible for managing the lifetime of the underlying path data - (i.e. the validity of the ``data`` pointer). - .. warning:: - The design of how `cutensornetContractionPath_t` is handled in Python is - experimental and subject to change in a future release. + After this function call, the output tensor descriptors ``tensor_u`` and + ``tensor_v`` may have their shapes and strides changed. See the documentation + for further information. + + .. seealso:: `cutensornetTensorSVD` + """ + with nogil: + status = cutensornetTensorSVD( + <_Handle>handle, <_TensorDescriptor>tensor_in, raw_data_in, + <_TensorDescriptor>tensor_u, u, + s, + <_TensorDescriptor>tensor_v, v, + <_TensorSVDConfig>config, <_TensorSVDInfo>info, + <_WorkspaceDescriptor>workspace, stream) + check_status(status) + + +cpdef workspace_compute_qr_sizes( + intptr_t handle, intptr_t tensor_in, intptr_t tensor_q, + intptr_t tensor_r, intptr_t workspace): + """Compute the required workspace sizes for :func:`tensor_qr`. + + Args: + handle (intptr_t): The library handle. + tensor_in (intptr_t): The input tensor descriptor. + tensor_q (intptr_t): The tensor descriptor for the output Q. + tensor_r (intptr_t): The tensor descriptor for the output R. + workspace (intptr_t): The workspace descriptor. + + .. seealso:: `cutensornetWorkspaceComputeQRSizes` """ - cdef _ContractionPath* path + with nogil: + status = cutensornetWorkspaceComputeQRSizes( + <_Handle>handle, <_TensorDescriptor>tensor_in, + <_TensorDescriptor>tensor_q, <_TensorDescriptor>tensor_r, + <_WorkspaceDescriptor>workspace) + check_status(status) - def __cinit__(self, int num_contractions, uintptr_t data): - self.path = <_ContractionPath*>PyMem_Malloc(sizeof(_ContractionPath)) - def __dealloc__(self): - PyMem_Free(self.path) +cpdef tensor_qr( + intptr_t handle, intptr_t tensor_in, intptr_t raw_data_in, + intptr_t tensor_q, intptr_t q, + intptr_t tensor_r, intptr_t r, + intptr_t workspace, intptr_t stream): + """Perform QR decomposition of a tensor. - def __init__(self, int num_contractions, uintptr_t data): - """ - __init__(self, int num_contractions, uintptr_t data) - """ - self.path.numContractions = num_contractions - self.path.data = <_NodePair*>data + Args: + handle (intptr_t): The library handle. + tensor_in (intptr_t): The input tensor descriptor. + raw_data_in (intptr_t): The pointer address (as Python :class:`int`) to the + input tensor (on device). + tensor_q (intptr_t): The tensor descriptor for the output Q. + q (intptr_t): The pointer address (as Python :class:`int`) to the output + tensor Q (on device). + tensor_r (intptr_t): The tensor descriptor for the output R. + r (intptr_t): The pointer address (as Python :class:`int`) to the output + tensor R (on device). + workspace (intptr_t): The workspace descriptor. + stream (intptr_t): The CUDA stream handle (``cudaStream_t`` as Python + :class:`int`). - def get_path(self): - """Get the pointer address to the underlying `cutensornetContractionPath_t` struct. + .. seealso:: `cutensornetTensorQR` + """ + with nogil: + status = cutensornetTensorQR( + <_Handle>handle, <_TensorDescriptor>tensor_in, raw_data_in, + <_TensorDescriptor>tensor_q, q, + <_TensorDescriptor>tensor_r, r, + <_WorkspaceDescriptor>workspace, stream) + check_status(status) - Returns: - uintptr_t: The pointer address. - """ - return self.path - def get_size(self): - """Get the size of the `cutensornetContractionPath_t` struct. +cpdef workspace_compute_gate_split_sizes( + intptr_t handle, intptr_t tensor_a, intptr_t tensor_b, + intptr_t tensor_g, intptr_t tensor_u, intptr_t tensor_v, + int algo, intptr_t svd_config, int compute_type, + intptr_t workspace): + """Compute the required workspace sizes for :func:`gate_split`. - Returns: - size_t: ``sizeof(cutensornetContractionPath_t)``. - """ - return sizeof(_ContractionPath) + Args: + handle (intptr_t): The library handle. + tensor_a (intptr_t): The tensor descriptor for the input A. + tensor_b (intptr_t): The tensor descriptor for the input B. + tensor_g (intptr_t): The tensor descriptor for the input G (the gate). + tensor_u (intptr_t): The tensor descriptor for the output U. + tensor_v (intptr_t): The tensor descriptor for the output V. + algo (cuquantum.cutensornet.GateSplitAlgo): The gate splitting algorithm. + svd_config (intptr_t): The tensor SVD config handle. + compute_type (cuquantum.ComputeType): The compute type of the + computation. + workspace (intptr_t): The workspace descriptor. + + .. seealso:: `cutensornetWorkspaceComputeGateSplitSizes` + """ + with nogil: + status = cutensornetWorkspaceComputeGateSplitSizes( + <_Handle>handle, <_TensorDescriptor>tensor_a, + <_TensorDescriptor>tensor_b, <_TensorDescriptor>tensor_g, + <_TensorDescriptor>tensor_u, <_TensorDescriptor>tensor_v, + <_GateSplitAlgo>algo, <_TensorSVDConfig>svd_config, + <_ComputeType>compute_type, <_WorkspaceDescriptor>workspace) + check_status(status) + + +cpdef gate_split( + intptr_t handle, intptr_t tensor_a, intptr_t raw_data_a, + intptr_t tensor_b, intptr_t raw_data_b, + intptr_t tensor_g, intptr_t raw_data_g, + intptr_t tensor_u, intptr_t u, + intptr_t s, + intptr_t tensor_v, intptr_t v, + int algo, intptr_t svd_config, int compute_type, + intptr_t svd_info, intptr_t workspace, intptr_t stream): + """Perform gate split operation. + + Args: + handle (intptr_t): The library handle. + tensor_a (intptr_t): The tensor descriptor for the input A. + raw_data_a (intptr_t): The pointer address (as Python :class:`int`) to the + input tensor A (on device). + tensor_b (intptr_t): The tensor descriptor for the input B. + raw_data_b (intptr_t): The pointer address (as Python :class:`int`) to the + input tensor B (on device). + tensor_g (intptr_t): The tensor descriptor for the input G (the gate). + raw_data_g (intptr_t): The pointer address (as Python :class:`int`) to the + gate tensor G (on device). + tensor_u (intptr_t): The tensor descriptor for the output U. + u (intptr_t): The pointer address (as Python :class:`int`) to the output + tensor U (on device). + s (intptr_t): The pointer address (as Python :class:`int`) to the output + array S (on device). + tensor_v (intptr_t): The tensor descriptor for the output V. + v (intptr_t): The pointer address (as Python :class:`int`) to the output + tensor V (on device). + algo (cuquantum.cutensornet.GateSplitAlgo): The gate splitting algorithm. + svd_config (intptr_t): The tensor SVD config handle. + compute_type (cuquantum.ComputeType): The compute type of the + computation. + svd_info (intptr_t): The tensor SVD info handle. + workspace (intptr_t): The workspace descriptor. + stream (intptr_t): The CUDA stream handle (``cudaStream_t`` as Python + :class:`int`). + + .. note:: + + After this function call, the output tensor descriptors ``tensor_u`` and + ``tensor_v`` may have their shapes and strides changed. See the documentation + for further information. + + .. seealso:: `cutensornetGateSplit` + """ + with nogil: + status = cutensornetGateSplit( + <_Handle>handle, + <_TensorDescriptor>tensor_a, raw_data_a, + <_TensorDescriptor>tensor_b, raw_data_b, + <_TensorDescriptor>tensor_g, raw_data_g, + <_TensorDescriptor>tensor_u, u, + s, + <_TensorDescriptor>tensor_v, v, + <_GateSplitAlgo>algo, <_TensorSVDConfig>svd_config, + <_ComputeType>compute_type, <_TensorSVDInfo>svd_info, + <_WorkspaceDescriptor>workspace, stream) + check_status(status) + + +cpdef distributed_reset_configuration( + intptr_t handle, intptr_t comm_ptr, size_t comm_size): + """Reset the distributed communicator. + + Args: + handle (intptr_t): The library handle. + comm_ptr (intptr_t): The pointer to the provided communicator. + comm_size (size_t): The size of the provided communicator + (``sizeof(comm)``). + + .. note:: For using MPI communicators from mpi4py, the helper function + :func:`~cuquantum.cutensornet.get_mpi_comm_pointer` can be used: + + .. code-block:: python + + cutn.distributed_reset_configuration(handle, *get_mpi_comm_pointer(comm)) + + .. seealso:: `cutensornetDistributedResetConfiguration` + """ + with nogil: + status = cutensornetDistributedResetConfiguration( + <_Handle>handle, comm_ptr, comm_size) + check_status(status) + + +cpdef int distributed_get_num_ranks(intptr_t handle) except -1: + """Get the number of distributed ranks. + + Args: + handle (intptr_t): The library handle. + + .. seealso:: `cutensornetDistributedGetNumRanks` + """ + cdef int rank + with nogil: + status = cutensornetDistributedGetNumRanks( + <_Handle>handle, &rank) + check_status(status) + return rank + + +cpdef int distributed_get_proc_rank(intptr_t handle) except -1: + """Get the current process rank. + + Args: + handle (intptr_t): The library handle. + + .. seealso:: `cutensornetDistributedGetProcRank` + """ + cdef int rank + with nogil: + status = cutensornetDistributedGetProcRank( + <_Handle>handle, &rank) + check_status(status) + return rank + + +cpdef distributed_synchronize(intptr_t handle): + """Synchronize the distributed communicator. + + Args: + handle (intptr_t): The library handle. + + .. seealso:: `cutensornetDistributedSynchronize` + """ + with nogil: + status = cutensornetDistributedSynchronize(<_Handle>handle) + check_status(status) class GraphAlgo(IntEnum): @@ -1741,6 +2425,7 @@ class ContractionOptimizerInfoAttribute(IntEnum): NUM_INTERMEDIATE_MODES = CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_NUM_INTERMEDIATE_MODES EFFECTIVE_FLOPS_EST = CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_EFFECTIVE_FLOPS_EST RUNTIME_EST = CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_RUNTIME_EST + SLICING_CONFIG = CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_SLICING_CONFIG class ContractionAutotunePreferenceAttribute(IntEnum): """See `cutensornetContractionAutotunePreferenceAttributes_t`.""" @@ -1756,6 +2441,39 @@ class WorksizePref(IntEnum): class Memspace(IntEnum): """See `cutensornetMemspace_t`.""" DEVICE = CUTENSORNET_MEMSPACE_DEVICE + HOST = CUTENSORNET_MEMSPACE_HOST + +class TensorSVDConfigAttribute(IntEnum): + """See `cutensornetTensorSVDConfigAttributes_t`.""" + ABS_CUTOFF = CUTENSORNET_TENSOR_SVD_CONFIG_ABS_CUTOFF + REL_CUTOFF = CUTENSORNET_TENSOR_SVD_CONFIG_REL_CUTOFF + S_NORMALIZATION = CUTENSORNET_TENSOR_SVD_CONFIG_S_NORMALIZATION + S_PARTITION = CUTENSORNET_TENSOR_SVD_CONFIG_S_PARTITION + +class TensorSVDNormalization(IntEnum): + """See `cutensornetTensorSVDNormalization_t`.""" + NONE = CUTENSORNET_TENSOR_SVD_NORMALIZATION_NONE + L1 = CUTENSORNET_TENSOR_SVD_NORMALIZATION_L1 + L2 = CUTENSORNET_TENSOR_SVD_NORMALIZATION_L2 + LINF = CUTENSORNET_TENSOR_SVD_NORMALIZATION_LINF + +class TensorSVDPartition(IntEnum): + """See `cutensornetTensorSVDPartition_t`.""" + NONE = CUTENSORNET_TENSOR_SVD_PARTITION_NONE + US = CUTENSORNET_TENSOR_SVD_PARTITION_US + SV = CUTENSORNET_TENSOR_SVD_PARTITION_SV + UV_EQUAL = CUTENSORNET_TENSOR_SVD_PARTITION_UV_EQUAL + +class TensorSVDInfoAttribute(IntEnum): + """See `cutensornetTensorSVDInfoAttributes_t`.""" + FULL_EXTENT = CUTENSORNET_TENSOR_SVD_INFO_FULL_EXTENT + REDUCED_EXTENT = CUTENSORNET_TENSOR_SVD_INFO_REDUCED_EXTENT + DISCARDED_WEIGHT = CUTENSORNET_TENSOR_SVD_INFO_DISCARDED_WEIGHT + +class GateSplitAlgo(IntEnum): + """See `cutensornetGateSplitAlgo_t`.""" + DIRECT = CUTENSORNET_GATE_SPLIT_ALGO_DIRECT + REDUCED = CUTENSORNET_GATE_SPLIT_ALGO_REDUCED del IntEnum @@ -1766,6 +2484,13 @@ MINOR_VER = CUTENSORNET_MINOR PATCH_VER = CUTENSORNET_PATCH VERSION = CUTENSORNET_VERSION +# numpy dtypes +tensor_qualifiers_dtype = _numpy.dtype( + {'names':('is_conjugate', ), + 'formats': (_numpy.int32, ), + 'itemsize': sizeof(_TensorQualifiers), + }, align=True +) # who owns a reference to user-provided Python objects (k: owner, v: object) cdef dict owner_pyobj = {} diff --git a/python/cuquantum/cutensornet/memory.py b/python/cuquantum/cutensornet/memory.py index 97fd19d..0c9925d 100644 --- a/python/cuquantum/cutensornet/memory.py +++ b/python/cuquantum/cutensornet/memory.py @@ -13,6 +13,9 @@ import cupy as cp +from ._internal import utils + + class MemoryPointer: """ An RAII class for a device memory buffer. @@ -84,20 +87,20 @@ def __init__(self, device_id, logger): """ __init__(device_id) """ - self.device = cp.cuda.Device(device_id) + self.device_id = device_id self.logger = logger def memalloc(self, size): - with self.device: + with utils.device_ctx(self.device_id): device_ptr = cp.cuda.runtime.malloc(size) self.logger.debug(f"_RawCUDAMemoryManager (allocate memory): size = {size}, ptr = {device_ptr}, " - f"device = {self.device}, stream={cp.cuda.get_current_stream()}") + f"device = {self.device_id}, stream={cp.cuda.get_current_stream()}") def create_finalizer(): def finalizer(): - with self.device: - cp.cuda.runtime.free(device_ptr) + # Note: With UVA there is no need to switch context to the device the memory belongs to before calling free(). + cp.cuda.runtime.free(device_ptr) self.logger.debug(f"_RawCUDAMemoryManager (release memory): ptr = {device_ptr}") return finalizer @@ -117,16 +120,16 @@ def __init__(self, device_id, logger): """ __init__(device_id) """ - self.device = cp.cuda.Device(device_id) + self.device_id = device_id self.logger = logger def memalloc(self, size): - with self.device: + with utils.device_ctx(self.device_id): cp_mem_ptr = cp.cuda.alloc(size) device_ptr = cp_mem_ptr.ptr self.logger.debug(f"_CupyCUDAMemoryManager (allocate memory): size = {size}, ptr = {device_ptr}, " - f"device = {self.device}, stream={cp.cuda.get_current_stream()}") + f"device = {self.device_id}, stream={cp.cuda.get_current_stream()}") return cp_mem_ptr @@ -165,4 +168,3 @@ def finalizer(): _MEMORY_MANAGER = {'_raw' : _RawCUDAMemoryManager, 'cupy' : _CupyCUDAMemoryManager, 'torch' : _TorchCUDAMemoryManager} - diff --git a/python/cuquantum/cutensornet/tensor_network.py b/python/cuquantum/cutensornet/tensor_network.py index df5c6c7..81cb583 100644 --- a/python/cuquantum/cutensornet/tensor_network.py +++ b/python/cuquantum/cutensornet/tensor_network.py @@ -10,10 +10,7 @@ import collections import dataclasses -import functools import logging -import os -import sys import cupy as cp import numpy as np @@ -71,12 +68,14 @@ class Network: >>> logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)-8s %(message)s', datefmt='%m-%d %H:%M:%S') Args: - subscripts : The mode labels (subscripts) defining the Einstein summation expression as a comma-separated sequence of + subscripts: The mode labels (subscripts) defining the Einstein summation expression as a comma-separated sequence of characters. Unicode characters are allowed in the expression thereby expanding the size of the tensor network that can be specified using the Einstein summation convention. - operands : A sequence of tensors (ndarray-like objects). The currently supported types are :class:`numpy.ndarray`, + operands: A sequence of tensors (ndarray-like objects). The currently supported types are :class:`numpy.ndarray`, :class:`cupy.ndarray`, and :class:`torch.Tensor`. - options : Specify options for the tensor network as a :class:`~cuquantum.NetworkOptions` object. Alternatively, a `dict` + qualifiers: Specify the tensor qualifiers as a :class:`numpy.ndarray` of :class:`~cuquantum.tensor_qualifiers_dtype` objects + of length equal to the number of operands. + 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. @@ -167,7 +166,7 @@ class Network: as specifying options for the tensor network and the optimizer. """ - def __init__(self, *operands, options=None): + def __init__(self, *operands, qualifiers=None, options=None): """ __init__(subscripts, *operands, options=None) """ @@ -192,6 +191,13 @@ def __init__(self, *operands, options=None): self.device_id = options.device_id self.operands = tensor_wrapper.to(self.operands, self.device_id) + # Set blocking or non-blocking behavior. + self.blocking = self.options.blocking is True or self.network_location == 'cpu' + if self.blocking: + self.call_prologue = "This call is blocking and will return only after the operation is complete." + 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) @@ -222,12 +228,13 @@ def __init__(self, *operands, options=None): extents_in = tuple(o.shape for o in self.operands) strides_in = tuple(o.strides for o in self.operands) - self.operands_data, alignments_in = utils.get_operands_data(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) - self.contraction, modes_out, extents_out, strides_out, alignment_out = utils.create_output_tensor( - self.output_class, self.package, self.output, self.size_dict, self.device, self.data_type) + self.contraction, modes_out, extents_out, strides_out = utils.create_output_tensor( + self.output_class, self.package, self.output, self.size_dict, self.device_id, self.data_type) # Create/set handle. if options.handle is not None: @@ -235,19 +242,19 @@ def __init__(self, *operands, options=None): self.handle = options.handle else: self.own_handle = True - with self.device: + with utils.device_ctx(self.device_id): self.handle = cutn.create() # Network definition. self.network = cutn.create_network_descriptor(self.handle, num_inputs, - num_modes_in, extents_in, strides_in, modes_in, alignments_in, # inputs - num_modes_out, extents_out, strides_out, modes_out, alignment_out, # output + num_modes_in, extents_in, strides_in, modes_in, self.qualifiers_in, # inputs + num_modes_out, extents_out, strides_out, modes_out, # output typemaps.NAME_TO_DATA_TYPE[self.data_type], self.compute_type) # Keep output extents for creating new tensors, if needed. self.extents_out = extents_out - # Path optimization atributes. + # Path optimization attributes. self.optimizer_config_ptr, self.optimizer_info_ptr = None, None self.optimized = False @@ -257,11 +264,16 @@ def __init__(self, *operands, options=None): # Contraction plan attributes. self.plan = None + self.planned = False # Autotuning attributes. self.autotune_pref_ptr = None self.autotuned = False + # Attributes to establish stream ordering. + self.workspace_stream = None + self.last_compute_event = None + self.valid_state = True self.logger.info("The network has been created.") @@ -285,6 +297,13 @@ def _check_optimized(self, *args, **kwargs): if not self.optimized: raise RuntimeError(f"{what} cannot be performed before contract_path() has been called.") + def _check_planned(self, *args, **kwargs): + """ + """ + what = kwargs['what'] + if not self.planned: + raise RuntimeError(f"Internal Error: {what} cannot be performed before planning has been done.") + def _free_plan_resources(self, exception=None): """ Free resources allocated in network contraction planning. @@ -327,21 +346,22 @@ 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_ctx): + def _allocate_workspace_memory_perhaps(self, stream, stream_ctx): if self.workspace_ptr is not None: return assert self.workspace_size is not None, "Internal Error." self.logger.debug("Allocating memory for contracting the tensor network...") - with self.device, stream_ctx: + with utils.device_ctx(self.device_id), stream_ctx: try: self.workspace_ptr = self.allocator.memalloc(self.workspace_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.logger.debug(f"Finished allocating memory of size {formatters.MemoryStr(self.workspace_size)} for contraction.") + 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(self.handle, self.workspace_desc, cutn.Memspace.DEVICE, device_ptr, self.workspace_size) @@ -357,7 +377,7 @@ def _calculate_workspace_size(self): # Release workspace already allocated, if any, because the new requirements are likely different. self.workspace_ptr = None - cutn.workspace_compute_sizes(self.handle, self.network, self.optimizer_info_ptr, self.workspace_desc) + cutn.workspace_compute_contraction_sizes(self.handle, self.network, self.optimizer_info_ptr, self.workspace_desc) min_size = cutn.workspace_get_size(self.handle, self.workspace_desc, cutn.WorksizePref.MIN, cutn.Memspace.DEVICE) max_size = cutn.workspace_get_size(self.handle, self.workspace_desc, cutn.WorksizePref.MAX, cutn.Memspace.DEVICE) @@ -376,7 +396,6 @@ def _calculate_workspace_size(self): # Set workspace size to enable contraction planning. The device pointer will be set later during allocation. cutn.workspace_set(self.handle, self.workspace_desc, cutn.Memspace.DEVICE, 0, self.workspace_size) - @utils.precondition(_check_valid_network) @utils.precondition(_check_optimized, "Planning") @utils.atomic(_free_plan_resources, method=True) @@ -456,10 +475,16 @@ def _set_optimizer_options(self, optimize): enum = ConfEnum.SEED self._set_opt_config_option('seed', enum, optimize.seed) + enum = ConfEnum.COST_FUNCTION_OBJECTIVE + self._set_opt_config_option('cost_function', enum, optimize.cost_function) + @utils.precondition(_check_valid_network) @utils.atomic(_free_path_resources, method=True) - def contract_path(self, optimize=None): - """Compute the best contraction path together with any slicing that is needed to ensure that the contraction can be + def contract_path(self, optimize=None, **kwargs): + """ + contract_path(optimize=None) + + Compute the best contraction path together with any slicing that is needed to ensure that the contraction can be performed within the specified memory limit. Args: @@ -481,6 +506,10 @@ def contract_path(self, optimize=None): optimize = utils.check_or_create_options(configuration.OptimizerOptions, optimize, "path optimizer options") + internal_options = dict() + internal_options['create_plan'] = utils.Value(True, validator=lambda v: isinstance(v, bool)) + utils.check_and_set_options(internal_options, kwargs) + if self.optimizer_config_ptr is None: self.optimizer_config_ptr = cutn.create_contraction_optimizer_config(self.handle) if self.optimizer_info_ptr is None: @@ -488,6 +517,10 @@ def contract_path(self, optimize=None): opt_info_ifc = optimizer_ifc.OptimizerInfoInterface(self) + # Special case worth optimizing, as it's an extremely common use case with a trivial path + if len(self.operands) == 2: + optimize.path = [(0, 1)] + # Compute path (or set provided path). if isinstance(optimize.path, configuration.PathFinderOptions): # Set optimizer options. @@ -525,11 +558,15 @@ def contract_path(self, optimize=None): self.optimized = True - # Calculate workspace size required. - self._calculate_workspace_size() + if internal_options['create_plan']: + # Calculate workspace size required. + self._calculate_workspace_size() - # Create plan. - self._create_plan() + # Create plan. + self._create_plan() + self.planned = True + else: + self.planned = False return opt_info.path, opt_info @@ -566,6 +603,7 @@ def _set_autotune_option(self, name, enum, value): @utils.precondition(_check_valid_network) @utils.precondition(_check_optimized, "Autotuning") + @utils.precondition(_check_planned, "Autotuning") def autotune(self, *, iterations=3, stream=None): """Autotune the network to reduce the contraction cost. @@ -588,24 +626,25 @@ 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, stream, self.package) - self._allocate_workspace_memory_perhaps(stream_ctx) + stream, stream_ctx, stream_ptr = utils.get_or_create_stream(self.device_id, stream, self.package) + self._allocate_workspace_memory_perhaps(stream, stream_ctx) # Check if we still hold an output tensor; if not, create a new one. if self.contraction is None: self.contraction = utils.create_empty_tensor(self.output_class, self.extents_out, self.data_type, self.device_id, stream_ctx) + timing = bool(self.logger and self.logger.handlers) self.logger.info(f"Starting autotuning...") - with self.device: - start = stream.record() + 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) - end = stream.record() - end.synchronize() - elapsed = cp.cuda.get_elapsed_time(start, end) + + if elapsed.data is not None: + self.logger.info(f"The autotuning took {elapsed.data:.3f} ms to complete.") self.autotuned = True - self.logger.info(f"The autotuning took {elapsed:.3f} ms to complete.") + @utils.precondition(_check_valid_network) def reset_operands(self, *operands): @@ -618,7 +657,7 @@ def reset_operands(self, *operands): - The shapes, strides, datatypes match those of the old ones. - The packages that the operands belong to match those of the old ones. - - If input tensors are on GPU, the library package, device, and alignments must match. + - If input tensors are on GPU, the library package and device must match. Args: operands: See :class:`Network`'s documentation. @@ -650,16 +689,13 @@ def reset_operands(self, *operands): raise ValueError(f"The new operands must be on the same device ({device_id}) as the original operands " f"({self.device_id}).") - _, orig_alignments = utils.get_operands_data(self.operands) - new_operands_data, new_alignments = utils.get_operands_data(operands) - utils.check_alignments_match(orig_alignments, new_alignments) - # Finally, replace the original data pointers by the new ones. - self.operands_data = new_operands_data + self.operands_data = utils.get_operands_data(operands) self.logger.info("The operands have been reset.") @utils.precondition(_check_valid_network) @utils.precondition(_check_optimized, "Contraction") + @utils.precondition(_check_planned, "Contraction") def contract(self, *, slices=None, stream=None): """Contract the network and return the result. @@ -675,8 +711,8 @@ 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, stream, self.package) - self._allocate_workspace_memory_perhaps(stream_ctx) + stream, stream_ctx, stream_ptr = utils.get_or_create_stream(self.device_id, stream, self.package) + self._allocate_workspace_memory_perhaps(stream, stream_ctx) # Check if we still hold an output tensor; if not, create a new one. if self.contraction is None: @@ -697,16 +733,15 @@ def contract(self, *, slices=None, stream=None): message = f"The provided 'slices' must be a range object or a sequence object. The object type is {type(slices)}." raise TypeError(message) + timing = bool(self.logger and self.logger.handlers) self.logger.info("Starting network contraction...") - with self.device: - start = stream.record() + 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) - end = stream.record() - end.synchronize() - elapsed = cp.cuda.get_elapsed_time(start, end) - self.logger.info(f"The contraction took {elapsed:.3f} ms to complete.") + if elapsed.data is not None: + self.logger.info(f"The contraction took {elapsed.data:.3f} ms to complete.") # Destroy slice group, if created. if slice_group != 0: @@ -718,6 +753,7 @@ def contract(self, *, slices=None, stream=None): else: out = self.contraction.tensor self.contraction = None # We cannot overwrite what we've already handed to users. + return out def free(self): @@ -731,6 +767,10 @@ def free(self): return try: + # 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) + self._free_path_resources() if self.autotune_pref_ptr is not None: @@ -759,7 +799,7 @@ def free(self): self.logger.info("The network resources have been released.") -def contract(*operands, options=None, optimize=None, stream=None, return_info=False): +def contract(*operands, qualifiers=None, options=None, optimize=None, stream=None, return_info=False): r""" contract(subscripts, *operands, options=None, optimize=None, stream=None, return_info=False) @@ -775,6 +815,8 @@ def contract(*operands, options=None, optimize=None, stream=None, return_info=Fa can be specified using the Einstein summation convention. operands : A sequence of tensors (ndarray-like objects). The currently supported types are :class:`numpy.ndarray`, :class:`cupy.ndarray`, and :class:`torch.Tensor`. + qualifiers: Specify the tensor qualifiers as a :class:`numpy.ndarray` of :class:`~cuquantum.tensor_qualifiers_dtype` objects + of length equal to the number of operands. 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. @@ -796,14 +838,15 @@ def contract(*operands, options=None, optimize=None, stream=None, return_info=Fa .. code-block:: python - from cuquantum import cutensornet, NetworkOptions, contract + from cuquantum import cutensornet as cutn + from cuquantum import contract, NetworkOptions - handle = cutensornet.create() + handle = cutn.create() network_opts = NetworkOptions(handle=handle, ...) out = contract(..., options=network_opts, ...) # ... the same handle can be reused for further calls ... # when it's done, remember to destroy the handle - cutensornet.destroy(handle) + cutn.destroy(handle) Examples: @@ -896,12 +939,8 @@ def contract(*operands, options=None, optimize=None, stream=None, return_info=Fa >>> r = contract('ij,jk', a, b) """ - options = utils.check_or_create_options(configuration.NetworkOptions, options, "network options") - - optimize = utils.check_or_create_options(configuration.OptimizerOptions, optimize, "path optimizer options") - # Create network. - with Network(*operands, options=options) as network: + with Network(*operands, qualifiers=qualifiers, options=options) as network: # Compute path. opt_info = network.contract_path(optimize=optimize) @@ -917,7 +956,7 @@ def contract(*operands, options=None, optimize=None, stream=None, return_info=Fa return output -def contract_path(*operands, options=None, optimize=None): +def contract_path(*operands, qualifiers=None, options=None, optimize=None): """ contract_path(subscripts, *operands, options=None, optimize=None) @@ -933,6 +972,8 @@ def contract_path(*operands, options=None, optimize=None): can be specified using the Einstein summation convention. operands : A sequence of tensors (ndarray-like objects). The currently supported types are :class:`numpy.ndarray`, :class:`cupy.ndarray`, and :class:`torch.Tensor`. + qualifiers: Specify the tensor qualifiers as a :class:`numpy.ndarray` of :class:`~cuquantum.tensor_qualifiers_dtype` objects + of length equal to the number of operands. 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. @@ -952,26 +993,23 @@ def contract_path(*operands, options=None, optimize=None): .. code-block:: python - from cuquantum import cutensornet, NetworkOptions, contract_path + from cuquantum import cutensornet as cutn + from cuquantum import contract, NetworkOptions - handle = cutensornet.create() + handle = cutn.create() network_opts = NetworkOptions(handle=handle, ...) path, info = contract_path(..., options=network_opts, ...) # ... the same handle can be reused for further calls ... # when it's done, remember to destroy the handle - cutensornet.destroy(handle) + cutn.destroy(handle) """ - options = utils.check_or_create_options(configuration.NetworkOptions, options, "network options") - - optimize = utils.check_or_create_options(configuration.OptimizerOptions, optimize, "path optimizer options") - # Create network. - with Network(*operands, options=options) as network: + with Network(*operands, qualifiers=qualifiers, options=options) as network: # Compute path. - path, opt_info = network.contract_path(optimize=optimize) + path, opt_info = network.contract_path(optimize=optimize, create_plan=False) return path, opt_info @@ -1102,6 +1140,6 @@ def einsum_path(*operands, optimize=True): with Network(*operands) as network: # Compute path. - path, opt_info = network.contract_path() + path, opt_info = network.contract_path(create_plan=False) return ['einsum_path', *path], str(opt_info) diff --git a/python/pyproject.toml b/python/pyproject.toml new file mode 100644 index 0000000..4f47793 --- /dev/null +++ b/python/pyproject.toml @@ -0,0 +1,13 @@ +# Copyright (c) 2021-2022, NVIDIA CORPORATION & AFFILIATES +# +# SPDX-License-Identifier: BSD-3-Clause + + +[build-system] +# Ideally we wanna add cuquantum to this list too, but its version +# constraint needs to be dynamically determined, and setuptools' +# support for dynamic dependencies is still on beta, so we use a +# custom PEP-517 backend to handle that instead. +requires = ["Cython>=0.29.22,<3", "packaging", "setuptools>=61.0.0", "wheel"] +build-backend = "pep517" +backend-path = ["builder"] diff --git a/python/samples/cutensornet/approxTN/gate_split_example.py b/python/samples/cutensornet/approxTN/gate_split_example.py new file mode 100644 index 0000000..6816685 --- /dev/null +++ b/python/samples/cutensornet/approxTN/gate_split_example.py @@ -0,0 +1,231 @@ +# Copyright (c) 2021-2022, 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("========================") + +################################################################################### +# Gate Split: A_{i,j,k,l} B_{k,o,p,q} G_{m,n,l,o}-> A'_{i,j,x,m} S_{x} B'_{x,n,p,q} +################################################################################### + +data_type = cuquantum.cudaDataType.CUDA_R_32F +compute_type = cuquantum.ComputeType.COMPUTE_32F + +# Create an array of modes + +modes_A_in = [ord(c) for c in ('i','j','k','l')] # input +modes_B_in = [ord(c) for c in ('k','o','p','q')] +modes_G_in = [ord(c) for c in ('m','n','l','o')] + +modes_A_out = [ord(c) for c in ('i','j','x','m')] # output +modes_B_out = [ord(c) for c in ('x','n','p','q')] + +# Create an array of extent (shapes) for each tensor +extent_A_in = (16, 16, 16, 2) +extent_B_in = (16, 2, 16, 16) +extent_G_in = (2, 2, 2, 2) + +shared_extent_out = 16 # truncate shared extent to 16 +extent_A_out = (16, 16, shared_extent_out, 2) +extent_B_out = (shared_extent_out, 2, 16, 16) + +############################ +# Allocate & initialize data +############################ +cp.random.seed(1) +A_in_d = cp.random.random(extent_A_in, dtype=np.float32).astype(np.float32, order='F') # we use fortran layout throughout this example +B_in_d = cp.random.random(extent_B_in, dtype=np.float32).astype(np.float32, order='F') +G_in_d = cp.random.random(extent_G_in, dtype=np.float32).astype(np.float32, order='F') + +A_out_d = cp.empty(extent_A_out, dtype=np.float32, order='F') +S_out_d = cp.empty(shared_extent_out, dtype=np.float32) +B_out_d = cp.empty(extent_B_out, dtype=np.float32, order='F') + +print("Allocate memory for data and initialize data.") + +free_mem, total_mem = dev.mem_info +worksize = free_mem *.7 + +############# +# cuTensorNet +############# + +stream = cp.cuda.Stream() +handle = cutn.create() + +nmode_A_in = len(modes_A_in) +nmode_B_in = len(modes_B_in) +nmode_G_in = len(modes_G_in) +nmode_A_out = len(modes_A_out) +nmode_B_out = len(modes_B_out) + +############################### +# Create tensor descriptors +############################### + +# strides are optional; if no stride (0) is provided, then cuTensorNet assumes a generalized column-major data layout +strides = 0 +desc_tensor_A_in = cutn.create_tensor_descriptor(handle, nmode_A_in, extent_A_in, strides, modes_A_in, data_type) +desc_tensor_B_in = cutn.create_tensor_descriptor(handle, nmode_B_in, extent_B_in, strides, modes_B_in, data_type) +desc_tensor_G_in = cutn.create_tensor_descriptor(handle, nmode_G_in, extent_G_in, strides, modes_G_in, data_type) + +desc_tensor_A_out = cutn.create_tensor_descriptor(handle, nmode_A_out, extent_A_out, strides, modes_A_out, data_type) +desc_tensor_B_out = cutn.create_tensor_descriptor(handle, nmode_B_out, extent_B_out, strides, modes_B_out, data_type) + +######################################## +# Setup gate split truncation parameters +######################################## + +svd_config = cutn.create_tensor_svd_config(handle) +absCutoff_dtype = cutn.tensor_svd_config_get_attribute_dtype(cutn.TensorSVDConfigAttribute.ABS_CUTOFF) +absCutoff = np.array(1e-2, dtype=absCutoff_dtype) + +cutn.tensor_svd_config_set_attribute(handle, + svd_config, cutn.TensorSVDConfigAttribute.ABS_CUTOFF, absCutoff.ctypes.data, absCutoff.dtype.itemsize) + +relCutoff_dtype = cutn.tensor_svd_config_get_attribute_dtype(cutn.TensorSVDConfigAttribute.REL_CUTOFF) +relCutoff = np.array(1e-2, dtype=relCutoff_dtype) + +cutn.tensor_svd_config_set_attribute(handle, + svd_config, cutn.TensorSVDConfigAttribute.REL_CUTOFF, relCutoff.ctypes.data, relCutoff.dtype.itemsize) + +# create SVDInfo to record truncation information +svd_info = cutn.create_tensor_svd_info(handle) + +gate_algo = cutn.GateSplitAlgo.REDUCED +print("Setup gate split truncation options.") + +############################### +# Query Workspace Size +############################### +work_desc = cutn.create_workspace_descriptor(handle) + +cutn.workspace_compute_gate_split_sizes(handle, + desc_tensor_A_in, desc_tensor_B_in, desc_tensor_G_in, + desc_tensor_A_out, desc_tensor_B_out, + gate_algo, svd_config, compute_type, work_desc) +required_workspace_size = cutn.workspace_get_size(handle, + work_desc, cutn.WorksizePref.MIN, cutn.Memspace.DEVICE) +if worksize < required_workspace_size: + raise MemoryError("Not enough workspace memory is available.") +work = cp.cuda.alloc(required_workspace_size) +cutn.workspace_set( + handle, work_desc, + cutn.Memspace.DEVICE, + work.ptr, required_workspace_size) + +print("Query and allocate required workspace.") + +########### +# Execution +########### + +min_time_cutensornet = 1e100 +num_runs = 3 # to get stable perf results +e1 = cp.cuda.Event() +e2 = cp.cuda.Event() + +for i in range(num_runs): + # restore output + A_out_d[:] = 0 + S_out_d[:] = 0 + B_out_d[:] = 0 + dev.synchronize() + + # restore output tensor descriptors as `cutensornet.gate_split` can potentially update the shared extent in desc_tensor_U/V. + # therefore we here restore desc_tensor_U/V to the original problem + cutn.destroy_tensor_descriptor(desc_tensor_A_out) + cutn.destroy_tensor_descriptor(desc_tensor_B_out) + desc_tensor_A_out = cutn.create_tensor_descriptor(handle, nmode_A_out, extent_A_out, strides, modes_A_out, data_type) + desc_tensor_B_out = cutn.create_tensor_descriptor(handle, nmode_B_out, extent_B_out, strides, modes_B_out, data_type) + + e1.record() + # execution + cutn.gate_split(handle, + desc_tensor_A_in, A_in_d.data.ptr, + desc_tensor_B_in, B_in_d.data.ptr, + desc_tensor_G_in, G_in_d.data.ptr, + desc_tensor_A_out, A_out_d.data.ptr, + S_out_d.data.ptr, + desc_tensor_B_out, B_out_d.data.ptr, + gate_algo, svd_config, compute_type, svd_info, work_desc, stream.ptr) + e2.record() + + # Synchronize and measure timing + e2.synchronize() + time = cp.cuda.get_elapsed_time(e1, e2) # ms + min_time_cutensornet = min_time_cutensornet if min_time_cutensornet < time else time + +full_extent_dtype = cutn.tensor_svd_info_get_attribute_dtype(cutn.TensorSVDInfoAttribute.FULL_EXTENT) +full_extent = np.empty(1, dtype=full_extent_dtype) +cutn.tensor_svd_info_get_attribute(handle, + svd_info, cutn.TensorSVDInfoAttribute.FULL_EXTENT, full_extent.ctypes.data, full_extent.itemsize) +full_extent = int(full_extent) + +reduced_extent_dtype = cutn.tensor_svd_info_get_attribute_dtype(cutn.TensorSVDInfoAttribute.REDUCED_EXTENT) +reduced_extent = np.empty(1, dtype=reduced_extent_dtype) +cutn.tensor_svd_info_get_attribute(handle, + svd_info, cutn.TensorSVDInfoAttribute.REDUCED_EXTENT, reduced_extent.ctypes.data, reduced_extent.itemsize) +reduced_extent = int(reduced_extent) + +discarded_weight_dtype = cutn.tensor_svd_info_get_attribute_dtype(cutn.TensorSVDInfoAttribute.DISCARDED_WEIGHT) +discarded_weight = np.empty(1, dtype=discarded_weight_dtype) +cutn.tensor_svd_info_get_attribute(handle, + svd_info, cutn.TensorSVDInfoAttribute.DISCARDED_WEIGHT, discarded_weight.ctypes.data, discarded_weight.itemsize) +discarded_weight = float(discarded_weight) + +print(f"Execution time: {min_time_cutensornet} ms") +print("SVD truncation info:") +print(f"For fixed extent truncation of {shared_extent_out}, an absolute cutoff value of {float(absCutoff)}, and a relative cutoff value of {float(relCutoff)}, full extent {full_extent} is reduced to {reduced_extent}") +print(f"Discarded weight: {discarded_weight}") + +# Recall that when we do value-based truncation through absolute or relative cutoff, +# the extent found at runtime maybe lower than we specified in desc_tensor_. +# Therefore we may need to create new containers to hold the new data which takes on fortran layout corresponding to the new extent + +if reduced_extent != shared_extent_out: + extent_A_out_reduced, strides_A_out = cutn.get_tensor_details(handle, desc_tensor_A_out)[2:] + extent_B_out_reduced, strides_B_out = cutn.get_tensor_details(handle, desc_tensor_B_out)[2:] + # note strides in cutensornet are in the unit of count and strides in cupy/numpy are in the unit of nbytes + strides_A_out = [i * A_out_d.itemsize for i in strides_A_out] + strides_B_out = [i * B_out_d.itemsize for i in strides_B_out] + A_out_d = cp.ndarray(extent_A_out_reduced, dtype=np.float32, memptr=A_out_d.data, strides=strides_A_out) + S_out_d = cp.ndarray(reduced_extent, dtype=np.float32, memptr=S_out_d.data, order='F') + B_out_d = cp.ndarray(extent_B_out_reduced, dtype=np.float32, memptr=B_out_d.data, strides=strides_B_out) + +T_d = cp.einsum("ijkl,kopq,mnlo->ijmnpq", A_in_d, B_in_d, G_in_d) +out = cp.einsum("ijxm,x,xnpq->ijmnpq", A_out_d, S_out_d, B_out_d) + +print(f"max diff after truncation {abs(out-T_d).max()}") +print("Check cuTensorNet result.") + +####################################################### + +cutn.destroy_tensor_descriptor(desc_tensor_A_in) +cutn.destroy_tensor_descriptor(desc_tensor_B_in) +cutn.destroy_tensor_descriptor(desc_tensor_G_in) +cutn.destroy_tensor_descriptor(desc_tensor_A_out) +cutn.destroy_tensor_descriptor(desc_tensor_B_out) +cutn.destroy_tensor_svd_config(svd_config) +cutn.destroy_tensor_svd_info(svd_info) +cutn.destroy_workspace_descriptor(work_desc) +cutn.destroy(handle) + +print("Free resource and exit.") diff --git a/python/samples/cutensornet/approxTN/mps_example.py b/python/samples/cutensornet/approxTN/mps_example.py new file mode 100644 index 0000000..6900565 --- /dev/null +++ b/python/samples/cutensornet/approxTN/mps_example.py @@ -0,0 +1,354 @@ +# Copyright (c) 2021-2022, NVIDIA CORPORATION & AFFILIATES +# +# SPDX-License-Identifier: BSD-3-Clause + +import itertools + +import cupy as cp +import numpy as np + +import cuquantum +from cuquantum import cutensornet as cutn + +class MPSHelper: + + """ + MPSHelper(num_sites, phys_extent, max_virtual_extent, initial_state, data_type, compute_type) + + Create an MPSHelper object for gate splitting algorithm. + i j + -------A-------B------- i j k + p| |q -------> -------A`-------B`------- + GGGGGGGGG r| |s + r| |s + + Args: + num_sites: The number of sites in the MPS. + phys_extents: The extent for the physical mode where the gate tensors are acted on. + max_virtual_extent: The maximal extent allowed for the virtual mode shared between adjacent MPS tensors. + initial_state: A sequence of :class:`cupy.ndarray` representing the initial state of the MPS. + data_type (cuquantum.cudaDataType): The data type for all tensors and gates. + compute_type (cuquantum.ComputeType): The compute type for all gate splitting. + + """ + + def __init__(self, num_sites, phys_extent, max_virtual_extent, initial_state, data_type, compute_type): + self.num_sites = num_sites + self.phys_extent = phys_extent + self.data_type = data_type + self.compute_type = compute_type + + self.phys_modes = [] + self.virtual_modes = [] + self.new_mode = itertools.count(start=0, step=1) + + for i in range(num_sites+1): + self.virtual_modes.append(next(self.new_mode)) + if i != num_sites: + self.phys_modes.append(next(self.new_mode)) + + untruncated_max_extent = phys_extent ** (num_sites // 2) + if max_virtual_extent == 0: + self.max_virtual_extent = untruncated_max_extent + else: + self.max_virtual_extent = min(max_virtual_extent, untruncated_max_extent) + + self.handle = cutn.create() + self.work_desc = cutn.create_workspace_descriptor(self.handle) + self.svd_config = cutn.create_tensor_svd_config(self.handle) + self.svd_info = cutn.create_tensor_svd_info(self.handle) + self.gate_algo = cutn.GateSplitAlgo.DIRECT + + self.desc_tensors = [] + self.state_tensors = [] + + # create tensor descriptors + for i in range(self.num_sites): + self.state_tensors.append(initial_state[i].astype(tensor.dtype, order="F")) + extent = self.get_tensor_extent(i) + modes = self.get_tensor_modes(i) + desc_tensor = cutn.create_tensor_descriptor(self.handle, 3, extent, 0, modes, self.data_type) + self.desc_tensors.append(desc_tensor) + + def get_tensor(self, site): + """Get the tensor operands for a specific site.""" + return self.state_tensors[site] + + def get_tensor_extent(self, site): + """Get the extent of the MPS tensor at a specific site.""" + return self.state_tensors[site].shape + + def get_tensor_modes(self, site): + """Get the current modes of the MPS tensor at a specific site.""" + return (self.virtual_modes[site], self.phys_modes[site], self.virtual_modes[site+1]) + + def set_svd_config(self, abs_cutoff, rel_cutoff, renorm, partition): + """Update the SVD truncation setting. + + Args: + abs_cutoff: The cutoff value for absolute singular value truncation. + rel_cutoff: The cutoff value for relative singular value truncation. + renorm (cuquantum.cutensornet.TensorSVDNormalization): The option for renormalization of the truncated singular values. + partition (cuquantum.cutensornet.TensorSVDPartition): The option for partitioning of the singular values. + """ + + if partition != cutn.TensorSVDPartition.UV_EQUAL: + raise NotImplementedError("this basic example expects partition to be cutensornet.TensorSVDPartition.UV_EQUAL") + + svd_config_attributes = [cutn.TensorSVDConfigAttribute.ABS_CUTOFF, + cutn.TensorSVDConfigAttribute.REL_CUTOFF, + cutn.TensorSVDConfigAttribute.S_NORMALIZATION, + cutn.TensorSVDConfigAttribute.S_PARTITION] + + for (attr, value) in zip(svd_config_attributes, [abs_cutoff, rel_cutoff, renorm, partition]): + dtype = cutn.tensor_svd_config_get_attribute_dtype(attr) + value = np.array([value], dtype=dtype) + cutn.tensor_svd_config_set_attribute(self.handle, + self.svd_config, attr, value.ctypes.data, value.dtype.itemsize) + + def set_gate_algorithm(self, gate_algo): + """Set the algorithm to use for all gate split operations. + + Args: + gate_algo (cuquantum.cutensornet.GateSplitAlgo): The gate splitting algorithm to use. + """ + + self.gate_algo = gate_algo + + def compute_max_workspace_sizes(self): + """Compute the maximal workspace needed for MPS gating algorithm.""" + modes_in_A = [ord(c) for c in ('i', 'p', 'j')] + modes_in_B = [ord(c) for c in ('j', 'q', 'k')] + modes_in_G = [ord(c) for c in ('p', 'q', 'r', 's')] + modes_out_A = [ord(c) for c in ('i', 'r', 'j')] + modes_out_B = [ord(c) for c in ('j', 's', 'k')] + + max_extents_AB = (self.max_virtual_extent, self.phys_extent, self.max_virtual_extent) + extents_in_G = (self.phys_extent, self.phys_extent, self.phys_extent, self.phys_extent) + + desc_tensor_in_A = cutn.create_tensor_descriptor(self.handle, 3, max_extents_AB, 0, modes_in_A, self.data_type) + desc_tensor_in_B = cutn.create_tensor_descriptor(self.handle, 3, max_extents_AB, 0, modes_in_B, self.data_type) + desc_tensor_in_G = cutn.create_tensor_descriptor(self.handle, 4, extents_in_G, 0, modes_in_G, self.data_type) + desc_tensor_out_A = cutn.create_tensor_descriptor(self.handle, 3, max_extents_AB, 0, modes_out_A, self.data_type) + desc_tensor_out_B = cutn.create_tensor_descriptor(self.handle, 3, max_extents_AB, 0, modes_out_B, self.data_type) + + cutn.workspace_compute_gate_split_sizes(self.handle, + desc_tensor_in_A, desc_tensor_in_B, desc_tensor_in_G, + desc_tensor_out_A, desc_tensor_out_B, + self.gate_algo, self.svd_config, self.compute_type, self.work_desc) + + workspace_size = cutn.workspace_get_size(self.handle, self.work_desc, cutn.WorksizePref.MIN, cutn.Memspace.DEVICE) + + # free resources + cutn.destroy_tensor_descriptor(desc_tensor_in_A) + cutn.destroy_tensor_descriptor(desc_tensor_in_B) + cutn.destroy_tensor_descriptor(desc_tensor_in_G) + cutn.destroy_tensor_descriptor(desc_tensor_out_A) + cutn.destroy_tensor_descriptor(desc_tensor_out_B) + return workspace_size + + def set_workspace(self, work, workspace_size): + """Compute the maximal workspace needed for MPS gating algorithm. + + Args: + work: Pointer to the allocated workspace. + workspace_size: The required workspace size on the device. + """ + cutn.workspace_set(self.handle, self.work_desc, cutn.Memspace.DEVICE, work.ptr, workspace_size) + + def apply_gate(self, site_A, site_B, gate, verbose, stream): + """Inplace execution of the apply gate algoritm on site A and site B. + + Args: + site_A: The first site on which the gate is applied to. + site_B: The second site on which the gate is applied to. + gate (cupy.ndarray): The input data for the gate tensor. + verbose: Whether to print out the runtime information during truncation. + stream (cupy.cuda.Stream): The CUDA stream on which the computation is performed. + """ + if site_B - site_A != 1: + raise ValueError("Site B must be the right site of site A") + if site_B >= self.num_sites: + raise ValueError("Site index cannot exceed maximum number of sites") + + desc_tensor_in_A = self.desc_tensors[site_A] + desc_tensor_in_B = self.desc_tensors[site_B] + + phys_mode_in_A = self.phys_modes[site_A] + phys_mode_in_B = self.phys_modes[site_B] + phys_mode_out_A = next(self.new_mode) + phys_mode_out_B = next(self.new_mode) + modes_G = (phys_mode_in_A, phys_mode_in_B, phys_mode_out_A, phys_mode_out_B) + extent_G = (self.phys_extent, self.phys_extent, self.phys_extent, self.phys_extent) + desc_tensor_in_G = cutn.create_tensor_descriptor(self.handle, 4, extent_G, 0, modes_G, self.data_type) + + # construct and initialize the expected output A and B + tensor_in_A = self.state_tensors[site_A] + tensor_in_B = self.state_tensors[site_B] + left_extent_A = tensor_in_A.shape[0] + extent_AB_in = tensor_in_A.shape[2] + right_extent_B = tensor_in_B.shape[2] + combined_extent_left = min(left_extent_A, extent_AB_in * self.phys_extent) * self.phys_extent + combined_extent_right = min(right_extent_B, extent_AB_in * self.phys_extent) * self.phys_extent + extent_Aout_B = min(combined_extent_left, combined_extent_right, self.max_virtual_extent) + + extent_out_A = (left_extent_A, self.phys_extent, extent_Aout_B) + extent_out_B = (extent_Aout_B, self.phys_extent, right_extent_B) + + tensor_out_A = cp.zeros(extent_out_A, dtype=tensor_in_A.dtype, order="F") + tensor_out_B = cp.zeros(extent_out_B, dtype=tensor_in_B.dtype, order="F") + + # create tensor descriptors for output A and B + modes_out_A = (self.virtual_modes[site_A], phys_mode_out_A, self.virtual_modes[site_A+1]) + modes_out_B = (self.virtual_modes[site_B], phys_mode_out_B, self.virtual_modes[site_B+1]) + + desc_tensor_out_A = cutn.create_tensor_descriptor(self.handle, 3, extent_out_A, 0, modes_out_A, self.data_type) + desc_tensor_out_B = cutn.create_tensor_descriptor(self.handle, 3, extent_out_B, 0, modes_out_B, self.data_type) + + cutn.gate_split(self.handle, + desc_tensor_in_A, tensor_in_A.data.ptr, + desc_tensor_in_B, tensor_in_B.data.ptr, + desc_tensor_in_G, gate.data.ptr, + desc_tensor_out_A, tensor_out_A.data.ptr, + 0, # we factorize singular values equally onto output A and B. + desc_tensor_out_B, tensor_out_B.data.ptr, + self.gate_algo, self.svd_config, self.compute_type, + self.svd_info, self.work_desc, stream.ptr) + + if verbose: + full_extent = np.array([0], dtype=cutn.tensor_svd_info_get_attribute_dtype(cutn.TensorSVDInfoAttribute.FULL_EXTENT)) + reduced_extent = np.array([0], dtype=cutn.tensor_svd_info_get_attribute_dtype(cutn.TensorSVDInfoAttribute.REDUCED_EXTENT)) + discarded_weight = np.array([0], dtype=cutn.tensor_svd_info_get_attribute_dtype(cutn.TensorSVDInfoAttribute.DISCARDED_WEIGHT)) + + cutn.tensor_svd_info_get_attribute( + self.handle, self.svd_info, cutn.TensorSVDInfoAttribute.FULL_EXTENT, + full_extent.ctypes.data, full_extent.dtype.itemsize) + cutn.tensor_svd_info_get_attribute( + self.handle, self.svd_info, cutn.TensorSVDInfoAttribute.REDUCED_EXTENT, + reduced_extent.ctypes.data, reduced_extent.dtype.itemsize) + cutn.tensor_svd_info_get_attribute( + self.handle, self.svd_info, cutn.TensorSVDInfoAttribute.DISCARDED_WEIGHT, + discarded_weight.ctypes.data, discarded_weight.dtype.itemsize) + + print("Virtual bond truncated from {0} to {1} with a discarded weight of {2:.6f}".format(full_extent[0], reduced_extent[0], discarded_weight[0])) + + self.phys_modes[site_A] = phys_mode_out_A + self.phys_modes[site_B] = phys_mode_out_B + self.desc_tensors[site_A] = desc_tensor_out_A + self.desc_tensors[site_B] = desc_tensor_out_B + + extent_out_A = np.zeros((3,), dtype=np.int64) + extent_out_B = np.zeros((3,), dtype=np.int64) + extent_out_A, strides_out_A = cutn.get_tensor_details(self.handle, desc_tensor_out_A)[2:] + extent_out_B, strides_out_B = cutn.get_tensor_details(self.handle, desc_tensor_out_B)[2:] + + # Recall that `cutensornet.gate_split` can potentially find reduced extent during SVD truncation when value-based truncation is used. + # Therefore we here update the container for output tensor A and B. + if extent_out_A[2] != extent_Aout_B: + # note strides in cutensornet are in the unit of count and strides in cupy/numpy are in the unit of nbytes + strides_out_A = [i * tensor_out_A.itemsize for i in strides_out_A] + strides_out_B = [i * tensor_out_B.itemsize for i in strides_out_B] + tensor_out_A = cp.ndarray(extent_out_A, dtype=tensor_out_A.dtype, memptr=tensor_out_A.data, strides=strides_out_A) + tensor_out_B = cp.ndarray(extent_out_B, dtype=tensor_out_B.dtype, memptr=tensor_out_B.data, strides=strides_out_B) + + self.state_tensors[site_A] = tensor_out_A + self.state_tensors[site_B] = tensor_out_B + + cutn.destroy_tensor_descriptor(desc_tensor_in_A) + cutn.destroy_tensor_descriptor(desc_tensor_in_B) + cutn.destroy_tensor_descriptor(desc_tensor_in_G) + + def __del__(self): + """Free all resources owned by the object.""" + for desc_tensor in self.desc_tensors: + cutn.destroy_tensor_descriptor(desc_tensor) + cutn.destroy(self.handle) + cutn.destroy_workspace_descriptor(self.work_desc) + cutn.destroy_tensor_svd_config(self.svd_config) + cutn.destroy_tensor_svd_info(self.svd_info) + + +if __name__ == '__main__': + + 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("========================") + + data_type = cuquantum.cudaDataType.CUDA_C_64F + compute_type = cuquantum.ComputeType.COMPUTE_64F + + num_sites = 16 + phys_extent = 2 + max_virtual_extent = 12 + + ## we initialize the MPS state as a product state |000...000> + initial_state = [] + for i in range(num_sites): + # we create dummpy indices for MPS tensors on the boundary for easier bookkeeping + # we'll use Fortran layout throughout this example + tensor = cp.zeros((1,2,1), dtype=np.complex128, order="F") + tensor[0,0,0] = 1.0 + initial_state.append(tensor) + + ################################## + # Initialize an MPSHelper object + ################################## + + mps_helper = MPSHelper(num_sites, phys_extent, max_virtual_extent, initial_state, data_type, compute_type) + + ################################## + # Setup options for gate operation + ################################## + + abs_cutoff = 1e-2 + rel_cutoff = 1e-2 + renorm = cutn.TensorSVDNormalization.L2 + partition = cutn.TensorSVDPartition.UV_EQUAL + mps_helper.set_svd_config(abs_cutoff, rel_cutoff, renorm, partition) + + gate_algo = cutn.GateSplitAlgo.REDUCED + mps_helper.set_gate_algorithm(gate_algo) + + ##################################### + # Workspace estimation and allocation + ##################################### + + free_mem, total_mem = dev.mem_info + worksize = free_mem *.7 + required_workspace_size = mps_helper.compute_max_workspace_sizes() + work = cp.cuda.alloc(worksize) + print(f"Maximal workspace size requried: {required_workspace_size / 1024 ** 3:.3f} GB") + mps_helper.set_workspace(work, required_workspace_size) + + ########### + # Execution + ########### + + stream = cp.cuda.Stream() + cp.random.seed(0) + num_layers = 10 + for i in range(num_layers): + start_site = i % 2 + print(f"Cycle {i}:") + verbose = (i == num_layers-1) + for j in range(start_site, num_sites-1, 2): + # initialize a random 2-qubit gate + gate = cp.random.random([phys_extent,]*4) + 1.j * cp.random.random([phys_extent,]*4) + gate = gate.astype(gate.dtype, order="F") + mps_helper.apply_gate(j, j+1, gate, verbose, stream) + + stream.synchronize() + print("========================") + print("After gate application") + for i in range(num_sites): + tensor = mps_helper.get_tensor(i) + modes = mps_helper.get_tensor_modes(i) + print(f"Site {i}, extent: {tensor.shape}, modes: {modes}") \ No newline at end of file diff --git a/python/samples/cutensornet/approxTN/tensor_qr_example.py b/python/samples/cutensornet/approxTN/tensor_qr_example.py new file mode 100644 index 0000000..4008436 --- /dev/null +++ b/python/samples/cutensornet/approxTN/tensor_qr_example.py @@ -0,0 +1,139 @@ +# Copyright (c) 2021-2022, 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("========================") + +############################################### +# Tensor QR: T_{i,j,m,n} -> Q_{i,x,m} R_{n,x,j} +############################################### + +data_type = cuquantum.cudaDataType.CUDA_R_32F + +# Create an array of modes + +modes_T = [ord(c) for c in ('i','j','m','n')] # input +modes_Q = [ord(c) for c in ('i','x','m')] # QR output +modes_R = [ord(c) for c in ('n','x','j')] + +# Create an array of extent (shapes) for each tensor +extent_T = (16, 16, 16, 16) +extent_Q = (16, 256, 16) +extent_R = (16, 256, 16) + +############################ +# Allocate & initialize data +############################ + +T_d = cp.random.random(extent_T, dtype=np.float32).astype(np.float32, order='F') # we use fortran layout throughout this example +Q_d = cp.empty(extent_Q, dtype=np.float32, order='F') +R_d = cp.empty(extent_R, dtype=np.float32, order='F') + +print("Allocate memory for data and initialize data.") + +free_mem, total_mem = dev.mem_info +worksize = free_mem *.7 + +############# +# cuTensorNet +############# +stream = cp.cuda.Stream() +handle = cutn.create() + +nmode_T = len(modes_T) +nmode_Q = len(modes_Q) +nmode_R = len(modes_R) + +############################### +# Create tensor descriptors +############################### + +# strides are optional; if no stride (0) is provided, then cuTensorNet assumes a generalized column-major data layout +strides = 0 +desc_tensor_T = cutn.create_tensor_descriptor(handle, nmode_T, extent_T, strides, modes_T, data_type) +desc_tensor_Q = cutn.create_tensor_descriptor(handle, nmode_Q, extent_Q, strides, modes_Q, data_type) +desc_tensor_R = cutn.create_tensor_descriptor(handle, nmode_R, extent_R, strides, modes_R, data_type) + +####################################### +# Query and allocate required workspace +####################################### +work_desc = cutn.create_workspace_descriptor(handle) + +cutn.workspace_compute_qr_sizes(handle, desc_tensor_T, desc_tensor_Q, desc_tensor_R, work_desc) +required_workspace_size = cutn.workspace_get_size(handle, + work_desc, cutn.WorksizePref.MIN, cutn.Memspace.DEVICE) +if worksize < required_workspace_size: + raise MemoryError("Not enough workspace memory is available.") +work = cp.cuda.alloc(required_workspace_size) +cutn.workspace_set( + handle, work_desc, + cutn.Memspace.DEVICE, + work.ptr, required_workspace_size) + +print("Query and allocate required workspace.") + +########### +# Execution +########### + +min_time_cutensornet = 1e100 +num_runs = 3 # to get stable perf results +e1 = cp.cuda.Event() +e2 = cp.cuda.Event() + +for i in range(num_runs): + # restore output + Q_d[:] = 0 + R_d[:] = 0 + dev.synchronize() + + e1.record() + # execution + cutn.tensor_qr(handle, desc_tensor_T, T_d.data.ptr, + desc_tensor_Q, Q_d.data.ptr, + desc_tensor_R, R_d.data.ptr, + work_desc, stream.ptr) + e2.record() + + # Synchronize and measure timing + e2.synchronize() + time = cp.cuda.get_elapsed_time(e1, e2) # ms + min_time_cutensornet = min_time_cutensornet if min_time_cutensornet < time else time + +print(f"Execution time: {min_time_cutensornet} ms") + +out = cp.einsum("ixm,nxj->ijmn", Q_d, R_d) + +rtol = atol = 1e-5 +if not cp.allclose(out, T_d, rtol=rtol, atol=atol): + raise RuntimeError(f"result is incorrect, max diff {abs(out-T_d).max()}") +print("Check cuTensorNet result.") + +################ +# Free resources +################ + +cutn.destroy_tensor_descriptor(desc_tensor_T) +cutn.destroy_tensor_descriptor(desc_tensor_Q) +cutn.destroy_tensor_descriptor(desc_tensor_R) +cutn.destroy_workspace_descriptor(work_desc) +cutn.destroy(handle) + +print("Free resource and exit.") diff --git a/python/samples/cutensornet/approxTN/tensor_svd_example.py b/python/samples/cutensornet/approxTN/tensor_svd_example.py new file mode 100644 index 0000000..6c5e065 --- /dev/null +++ b/python/samples/cutensornet/approxTN/tensor_svd_example.py @@ -0,0 +1,208 @@ +# Copyright (c) 2021-2022, 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("========================") + +###################################################### +# Tensor SVD: T_{i,j,m,n} -> U_{i,x,m} S_{x} V_{n,x,j} +###################################################### + +data_type = cuquantum.cudaDataType.CUDA_R_32F + +# Create an array of modes + +modes_T = [ord(c) for c in ('i','j','m','n')] # input +modes_U = [ord(c) for c in ('i','x','m')] # SVD output +modes_V = [ord(c) for c in ('n','x','j')] + +# Create an array of extent (shapes) for each tensor +extent_T = (16, 16, 16, 16) +shared_extent = 256 // 2 # truncate shared extent from 256 to 128 +extent_U = (16, shared_extent, 16) +extent_V = (16, shared_extent, 16) + +############################ +# Allocate & initialize data +############################ +cp.random.seed(1) +T_d = cp.random.random(extent_T, dtype=np.float32).astype(np.float32, order='F') # we use fortran layout throughout this example +U_d = cp.empty(extent_U, dtype=np.float32, order='F') +S_d = cp.empty(shared_extent, dtype=np.float32) +V_d = cp.empty(extent_V, dtype=np.float32, order='F') + +print("Allocate memory for data and initialize data.") + +free_mem, total_mem = dev.mem_info +worksize = free_mem *.7 + +############# +# cuTensorNet +############# + +stream = cp.cuda.Stream() +handle = cutn.create() + +nmode_T = len(modes_T) +nmode_U = len(modes_U) +nmode_V = len(modes_V) + +############################### +# Create tensor descriptor +############################### + +# strides are optional; if no stride (0) is provided, then cuTensorNet assumes a generalized column-major data layout +strides = 0 +desc_tensor_T = cutn.create_tensor_descriptor(handle, nmode_T, extent_T, strides, modes_T, data_type) +desc_tensor_U = cutn.create_tensor_descriptor(handle, nmode_U, extent_U, strides, modes_U, data_type) +desc_tensor_V = cutn.create_tensor_descriptor(handle, nmode_V, extent_V, strides, modes_V, data_type) + +################################## +# Setup SVD truncation parameters +################################## + +svd_config = cutn.create_tensor_svd_config(handle) +abs_cutoff_dtype = cutn.tensor_svd_config_get_attribute_dtype(cutn.TensorSVDConfigAttribute.ABS_CUTOFF) +abs_cutoff = np.array(1e-2, dtype=abs_cutoff_dtype) + +cutn.tensor_svd_config_set_attribute(handle, + svd_config, cutn.TensorSVDConfigAttribute.ABS_CUTOFF, abs_cutoff.ctypes.data, abs_cutoff.dtype.itemsize) + +rel_cutoff_dtype = cutn.tensor_svd_config_get_attribute_dtype(cutn.TensorSVDConfigAttribute.REL_CUTOFF) +rel_cutoff = np.array(4e-2, dtype=rel_cutoff_dtype) + +cutn.tensor_svd_config_set_attribute(handle, + svd_config, cutn.TensorSVDConfigAttribute.REL_CUTOFF, rel_cutoff.ctypes.data, rel_cutoff.dtype.itemsize) + +print("Setup SVD truncation parameters.") + +# create SVDInfo to record truncation information +svd_info = cutn.create_tensor_svd_info(handle) + +############################### +# Query Workspace Size +############################### +work_desc = cutn.create_workspace_descriptor(handle) + +cutn.workspace_compute_svd_sizes(handle, desc_tensor_T, desc_tensor_U, desc_tensor_V, svd_config, work_desc) +required_workspace_size = cutn.workspace_get_size(handle, + work_desc, cutn.WorksizePref.MIN, cutn.Memspace.DEVICE) +if worksize < required_workspace_size: + raise MemoryError("Not enough workspace memory is available.") +work = cp.cuda.alloc(required_workspace_size) +cutn.workspace_set( + handle, work_desc, + cutn.Memspace.DEVICE, + work.ptr, required_workspace_size) + +print("Query and allocate required workspace.") + +##### +# Run +##### + +min_time_cutensornet = 1e100 +num_runs = 3 # to get stable perf results +e1 = cp.cuda.Event() +e2 = cp.cuda.Event() + +for i in range(num_runs): + # restore output + U_d[:] = 0 + S_d[:] = 0 + V_d[:] = 0 + dev.synchronize() + + # restore output tensor descriptors as `cutensornet.tensor_svd` can potentially update the shared extent in desc_tensor_U/V. + # therefore we here restore desc_tensor_U/V to the original problem + cutn.destroy_tensor_descriptor(desc_tensor_U) + cutn.destroy_tensor_descriptor(desc_tensor_V) + desc_tensor_U = cutn.create_tensor_descriptor(handle, nmode_U, extent_U, strides, modes_U, data_type) + desc_tensor_V = cutn.create_tensor_descriptor(handle, nmode_V, extent_V, strides, modes_V, data_type) + + e1.record() + # execution + cutn.tensor_svd(handle, desc_tensor_T, T_d.data.ptr, + desc_tensor_U, U_d.data.ptr, + S_d.data.ptr, + desc_tensor_V, V_d.data.ptr, + svd_config, svd_info, + work_desc, stream.ptr) + + e2.record() + + # Synchronize and measure timing + e2.synchronize() + time = cp.cuda.get_elapsed_time(e1, e2) # ms + min_time_cutensornet = min_time_cutensornet if min_time_cutensornet < time else time + +full_extent_dtype = cutn.tensor_svd_info_get_attribute_dtype(cutn.TensorSVDInfoAttribute.FULL_EXTENT) +full_extent = np.empty(1, dtype=full_extent_dtype) +cutn.tensor_svd_info_get_attribute(handle, + svd_info, cutn.TensorSVDInfoAttribute.FULL_EXTENT, full_extent.ctypes.data, full_extent.itemsize) +full_extent = int(full_extent) + +reduced_extent_dtype = cutn.tensor_svd_info_get_attribute_dtype(cutn.TensorSVDInfoAttribute.REDUCED_EXTENT) +reduced_extent = np.empty(1, dtype=reduced_extent_dtype) +cutn.tensor_svd_info_get_attribute(handle, + svd_info, cutn.TensorSVDInfoAttribute.REDUCED_EXTENT, reduced_extent.ctypes.data, reduced_extent.itemsize) +reduced_extent = int(reduced_extent) + +discarded_weight_dtype = cutn.tensor_svd_info_get_attribute_dtype(cutn.TensorSVDInfoAttribute.DISCARDED_WEIGHT) +discarded_weight = np.empty(1, dtype=discarded_weight_dtype) +cutn.tensor_svd_info_get_attribute(handle, + svd_info, cutn.TensorSVDInfoAttribute.DISCARDED_WEIGHT, discarded_weight.ctypes.data, discarded_weight.itemsize) +discarded_weight = float(discarded_weight) + +print(f"Execution time: {min_time_cutensornet} ms") +print("SVD truncation info:") +print(f"For fixed extent truncation of {shared_extent}, an absolute cutoff value of {float(abs_cutoff)}, and a relative cutoff value of {float(rel_cutoff)}, full extent {full_extent} is reduced to {reduced_extent}") +print(f"Discarded weight: {discarded_weight}") + +# Recall that when we do value-based truncation through absolute or relative cutoff, +# the extent found at runtime maybe lower than we specified in desc_tensor_. +# Therefore we may need to create new containers to hold the new data which takes on fortran layout corresponding to the new extent +extent_U_out, strides_U_out = cutn.get_tensor_details(handle, desc_tensor_U)[2:] +extent_V_out, strides_V_out = cutn.get_tensor_details(handle, desc_tensor_V)[2:] + +if extent_U_out[1] != shared_extent: + # note strides in cutensornet are in the unit of count and strides in cupy/numpy are in the unit of nbytes + strides_U_out = [i * U_d.itemsize for i in strides_U_out] + strides_V_out = [i * V_d.itemsize for i in strides_V_out] + U_d = cp.ndarray(extent_U_out, dtype=np.float32, memptr=U_d.data, strides=strides_U_out) + S_d = cp.ndarray(extent_U_out[1], dtype=np.float32, memptr=S_d.data, order='F') + V_d = cp.ndarray(extent_V_out, dtype=np.float32, memptr=V_d.data, strides=strides_V_out) + +out = cp.einsum("ixm,x,nxj->ijmn", U_d, S_d, V_d) + +print(f"max diff after truncation {abs(out-T_d).max()}") +print("Check cuTensorNet result.") + +####################################################### + +cutn.destroy_tensor_descriptor(desc_tensor_T) +cutn.destroy_tensor_descriptor(desc_tensor_U) +cutn.destroy_tensor_descriptor(desc_tensor_V) +cutn.destroy_workspace_descriptor(work_desc) +cutn.destroy_tensor_svd_config(svd_config) +cutn.destroy_tensor_svd_info(svd_info) +cutn.destroy(handle) + +print("Free resource and exit.") diff --git a/python/samples/cutensornet/circuit_converter/cirq_advanced.ipynb b/python/samples/cutensornet/circuit_converter/cirq_advanced.ipynb index 713e367..79967fe 100644 --- a/python/samples/cutensornet/circuit_converter/cirq_advanced.ipynb +++ b/python/samples/cutensornet/circuit_converter/cirq_advanced.ipynb @@ -154,7 +154,7 @@ }, { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -392,7 +392,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Exact energy from cutn: 0.44448295245377206\n" + "Exact energy from cutn: 0.4444829524537696\n" ] } ], @@ -400,12 +400,11 @@ "def compute_energy_cutn(resolved_circuit, length, h, jr, jc):\n", " nrow = ncol = length\n", " assert length == jr.shape[1] == jc.shape[0]\n", - " Zop = cp.diag([1,-1]).astype('complex128')\n", " \n", - " def compute_rdm(myconverter, where, options):\n", - " expression, operands = myconverter.reduced_density_matrix(where, lightcone=True)\n", - " rdm = contract(expression, *operands, options=options)\n", - " return rdm\n", + " def compute_energy_term(myconverter, pauli_string, options):\n", + " expression, operands = myconverter.expectation(pauli_string, lightcone=True)\n", + " e = contract(expression, *operands, options=options).real\n", + " return e\n", " \n", " def expectation(x):\n", " energy = 0.\n", @@ -418,30 +417,25 @@ " \n", " for i in range(nrow):\n", " for j in range(ncol):\n", - " # for one-body terms, we construct the 1-RDM for each qubit\n", + " # one-body terms\n", " q = qubits[i*ncol+j]\n", - " where = (q,)\n", - " rdm = compute_rdm(myconverter, where, options)\n", - " energy += cp.sum(rdm * Zop).real * h[i][j]\n", - " # this would work too\n", - " # energy += contract('ij,ij->', rdm, Zop, options=options).real * h[i][j]\n", + " pauli_string = {q: 'Z'}\n", + " energy += compute_energy_term(myconverter, pauli_string, options) * h[i][j]\n", " \n", - " # for two-body terms, we construct the 2-RDM for all adjacent pairs of qubits\n", + " # two-body terms\n", " # - vertical bond\n", " if i != nrow-1:\n", " top = qubits[i*ncol+j]\n", " bottom = qubits[(i+1)*ncol+j]\n", - " where = (top, bottom)\n", - " rdm = compute_rdm(myconverter, where, options)\n", - " energy += cp.einsum('ijIJ,iI,jJ->', rdm, Zop, Zop).real * jr[i][j]\n", + " pauli_string = {top: 'Z', bottom: 'Z'}\n", + " energy += compute_energy_term(myconverter, pauli_string, options) * jr[i][j]\n", " \n", " # - horizontal bond\n", " if j != ncol-1:\n", " left = qubits[i*ncol+j]\n", " right = qubits[i*ncol+(j+1)]\n", - " where = (left, right)\n", - " rdm = compute_rdm(myconverter, where, options)\n", - " energy += cp.einsum('ijIJ,iI,jJ->', rdm, Zop, Zop).real * jc[i][j]\n", + " pauli_string = {left:'Z', right:'Z'}\n", + " energy += compute_energy_term(myconverter, pauli_string, options) * jc[i][j]\n", " \n", " # handle should be explictly destroyed\n", " cutn.destroy(handle)\n", @@ -526,7 +520,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -563,7 +557,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.10" + "version": "3.9.12" } }, "nbformat": 4, diff --git a/python/samples/cutensornet/circuit_converter/cirq_basic.ipynb b/python/samples/cutensornet/circuit_converter/cirq_basic.ipynb index 85b9eab..846d33a 100644 --- a/python/samples/cutensornet/circuit_converter/cirq_basic.ipynb +++ b/python/samples/cutensornet/circuit_converter/cirq_basic.ipynb @@ -26,6 +26,8 @@ "metadata": {}, "outputs": [], "source": [ + "import itertools\n", + "\n", "import cirq\n", "from cirq.testing import random_circuit\n", "import cupy as cp\n", @@ -162,9 +164,9 @@ "einsum expression:\n", "a,b,c,d,e,f,g,ha,ijdb,kg,lf,mc,nm,ok,pqhj,ri,sp,tl,uvqe,wn,xs,yzwo,Ar,By,CDuv,EFtx,Gz,HIDF,JC,KLGE,I,J,B,A,H,L,K->\n", "\n", - "for bitstring 0000000, amplitude: (0.17677669529663717+0j), probability: 0.031250000000000104\n", + "for bitstring 0000000, amplitude: (0.17677669529663714+0j), probability: 0.03125000000000009\n", "\n", - "difference from state vector: 0.0\n" + "difference from state vector: 2.7755575615628914e-17\n" ] } ], @@ -183,6 +185,98 @@ "print(f'difference from state vector: {amp_diff}')" ] }, + { + "cell_type": "markdown", + "id": "34161bd5-0a1b-4972-b84e-0ae34b7ab216", + "metadata": {}, + "source": [ + "### calculate batch of bistring amplitudes\n", + "\n", + "In this example, we calculate a batch of bistring amplitudes $\\langle 00000ij|\\psi\\rangle$ where the first 5 qubits are fixed at state $00000$ and the last two qubit states are batched. This is equivalent to computing a slice of the full state vector." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "e5e6a666-a586-4ec2-ba03-7398049cc721", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "for bitstring 0000000, amplitude: 0.1768+0.0000j, difference from state vector: 0.0000\n", + "for bitstring 0000001, amplitude: 0.0000+0.0000j, difference from state vector: 0.0000\n", + "for bitstring 0000010, amplitude: 0.0000+0.0000j, difference from state vector: 0.0000\n", + "for bitstring 0000011, amplitude: 0.1250+0.1250j, difference from state vector: 0.0000\n" + ] + } + ], + "source": [ + "fixed_states = '00000'\n", + "fixed_index = tuple(map(int, fixed_states))\n", + "num_fixed = len(fixed_states)\n", + "\n", + "# mapping of the first 5 qubits to the fixed state\n", + "fixed = dict(zip(myconverter.qubits[:num_fixed], fixed_states))\n", + "\n", + "expression, operands = myconverter.batched_amplitudes(fixed)\n", + "batched_amplitudes = contract(expression, *operands)\n", + "\n", + "for ibit, jbit in itertools.product(range(2), repeat=2):\n", + " bitstring = fixed_states + str(ibit) + str(jbit)\n", + " index = fixed_index + (ibit, jbit)\n", + " amplitude = batched_amplitudes[(ibit, jbit)]\n", + " amplitude_from_sv = sv[index]\n", + " amp_diff = abs(amplitude-amplitude_from_sv)\n", + " print(f'for bitstring {bitstring}, amplitude: {amplitude:.4f}, difference from state vector: {amp_diff:.4f}')" + ] + }, + { + "cell_type": "markdown", + "id": "773d861a-1b00-4bfa-8a33-4e766954e560", + "metadata": {}, + "source": [ + "### compute expectation value $\\langle \\psi|\\hat{O}| \\psi\\rangle$\n", + "\n", + "In this example, we compute the expectation value for a pauli string $IIYXIXY$. For comparision, we compute the same value via contracting reduced density matrix with the operator." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "b05c257d-602a-4473-83ee-93adbe701b5a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "expectation value for IIYXIXY: (0.5000000000000016+0j)\n", + "is expectation value in agreement? True\n" + ] + } + ], + "source": [ + "pauli_string = 'IIYXIXY'\n", + "expression, operands = myconverter.expectation(pauli_string, lightcone=True)\n", + "expec = contract(expression, *operands)\n", + "print(f'expectation value for {pauli_string}: {expec}')\n", + "\n", + "# expectation value from reduced density matrix\n", + "qubits = myconverter.qubits\n", + "where = qubits[2:4] + qubits[5:]\n", + "rdm_expression, rdm_operands = myconverter.reduced_density_matrix(where, lightcone=True)\n", + "rdm = contract(rdm_expression, *rdm_operands)\n", + "\n", + "pauli_x = cp.asarray([[0,1],[1,0]], dtype=myconverter.dtype)\n", + "pauli_y = cp.asarray([[0,-1j], [1j,0]], dtype=myconverter.dtype)\n", + "pauli_z = cp.asarray([[1,0],[0,-1]], dtype=myconverter.dtype)\n", + "expec_from_rdm = cp.einsum('abcdABCD,aA,bB,cC,dD->', rdm, pauli_y, pauli_x, pauli_x, pauli_y)\n", + "\n", + "print(f\"is expectation value in agreement?\", cp.allclose(expec, expec_from_rdm))" + ] + }, { "cell_type": "markdown", "id": "cc22ffa5-92b8-4978-a6a9-c7e4ec731bd6", @@ -195,7 +289,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 8, "id": "75170ff5-8b17-43c3-9220-610680cd4f39", "metadata": {}, "outputs": [ @@ -209,7 +303,6 @@ } ], "source": [ - "qubits = sorted(circuit.all_qubits()) # ensure we can index the qubits correctly\n", "where = qubits[:2]\n", "fixed = {qubits[3]: '0',\n", " qubits[4]: '0'}\n", @@ -241,7 +334,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.10" + "version": "3.9.12" } }, "nbformat": 4, diff --git a/python/samples/cutensornet/circuit_converter/qiskit_advanced.ipynb b/python/samples/cutensornet/circuit_converter/qiskit_advanced.ipynb index 07cb563..b94378b 100644 --- a/python/samples/cutensornet/circuit_converter/qiskit_advanced.ipynb +++ b/python/samples/cutensornet/circuit_converter/qiskit_advanced.ipynb @@ -57,7 +57,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -86,7 +86,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -174,7 +174,7 @@ }, { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -227,7 +227,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -257,7 +257,7 @@ "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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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", "text/plain": [ "
" ] @@ -349,22 +349,31 @@ " \n", " circuit = create_qaoa_circ(G, theta)\n", " myconverter = CircuitToEinsum(circuit, dtype='complex128', backend=cp)\n", - " Zop = cp.diag([1,-1]).astype('complex128')\n", " \n", " for (i, j), weight in weights.items():\n", - " where = (circuit.qubits[i], circuit.qubits[j])\n", - " \n", - " expression, operands = myconverter.reduced_density_matrix(where, lightcone=True)\n", + " pauli_string = {circuit.qubits[i]: 'Z',\n", + " circuit.qubits[j]: 'Z'}\n", + " expression, operands = myconverter.expectation(pauli_string, lightcone=True)\n", " _, path_info = get_path(expression, operands, options, (i, j))\n", - " rdm = contract(expression, *operands,\n", - " optimize={'path': path_info.path, 'slicing': path_info.slices},\n", - " options=options)\n", + " e += contract(expression, *operands,\n", + " optimize={'path': path_info.path, 'slicing': path_info.slices},\n", + " options=options).real\n", " \n", - " e += cp.einsum('ijIJ,iI,jJ->', rdm, Zop, Zop).real\n", - "\n", + " # the same task can be achieved with reduced density matrix:\n", + " \n", + " # where = (circuit.qubits[i], circuit.qubits[j])\n", + " # expression, operands = myconverter.reduced_density_matrix(where, lightcone=True)\n", + " # _, path_info = get_path(expression, operands, options, (i, j))\n", + " # rdm = contract(expression, *operands, \n", + " # optimize={'path': path_info.path, 'slicing': path_info.slices}, \n", + " # options=options).real\n", + " # Zop = cp.diag([1,-1]).astype('complex128')\n", + " # e+= cp.einsum('ijIJ,Ii,Jj->', rdm, Zop, Zop).real\n", + " \n", " # handle should be explictly destroyed\n", + " \n", " cutn.destroy(handle)\n", - " \n", + " \n", " return e\n", " \n", " return expectation\n", @@ -460,13 +469,13 @@ "name": "stdout", "output_type": "stream", "text": [ - " fun: -5.974514320263179\n", + " fun: -5.974513781450166\n", " maxcv: 0.0\n", " message: 'Optimization terminated successfully.'\n", - " nfev: 99\n", + " nfev: 126\n", " status: 1\n", " success: True\n", - " x: array([2.09900591, 1.27967004, 1.80460603, 1.99555842]) \n", + " x: array([2.09892427, 1.27968803, 1.80457485, 1.99563031]) \n", "\n", " fun: -6.4296875\n", " maxcv: 0.0\n", @@ -590,7 +599,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.10" + "version": "3.9.12" } }, "nbformat": 4, diff --git a/python/samples/cutensornet/circuit_converter/qiskit_basic.ipynb b/python/samples/cutensornet/circuit_converter/qiskit_basic.ipynb index 89e0584..c239095 100644 --- a/python/samples/cutensornet/circuit_converter/qiskit_basic.ipynb +++ b/python/samples/cutensornet/circuit_converter/qiskit_basic.ipynb @@ -26,6 +26,8 @@ "metadata": {}, "outputs": [], "source": [ + "import itertools\n", + "\n", "import cupy as cp\n", "import numpy as np\n", "import qiskit\n", @@ -53,7 +55,7 @@ "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAA4EAAAFeCAYAAAA7XRofAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAAAsTAAALEwEAmpwYAABXbklEQVR4nO3dd3hUZd7G8e/MpNIJEQKhBkKEQEJVQDChqIAFLKiArrLswgKrq8jakFUXjS5iX0VdfcUKCqKyLqBSEkGw0EJRCB1CbwlJSJ95/xgSCKRMIJkzM+f+XNdcMM85M+c3k2n3Oc95HovD4XAgIiIiIiIipmA1ugARERERERFxH4VAERERERERE1EIFBERERERMRGFQBERERERERNRCBQRERERETERhUARERERERETUQgUERERERExEYVAERERERERE1EIFBERERERMRGFQBERERERERNRCBQRERERETERhUARERERERETUQgUERERERExEYVAERERERERE1EIFBERERERMRGFQBERERERERNRCBQRERERETERhUARERERERETUQgUERERERExEYVAERERERERE1EIFBERERERMRGFQBERERERERNRCBQRERERETERhUARERERERETUQgUERERERExEYVAERERERERE/EzugBP99DW30jOyDBk27G1a/NiVHtDti0iIiIi4k30u911CoEVSM7I4IeTJ4wuQ0REREREyqHf7a5Td1ARERERERETUQgUERERERExEYVAERERERERE1EIFBERERERMRGFQBERERERERNRCBQRERERETERhUARERERERETUQgUERERERExEU0WL1IGhwNOHYTsU+DnD/Wbg83f6KpERKpX5lHIOg4WG9RvBv5BRlckniQ/G06mgr0QaoU6L+J9Ms68z616n5uWR4dAu93OSy+9xNtvv82+ffuIioritddeY8yYMcTFxfHOO+8YXeIF7Bs3UTj5HxcuKCyE/HxsL07D2rGD+wuTSjm2E1IS4fSJs222AGjeFSJ6gkXH0EXEx5w6DFuXQPqBs20WGzTpAG3jnJ+BYl4FebAtEQ5sBkfh2fa6TSCqP9RpZFhpUgnpB53v81OHzrZZz7zPI+O1s7tg0iNYOnfCNnK40aVUO48OgaNHj2bevHlMmTKFrl27snLlSoYPH87Ro0eZOHGi0eWVytqx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RColMTGRJ554ovj6U089RWJiYrm3mT59OuvWrWP37t00atSI+Ph4rr322hLrnD59muuvv574+HiGDBlCbm5uqesnJyczbdq0Kn9cIiJmoRAoIiIi1cput/Pjjz/SubNzKo9rrrmGxMREvvvuuxLrLVq0iCuvvJLExESuuOIKFi1aVOr6sbGxrFq1CofD4d4HIiKm9M477xAfH098fDxxcXEEBATw6quvXtCWlZVV4nZFO78OHDhAly5dCAoKoqCg4IL7L235pk2b6NWrF3369GHUqFE4HI4q3QGmECgiIiLVKjk5mTZt2hRfX7ZsGX369OHll18usV7r1q2Lf0SlpaXRoEGDMtePjIxk3bp1bqheRMxuzJgxJCYmkpiYyLBhw3jkkUf429/+dkFbzZo1i29z7s6vkJAQlixZQo8ePUq9/9KWR0VFsXLlSpYvXw7A6tWrq3QHmEKgiIiIVKtt27bRsmVLABo3bkxKSgrLli1j8eLFbNiwoXi9yMhIVq1aRXR0NKtXr6ZXr15lrh8REcGWLVuMeDgiYlK7du3ik08+YcqUKeW2QcmdX0FBQdSvX7/M+y1tub+/f/H/AwMDadasGVB1O8AUAkVERKRSgoKCyM3NLb6ek5ODxWJh6NChFQazwMBAatasiZ+fHzfccAObNm0qXvbBBx9w4403snnzZq6//no+/vjjctcXEXEXh8PB2LFj+fe//01AQECZbUXO3fl1sebPn0+HDh04fPhwcc+IqtoBphAoIiIilVK0J9put2O321m7di0dO3Zk6NChZa6/e/duADIyMorbf/zxR1q3bl183eFwEBISAkBoaCjp6ellrr9z504uv/zyKn5kIiKlmzFjBt27d6dr167ltlWlm266iU2bNtG0aVO++eabKr1vhUARERGplAYNGnDrrbfSp08f+vTpw2233VYc3koTGxtLSkoKAMuXL6dr16706tWL8PBwrrzySg4dOsSzzz7LiBEj+Pzzz4mPj+eTTz5h5MiRpa4PkJKSQqdOndzxcEXE5Hbv3s1HH33Ek08+WW7buc7d+XUxzu1tUadOHYKDg4Gq2wGmyeJFRESk0saNG8e4ceOKr+/Zs4fvvvuOlJQUnnzySQIDA4uXWa1W+vTpw7p16xg8eDCDBw8ucV9hYWFMnjwZgG+//bbEstLWT05OpmfPnlit2pctItVv2rRp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ddQdVERERERExEQUAkVERERERExEIVDkEiUmJvLEE08UX3/qqadITEws9zbTp09n3bp1xddffvllevfuXeq6H374If379yc+Pp79+/eXepvk5GSmTZt2CY9CRESkalTX9+LPP/9Mr1696N27Nw8++CAABQUF3HnnnfTt25eHH34Y0HeiiCsUAkXczG638+OPP9K5c2cAcnNzWb9+fanr7t+/n6SkJJYsWUJiYiLh4eGl3iY2NpZVq1bhcDiqu3wREZEq5er3YosWLVi6dCkrVqzgyJEjbNy4kS+//JLY2FiWLVtGdnY2ycnJ+k4UcYFCoIibJScn06ZNm+Lr7733Hvfcc0+p63777bcUFhbSv39/7rvvPgoLC8u8TWRkZIm9qCIiIt7A1e/FsLAwgoKCAPD398dms7Fz505iYmIA6NSpEytXrgT0nShSEYVAETfbtm0bLVu2BCA/P5/ExET69St9GNrDhw+Tl5fHkiVLqFGjBl9//XWZt4mIiGDLli3VXb6IiEiVqsz3IsCGDRs4evQo7du3JyoqiqSkJACWLVtGWloaoO9EkYooBIpcoqCgIHJzc4uv5+TkYLFYGDp0aIVfQB999BEjRowoc3ndunWJi4sDoF+/fvz+++8V3kZERMRI1fm9eOLECf7617/y3nvvAXDjjTeSnZ1N//79CQwMpFGjRlXzIER8nEKgyCUq6nJit9ux2+2sXbuWjh07MnTo0DLX3717NwBbt25lxowZDBw4kM2bN/P666+XWLdXr15s2LABgPXr19OqVasyb7Nz504uv/zyanucIiIirqiu78WCggLuuusupk+fTlhYGAA2m43XX3+dJUuWYLPZuO666wB9J4pURJPFi1yiBg0acOutt9KnTx8A7rnnHkJCQspcPzY2lqeeegqAf/3rX8XtvXv35r777uPQoUO89957TJ48mU6dOhEcHEx8fDyhoaE8+OCDJfaQFt0GICUlhU6dOlX9AxQREamE6vpejIiI4Ndffy0eBfS5556jefPmjBw5EqvVyh/+8IfiAdT0nShSPoVAkSowbtw4xo0bV3x9z549fPfdd6SkpPDkk08SGBhYvMxqtdKnTx/WrVtXPBIawIoVKwDnie+TJ08ubp8+fXqZ2y26TXJyMj179sRq1cF9ERExXnV9Lw4fPvyCbZ0//YS+E0UqphAoUg1atGjBp59+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fZGf6ZeybSMoBFbgxaj2RpcgIiIuiOp38bddPN35b7c7q6YWERFxv1u6XfxtH/jE+e9911RNLZ5O3UFFRERERERMRCFQRERERETERBQCRURERERETEQhUERERERExEQUAkVERERERExEIVBERERERMREFAJFRERERERMRCFQRERERETERBQCRURERERETEQhUERERERExEQUAkVERERERExEIVBERERERMREFAJFRERERERMxM/oAkRERKTqbV0KGUeM2XbthhDVz5hti4hIxRQCRUREfFDGEUhLNboKERHxROoOKiIiIiIiYiIKgSIiIiIiIiai7qAiIiIiIj7OXgjHdkL6/pLnC2+YD3UaQYMIqH2ZcfWJeykEioiIiIj4qMIC2PMLpK6HvNMXLj+S4rxsXw71wiGiF4S0cHuZ4mYKgSIiIiIiPujUYdj8P8g64dr6afth7RwIj4W28WDzr9byxEAKgSIiIiIiPubkPlg/DwrzK3/b/cmQdRw63wK2gKqvTYyngWFMyl4I25IgP9u19fNOO9e3F1ZvXSIiIiJyaU6fhPVfXlwALJKWCpsWgMNRdXWJ51AINKmURNjzq/OQf0VBMO80rP3cuf725W4pT0REREQugsMOmxdBYV756w2Y5LyU5+h2OLi56moTz+HRIdButzN9+nQiIyMJCgoiNjaWpKQkoqKiGDNmjNHlebWWV0BwPefoUOUFwaIAmHkMaoRAi+5uLVNEqlj6wbP/35YIGUcNK8UjnD5ZcufWib3a6y0i3u3wVucIoFVlW+KlHVH0dDn5sCLl7PWftkNegXH1uItHnxM4evRo5s2bx5QpU+jatSsrV65k+PDhHD16lIkTJxpdnlcLqg1d74A1n50Ngl2GlVzn/ADY9Q4IrGlMvSJyaQpyYcN/4cTus217VjsvDdtC9CBzDQBgt0PKEkhNLtm+9nPnUOmxt5jv8+7GybWK/59fkAuAv19gcdt/n810e00iUnn71lXt/eXnwOEUaBJdtffrCTbsg49Xlgx9s3+Gr9bCPb2hXRPjaqtuHhsCZ82axcyZM0lMTCQuLg6Avn37snbtWubNm0eXLl0MrtD7lRYEiygAivgOhwOSv4aTe0tffuTMHtCYm9xXk9G2J10YAIucOvN5eOVdYPXYb8mqd27Ie3HOnygsLODhO2caV5CIVFp2OqQfqPr7Pfy774XAHYdh5vLSe3/k5sO7SfC3a6F5A/fX5g4e2x00ISGBgQMHFgfAIm3atMHf35+YmBgA8vLymDhxIpGRkXTs2JGrr77aiHK9VlEQLOoaWkQBUMR3pKWWHQCLHEmBTJN0Dc3NqmBPuQOyjsGRbW4rSUSkSpw6VE33e9j3usov2ggOnJfzOXD2GPl+k5uLciOPDIGpqals2rSJYcOGXbBs7969REdHExjo7KLy+OOPk5GRwZYtW9i4cSOfffaZu8v1eucGwSIKgCK+4+BmwOLCer9Veyke4fAW58AJ5bJoMAQR8T5Zx6vnfvOzS59o3lulnYZtFQRbB7ApFU7nuq0st/LIji6pqakAhIWFlWjPzs4mKSmJQYMGAXD69Gnefvtt9u3bh81mA6Bx48YubcNiceEXkcm0CuvAOw9tBCAvP4dRj3YidfRWg6sSkUs1ddR/6X75IGxWW5nrFBYW8O5bnzIt/h43VmaMPw5K4M6+j2CxlLMf1AG/rkymy7BObqurqk3/yzJiW8cbsu2kpES6D+9ryLZFzOxPg5/njr6PlGiraATQspYvnl7yesvmERw6sesSqvMcl7XoxIhnKz550gGEt4gk7fD26i+qijhcPGTrkUcCQ0NDAUhJSSnRPm3aNA4ePEjXrl0B2L59O3Xr1uWll17iiiuuoEePHnz++edur9cX1K0ZyqMjPim+HuAfxOMjPqV2cH0DqxKRqnDq9DFK7/BylsViIT3rmHsKMtiprGPlB0DAbi8kLfNIueuIiHiavIKc6rtvVyeX9gLZGa593zkcdnKyTlRzNcbwyCOBERERxMTEkJCQQEhICOHh4cydO5cFCxYAFIfAgoIC9u/fT+PGjfnll1/YvXs3vXr1IjIyks6dO5e7DVdTshmcPwhMh+th438hki58/9YJugwD/2CjqxSRi3V8N6ybW/46VquNaf+ZyFuNfH/k5ZwMWPEO5eZiq9XGXfdfw8PveO93xerZzvNBjRAXF49jhvc+dyLe6tAW2PRNybbzj+gVKToCWNbyc/kFwbH0g/hSR7pXv4PdR8v+KrAA7cKtZGdUUx9bg3nkkUCr1cqcOXOIjo5m3LhxjBo1itDQUCZMmIDNZiseFKZ58+YA3HOPs/tSy5Ytueqqq/jll18Mq93blDYKaJ1GJQeLcWVCeRHxXCEtoG4Fw1w3aOV875tBUG0IjylnBYvz869RlLsqEhGpGnXCKl7nou63ET4VAAGu61h+ALRY4BofGxH1XB4ZAgHatm3LsmXLyMrKYu/evUydOpWNGzfSvn17goOdh6VCQ0MZOHAg//vf/wA4fvw4v/zyC7GxsUaW7jXKmwbi/FFDFQRFvJfFArE3Q92iU6Yt51xwhsSONxpUnEGi+kFYu3Maznk+atR3zptqpnkTRcQ3BNeF2tWwQ88Xd4pd3hju6gW2M2moKPiBs+2e3hDR0LDyqp3F4UX9Itu1a0ePHj14//33i9v27NnD6NGjOXz4MA6HgwkTJjBu3DgDq/QOrs4DmJPhnEcwOw1qN0RdQ0W8mMPhnCri4G/OyX8Dajjnfaob7nt7eF116jAc2Oj8rPMLgIZREBoBVo/dReo6I7uD1msK3e40ZtsiZrd/I/z+bcXrudod1C8Q+owFW8Cl1+aJMnPgpx2w55jzu7DVZXBFBNQMNLqy6uWR5wSWJjMzk5SUFMaPH1+ivUWLFixevNigqrzXzpWuTQNx/oTyu36CthrwTcQrWSzOo34hLYyuxHPUaWSebrAiYg6NoyF1PWQcrpr7a9PHdwMgQK0gGODD3T7L4jUhsFatWhQWFhpdhs+IjHPOkxVxVcXzABYFwV0/Qes+7qlPRERERCrPaoXogfDLx2Av56ezKwPChLSAcJ1l5ZN8oMOLXAybP7S71vWJ4INqQ7trwOY1uw1EREREzKnWZRAzBCxlTw9boTqNIOYm854u4OsUAkVEREREfExohHMsh6A6lb9to8uhy+3O8wHFN+m4joiIiIiID6rfFHrc6xwLYv8GKMwrf/1aoc5ThRpGuqU8MZBCoIiIiIiIj/ILgLbxENELjqRA+gHnoDH5uc6unkXTSoS2Mvdo0WajECgiIiIi4uP8AqBJB+dFROcEioiIiIiImIhCoIiIiIiIiImoO6iIiIgPqt3QnNsWEZGKWRwOh8PoIkRERERERMQ91B1URERERETERBQCRURERERETEQhUERERERExEQUAkVERERERExEIVBERERERMREFAJFRERERERMRCFQRERERETERBQCRURERERETEQhUERERERExEQUAkVERERERExEIVBERERERMREFAJFRERERERMRCFQRERERETERBQCRURERERETEQhUERERERExEQUAkVERERERExEIVBERERERMREFAJFRERERERM5P8BHfUt9Ejgh78AAAAASUVORK5CYII=\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA4EAAAFeCAYAAAA7XRofAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8qNh9FAAAACXBIWXMAAAsTAAALEwEAmpwYAABXbklEQVR4nO3dd3hUZd7G8e/MpNIJEQKhBkKEQEJVQDChqIAFLKiArrLswgKrq8jakFUXjS5iX0VdfcUKCqKyLqBSEkGw0EJRCB1CbwlJSJ95/xgSCKRMIJkzM+f+XNdcMM85M+c3k2n3Oc95HovD4XAgIiIiIiIipmA1ugARERERERFxH4VAERERERERE1EIFBERERERMRGFQBERERERERNRCBQRERERETERhUARERERERETUQgUERERERExEYVAERERERERE1EIFBERERERMRGFQBERERERERNRCBQRERERETERhUARERERERETUQgUERERERExEYVAERERERERE1EIFBERERERMRGFQBERERERERNRCBQRERERETERhUARERERERETUQgUERERERExEYVAERERERERE1EIFBERERERMRGFQBERERERERNRCBQRERERETERhUARERERERETUQgUERERERExEYVAERERERERE/EzugBP99DW30jOyDBk27G1a/NiVHtDti0iIiIi4k30u911CoEVSM7I4IeTJ4wuQ0REREREyqHf7a5Td1ARERERERETUQgUERERERExEYVAERERERERE1EIFBERERERMRGFQBERERERERNRCBQRERERETERhUARERERERETUQgUERERERExEU0WL1IGhwNOHYTsU+DnD/Wbg83f6KpERKpX5lHIOg4WG9RvBv5BRlckniQ/G06mgr0QaoU6L+J9Ms68z616n5uWR4dAu93OSy+9xNtvv82+ffuIioritddeY8yYMcTFxfHOO+8YXeIF7Bs3UTj5HxcuKCyE/HxsL07D2rGD+wuTSjm2E1IS4fSJs222AGjeFSJ6gkXH0EXEx5w6DFuXQPqBs20WGzTpAG3jnJ+BYl4FebAtEQ5sBkfh2fa6TSCqP9RpZFhpUgnpB53v81OHzrZZz7zPI+O1s7tg0iNYOnfCNnK40aVUO48OgaNHj2bevHlMmTKFrl27snLlSoYPH87Ro0eZOHGi0eWVytqxA9b580q0OfLyKHzoYahXD0t0e4MqE1cd2QYbvgYsJdsL82DXKsg5Be0HgsVS6s1FRLzOqcOwepbz6M65HIWwPxmyjkGXYWD16F8NUl0KC2DdF5C+/8Jl6QdhzWzoNhxqN3R/beK6or+V3V6y3V4IqcmQdQI63+YMheL7PPZ4xqxZs5g5cybz589n0qRJ9O3bl8mTJ9OzZ08KCgro0qWL0SW6rPDFl3Hk5WF7/BEsVo99ygXnB+Hv35254ih9nYObIS3VbSWJiFS7rYvPBMAyPvfS9juPAIk5HdhYegAEwOEMiVuXuLUkuQhbFp8JgGW8z0/ug4O/ubUkMZDHJpKEhAQGDhxIXFxcifY2bdrg7+9PTEwMALt37yYuLo62bdvSsWNHli9fbkS5ZSr8+FMc65Lx++eTWIKDjS5HKnB0u/N8h3JZnHvMRER8QeZR5xGCsn4YFtm3zi3liAdKXc8FvWNKcDh3FGQec1NBUmmnDkPGYcp/n1sgVe9z0/DIjh2pqals2rSJBx988IJle/fuJTo6msDAQADGjh3LHXfcwfjx41m5ciXDhg1j165dBASUf/KCxcW+fLYXnscaG1P5BwHYf1iO/bM52P6VgKVR5TvLJyYmYune46K2LRfn7mue5A/XPlX+Sg74OXEzMTfq3E4R8X5xsbfzxF2fVbjeqcMFWCwmP2HIpL79VwFWF/oIXt9/GD9smOuGiqSy+ncZyaPDPy5/JQcc35+DxeK9By0u5Xf7pfKU3+0ORwV79M7wyCOBqanOvnZhYWEl2rOzs0lKSiruCnrs2DFWrFjB6NGjAejVqxdNmjRh2bJl7i24FPaUFApfeAnbA/djbd/O6HLERQWFeRWu43A4yC/IdUM1IiLVz5XPPYACe341VyKeytW/fb6LryVxv/wCF9/n+huahkceCQwNdY43nJKSwuDBg4vbp02bxsGDB+natSvgPCrYqFGj4qOCAK1atWLPnj0VbsPVlDxg9c/8cPJExSuee9/HjlH45FSst96MtX/fSt32XPHx8Sx2sU6pGqcOwy8flb+OxWLh2tu64HhFfxsR8X552bD8rZIjPl7AAk3bB7v83Sm+JfkrOLqDcrsSWm2QtPprTTXgofKyYPnb4LCXs5IFWnWq49Xv84v53V5VvO13u0eGwIiICGJiYkhISCAkJITw8HDmzp3LggULAIpDoCdy5ORQ+OQ/sbRvh/Weu40uRyqpTiPncNflnR9jsUK4MT0NRESqXEAwNImG/RvKWckBzb1nPDapYs26OM+ZL0/jDpprzpMF1ISwds7B7crkgGad3VaSGMwju4NarVbmzJlDdHQ048aNY9SoUYSGhjJhwgRsNlvxoDDNmzfn8OHD5Oae7Zq3a9cuWrRoYVTpOFb8iGPbdhy//ErBkFvJv+mWEhf7EuO7qkr5Ot4IwXVKX2axOpcHlbFcRMQbRcY7d4BdwHJ2ef1mbixIPEpIc4gsGqevlCEV6oVD23h3ViQXI6o/1GlcyoIzf9O2/Zx/SzEHi8OLjvnefffdJCcns2HD2d2V1157LUOHDi0eGOa2225j9+7dFQ4M4yojDytfXT+Exd2uNGTbZpef4xwBdH+yc15AgCYdnXvCa11mbG0iItWhsAAObnKOApp13Nl2WaTzc08BUABO7HW+Po5uc16vGQrNOjknGtcckt6hMB8ObHKO+Fr0Pm/Y1vk+r9fU0NKqhH63u84jjwSWZfXq1Rd0BX3rrbeYPXs2bdu2ZcyYMcyaNavKAqCYl38QtLoSeo8529b+OgVAEfFdNj9o2gl6jjrbFjtEAVDOCmnufE0U6Xmv8zWjAOg9bP7OLp/nvs9jbvKNACiV4zVv28zMTFJSUhg/fnyJ9oiICH744QeDqhIREREREfEuXhMCa9WqRWFheUOXiYiIiIiISEW8qjuoiIiIiIiIXBqFQBERERERERNRCBQRERERETERhUARERERERETUQgUERERERExEYVAERERERERE1EIFBERERERMRGvmSfQKLG1a5ty2yIiIiIi3kS/212nEFiBF6PaG12CiIiIiIhUQL/bXafuoCIiIiIiIiaiECgiIiIiImIiCoEiIiIiIiImohAoIiIiIiJiIgqBIiIiIiIiJqIQKCIiIiIiYiIKgSIiIiIiIiaiECgiIiIiImIiCoEiIiIiIiImohAoIiIiIiJiIgqBIiIiIiIiJqIQKCIiIiIiYiIKgSIiIiIiIibiZ3QBImK8rUsh44jRVXiO2g0hql/V3V/h12twHEirujv0UZYm9bAN6Wp0GeKj5q2G/SeN2XZ4fbilmzHbFhEpjUKgiJBxBNJSja7CdzkOpOHYqZQtYqT9J2GH3oYiIoC6g4qIiIiIiJiKQqCIiIiIiIiJKASKiIiIiIiYiEKgiLjdXQktWbzmY5fb5eJFvnM/n/y2wuV2ERER8X0KgSIiIiIiIiaiECgiIiIiImIiCoEiIiIiIiImonkCRcTjnMw4zFMf3IyfLYC8/GxGDUqgS2R/o8vySYez0hn29UsEWP3ILshjap876Neig9FliYiISDXy6BBot9t56aWXePvtt9m3bx9RUVG89tprjBkzhri4ON555x2jS/QZ+dlwYBMc2Az5pyGgBjTuAE06gH+Q0dWJr7HZ/Cmw51/QXlCYj5/Nnzo1Q3lp/HJsVhsHj+/kmY/voMvffjWgUu/nb7VRUFhwQXu+vRB/q43Q4Nosu/NJbFYrO9MOM/K/r7Pq7mcMqFRExLvkZMD+ZDi8FQpyIaguhMdA2OVg8ze6OnHV9sOwPAV2HQUL0KYR9G4LrS4zurLq5dEhcPTo0cybN48pU6bQtWtXVq5cyfDhwzl69CgTJ040ujyfkXUC1nwGeVln2/JOw7ZE2LsGut4BNeoZVZ34orD6LTlwbHuJtuzcTE5mHKJxgwhsVltxe2Z2GhGNY9xdos9oUfcytqcdLtGWmZfDoaw0Iuo2xGY9e1ZAeu5pOl7W3N0linik7/8zmlNHdnLLY0uwnHmfOOx25j4bT0iTdvQf/bbBFYqRTqbC+i+g8Jz9mXnZcOogpK6DzsMgINi4+qRiDgfMXwfLfgeLxXkdYO1uWLMbBnaEgT7888NjzwmcNWsWM2fOZP78+UyaNIm+ffsyefJkevbsSUFBAV26dDG6RJ9gL4R1c52hrzS5mc4POYfdvXWJb7u2270s+PkdNu5cTqG9kIzTJ3nz67/RMqwjbZp0BuDgiV088EZvHnv3Oq7qcLPBFXuvu6Ov5r0NS1mRuoVCu52TOZlMXPohHUKb06lRSwB2pR0hftZTXD/3eYZEdjO2YBEPEXf3q2Qc38fahS8Vt63+ZhrZp45w9V0vG1iZGC3vNKyfBxd0sjgTIjKOwKb/ub0sqaSfdjgDIJwNgFD8Z2TRRmcg9FUeeyQwISGBgQMHEhcXV6K9TZs2+Pv7ExPjjOb/+Mc/mD17Ntu3b+fzzz/ntttuM6Jcr3V0O+ScKmcFB5w+Ccd2wWWt3VaW+Lj+XUaSm3+a17+cwOG0PQQH1CImIo6pf/wvNpvzY6lxSCtembCCg8d38ve3+9Gj/Q0GV+2dRrTvTXZBHvcvfp+9p45RKyCIPk3b8eXNk/A7c8S1Vb2GJA5/ip1ph7nu82e5vrV2sokEBNVi4PhPmPdcf5p3vBYcdn79+hlufSIJ/8AaRpcnBjqwEQrzyl/nxG7IPAa1Qt1SklSSwwFLf3N2/3SUsY4FZ0js0tJ9dbmTR4bA1NRUNm3axIMPPnjBsr179xIdHU1gYCAAAwcO5N577+WPf/xjpbZhsViqpFZv98jwj+jb6U5s1rJfCoX2Qp5/9H1emvtnN1bmOb5/wfnx4Muvmel/WUZs63i3bnPwlX9m8JWlv6byCnIJ8HO+x2sE1SE4oJY7SyMpKZHuw/tW2f19f8cTxDVrX2X3V1mjY/oxOqZfqctyC/IJ9HOevFInIJiaBp4EnJiUyDXjBxi2fTnLFz/3bp28jKbt4it1m7A2V9L1hkf49s2RgIPuQ56gUauuld52UlIi919bdZ8pRvPF10dlvPbXVUQ1vwKrpfwOdX++/TFmL3veTVVVnpn/jvXCIrlnekq56ziAfSegVv0mZKUddE9hVcDhKCvWluSxIRAgLCysRHt2djZJSUkMGjSouK1Xr15urc3XBAXUrHglh4PAAO31FPdJ2fcr/7fwcaxWGwWF+Ywb8qrRJfms1Yd2MGXF59gsVvLtBbzU7w9GlyTiUboPeZyda+djtdrodsPDRpcjHiA4sHaFAdCh304ezT/Qhd+/Z/j56JF/jwyBoaHOY+cpKSkMHjy4uH3atGkcPHiQrl0rvxfufK6mZF+3LQn2VDDoos3mx+hxI0j4ZIR7ivIwi6c7//Xl18zq2ZCWanQVZ3Vo1ZuXxv9g2Pbj4uJxzKi6v3fBjCU4dh6psvurSlc1vZyld/7D6DIAiI+LxzF7qtFlCL75uff697DjIt6GVquNBk2jsVr9igeIqay4uHjmPuM7z6Uvvj4qI/lr5+k0ZfYjxHl0LWH6FGYumuK2uirLzH/HrFyY8gXYK3jofjY4nLqdQI9MTJfGIx9SREQEMTExJCQkEBISQnh4OHPnzmXBggUAVRICxalJx4pDYNF6IiIiImYXHgNHt5W/ji0AGkW5px6pvJqBENsc1u8p/5zA7q3wyQAIHjo6qNVqZc6cOURHRzNu3DhGjRpFaGgoEyZMwGazFQ8KI5euZgg0qyBTt7gCguu6px4RERERT9agJYRWMFhe23jNFejpBsdAkL8z7J3PYnEGxWs7uL0st/HYbNu2bVuWLVtWou3uu++mffv2BAdr4pWq1DYe/AKcRwTt5wx3bPOHllc6LyIiIiLiDAgxN8LWZc6RQs+dRss/GCKvVg8qb3BZHbj/Wvh4Jew/WXJZ8xC4qxfUd/3UQa/jkUcCy7J69eoLuoJOmTKFpk2bsmrVKsaOHUvTpk3ZsWOHQRV6J4sFWl8FV48729bheugzDlr1cC4X8ziWfoBxr3Rh8GNBFF4wCRI8+/GdPDQjnvtf78nYlzoBsH3/ev7276t48M0+bNy5vNz7dzgcjH2pEwt+fveCZZ8nvsADb/TmuU9HUlCYz/b963loRjwPzYjn7oRWzFv+Srn3vXzjPEY80+yC9tlLn2fijDgmvNqdFRu/BODNrx8ovu+b/1G/3PutLq+uXkD8rKcuaJ+79Sd6ffwEV308hfnbVwNwIjuT4fNf5drPnuG5n74q8z6X7d1Mn0/+wTWfPUNqxvESy9Yd3k3nmY8Q+c79xW3puacZOu8FBsyeymtrFgJwIPMkV3z4OLVfvocCe2GFj2Pi0g+4539vlLrsYOZJ6rxyD9tPHiq3TcSTXTt2JgP+fOFnlpiX1Q/aXQN9xp5tixkCff6iAOhNGteDSYPgwevOtk0aBA8OdIZEX+axRwLPl5mZSUpKCuPHjy/RPnXqVKZO1UACVeHMiPwAhLUzrg4xVp0aIUwbs4SnPih9gvbJd80GYMXGL9m2fw0AH3z3D5646zNq1wjh6Q9u4bmIRWXe/6rf/ku9Wpdd0H4y8wjrdyzjlQkrmL3sX/y46SviYofx4rhEAP7x/hCubFf+XIHLN8zlsnoXhsDb4h7izn6Pkp2bycPvDKB3x5sZP+QVALbvX8fcpBfLvd/qkFuQT/KRPaUue23NQhbfMQULFm744nluatONZ1Z9wZNX3cblDcLLvd+EVV+yYNhj/H58P9N+ns9rA0YVL2tTvxErRjzN9V+cHbL83eQl3NnuKu5s14vbv36ZEe2uIiSoJt/e/jjDvqp4QuzDWensSj9KnYDSe2i8tmYRV4S1qbBNRMQbnTvIesNI4+qQi2exQItz5nNsGmJcLe7kNUcCa9WqRWFhIffdd5/RpYj4tAD/IGrXqPjI2I+bvqR3h1sAyMw+yWX1mhIUUIOc/Cxy87PLvN2ydZ8S3+nOC9pT9q0mNiIegC6RA/h9z6riZdl5WZzMOER4aNnB4effF9AlcgCWUobt9jtzYkZufjYtw0p28F+x6Uuu6nhL2Q+0mry/MZG7O1xd6rKIeo3Iys8lMz+H2mfC1eZjqfzr56+55rNn+OlA6XMbnc7PJdjPn9oBwVzRuA2/HSs55GvtgGBqBpScB3BX+hE6XtYcgHYNwll9aCdBfgHUD3JtbsbX1ixkQufrSl127PQpMvJO06LuZeW2iYiIiHt5TQgUEc9RUJjPrkMbiWzaBYC6NS9j16FNpGUeZfehTWRmp5V6u9VbvyMmIg6rxXbBsqycNGoEOfte1AyqS2bO2fv4dctCukUNLLem79d8QP8ud5W5/LV54xn7Ugyd25ScNH311kV0r+C+q1p+YQFJ+36jb/PoUpcPadONKz58nO4fPsaELs6AtepACg9fOYSPb7iPR5M+LfV2abmni0MjQOG5J6qUoW1IE5bv+51Cu50VqVtIyz3t8uM4kZ3JsexTtKkfVury19YuYtx5AbG0NhEREXEvhUARqbTkHYnEto4vvv6nwc/z1vyJvPrFX2jVOIa6NUNLvd3CX97luu6jSl1WM6gup3NOAXA65xS1guoVL/tx05f0Ludo3brtS2nfoif+fgFlrnP/LW/yf3/fwqdLni1uSz26jdA64QS5eULfT35bwZ3tepW5/NlVX5I8ahobRr3As6vmARBZvzHtGoTTqGbdMicprhsQTEbe2aOwtgomMwYY3bEvqw6kcOMX/6Jxrfo0qun6UMCvr13EXzpdW+qytJwsUk8dJzq0abltIiIi4n4KgSJSaT9u+pKrOpw9Z7DpZW3515jveOC2t2lYrzl+Nn9O52SQdSbUFUk9msKTM4cy94cX+XL5K+w9sqV4Wdtm3dmwMwmAtdsW065FD8B51HHvkd9p3SS2eN1j6ftL3O/uQ5tYtXk+j/1nIHsOb+b9RU+UWJ5XkAtAgH8wNQLPnul9/uNwl60nDvJO8mJumPs8vx1L5Y2135ZYHmjzo4Z/IDX9g8g7MzhPZP0wDmaeJCsvp3iwliNZ6eSfM3hPzYAgsgvyyczL4deD22lXwfmDRbf54PoJ/PfWR7A7HPRoXPpJLQX2Qg5npZdo251+hCnLZzN64QwS925mzpafipelnDzItpMHuWHu8yzZs5G/fv9eqW0iIiLifl4zMIyIuEdBYT6PvzuInQeTefTd6/jjoAQa1WvBwl/fY2T/yTgcDn7bs4q/Dv138W0W/vIeS9Z+TIB/MPfd7BwlMjH5MwL9g+nfZWTxem9PXA/At7/OpNBeQPOGl/PtrzNp1bgjbZt2pWPE1TzwRm8a1mvOLX0eAJxH+Tq1LtmF87lPRxYPGANwc+/7ubm3c8TLB97ozaiBzwDw76/u469DX+fNr//GviNbKCjMY1j834tv9/Pv3/D0vV9X2XPnqufihhf/P37WU0zoch2HstJ4f2Mij/UYythOA4j79CkA/hTTH4B/9LqNu7/5N9kFeTzRy3lU9O+JH5Nw9XDCa589i/3RHkMYNCeBIL8A3hv0FwAeWvoh0+Lv4kDmSf686C02H0tl4OfP8tZ1YziRncEjSZ9iAR664kaC/QPILyzgxi/+xYaje7h+7vNM7XMHIUG1mP7Lf3nruj8Xb+v9wc6BunanH+XJFZ8z7PIerD+ym3WHdzGqY1+Wj/wnAKMXvsVjPYbSpn7YBW0iIiLifhaHw+EwugjxHIunO/8dMMnYOjyJGZ6T1bMhLbXi9SrjvQWPcWe/x6gZVLVjLKdnHeOrFa9zz3VPV+n9nqteU+h24dg1F61gxhIcO49U3R2ecd/3/8fr1/yxyu+3NPNSfqF+UM0yz2OsCpaIhviN619t9y+u88XPvde/hx1V/zZ0SeuGcN81xmy7Ovji6+NSeOvz4a11V5cHPnH++8rI8tfzFToSKCLVYvTg56rlfuvWDK3WAOhN3BUAAW5pe4XbtiUiIiLVS+cEioiIiIiImIiOBIoItRsaXYFTUZfUegYPHlnVz4elSb2qvUMfpedJqlN4xdOflqmoG2nri/xsuJRti4hUB4VAESGqX8XruEPR+QlVeT6eJ7AN6Wp0CSKmd0u3i79t0blCvnRen4iYm7qDioiIiIiImIhCoIiIiIiIiIkoBIqIiIiIiJiIQqCIiIiIiIiJKASKiIiIiIiYiEKgiIiIiIiIiSgEioiIiIiImIhCoIiIiIiIiIkoBIqIiIiIiJiIQqCIiIiIiIiJKASKiIiIiIiYiEKgiIiIiIiIiSgEioiIiIiImIif0QWIiIhUhXmrYf9J9283vD7c0s392xURkZKq4nvg9e8v7nbe9l2gECgiIj5h/0nYccToKkRExChV8T1glu8RdQcVERERERExEYVAERERERERE1EIFBERERERMRGFQBERERERERNRCBQRERERETERhUARERERERETUQg0KYcDDm0Bu9219e2FzvVFxHs4HHDo90q8zwvgsN7nAJw6upvv3r7X6DJERESqheYJNKldP8HOH6FhW+hwA1jL2R1gL4SN/4Wj2yHnFLS8wn11isjF2/mj870ethPaD6rgfV4AG+bDsZ2QkwktvGjCWxEREakcjz4SaLfbmT59OpGRkQQFBREbG0tSUhJRUVGMGTPG6PK8WoOWYAuAIymw6ZuyjxScGwD9giCkhVvLFJFL0KAV2PydRwN/W1jO+/ycAOgfBCHN3Vunp0n84D4W/vtO9mxYxNxn4jm8a43RJYmB9h4/+/99x8teT8RbOBxwfPfZ69lpRlUiRvLoI4GjR49m3rx5TJkyha5du7Jy5UqGDx/O0aNHmThxotHlebW6jaHLMFg752wQ7HBDyXXOD4BdhkGdRsbUKyKVVy8cOt8G6+Y6gyBceETw/ADY5Xao3dCYet0h/chOVsx+hANbl5Ofk0lgzfo0atWNQfd9hs0vAID4e17n1NHd/DTvKa4dO9PYgsUwh9Lhk5Ww78TZthcXQbMQuKsXNKprXG0iF+vYLtjyvbNnV5Ef34XL2kD768A/2LjajPD9f0Zz6shObnlsCZYzX44Ou525z8YT0qQd/Ue/bXCF1cdjjwTOmjWLmTNnMn/+fCZNmkTfvn2ZPHkyPXv2pKCggC5duhhdotcrCoLnHhEsogAo4huKgmBpRwTNFgABvn5hMDXrNeYPL2xl3LsZ3P7kKprHXIfD4TC6NPEgxzLgte8g9cSFy1JPwqvfwfFM99clcimO74b18yAn48JlR7fDms+hMM/tZRkq7u5XyTi+j7ULXypuW/3NNLJPHeHqu142sLLq57EhMCEhgYEDBxIXF1eivU2bNvj7+xMTE8PJkye54YYbaNu2LbGxsVx77bVs377doIq90/lBsIgCoIjvKC0IFuSZLwBmZxzn5MGtdOz/FwJr1MVisVC7QVNi+v8FP/9Ao8sTD7JoI2TnQWm7BhwO57JvN7q9LJGL5nDA1qVFV0pfJ/Mo7N/ktpI8QkBQLQaO/4Sf5z3F0b0bOLpnPb9+/QzXjf8E/8AaRpdXrTwyBKamprJp0yaGDRt2wbK9e/cSHR1NYGAgFouFBx54gJSUFJKTk7nhhhsYNWqUARV7t3ODYBEFQBHfcn4QXPV/5gqAAMG1G9CgaTRL3v0Tvy//kOP7f9MRQLnA6TxYt7vM38mAc9ma3ZCT756aRC5V+kE4fYLyX9jA/mS3lONRwtpcSdcbHuHbN0fy7Yy76D7kCRq16mp0WdXOY0MgQFhYWIn27OxskpKSiruC1qtXjwEDBhQv79WrF7t27XJpGxaLRZdzLvWaWLjv5auKn5/8glzG/qs7dcOMr83oi14zeq595VK/qYUHXnX2rsjNdL7P//RcF+o0Mr62qrgkJSVW+Nl/6+REwi+PZ92iV/j08U78Z0Ijfv5y6gVh0OYfRP3Gl7v0fZKUlGj4Y6/qi5nfi81bd6DQhX0DhXZo0rKd4fXq9WH8xRuej2E3/cGlz7OTh3MMr/VSLq58D5Sm+5DHsfkH4R9Yi243PHxR9+Ep3wWu8sgQGBoaCkBKSkqJ9mnTpnHw4EG6di09nb/yyisMHTq0usvzSX42f26Lm1R83d8vkNvj/47VajOwKhGpSv62AG6LL/k+vy3uIVO9z4Nrh3LVHQmMeHYtf3knjd53TuOXr/7Jbz+8X2K9mvXC6H7TowZVKUbKz81yed2C3NPVWIlI1cnJc+11nZefXc2VeCar1UaDptE0aNqheIAYX+eRo4NGREQQExNDQkICISEhhIeHM3fuXBYsWABQagh8+umn2b59O0uXLr1gWWnUBeiscweBKWILgLjY2xk27PYK5xH0dYunO//Va6b66bmuPucOAlPE5g/9u4xk5MiRFc4j6A1e/x52HHF9ff/AGrS/+l6Sv3udo3vWX/R24+LimfuMb71mzfxedDjgX/+Dw+ll95yzAI3rQfrRPVRix7vPMPProzTe8Hzk58DyGc7ffOVpe0V9j34cFans90BV8rbvAo/8yrdarcyZM4fo6GjGjRvHqFGjCA0NZcKECdhsNmJiYkqs/8wzz/DNN9+waNEiatTw7ZM4q9r5o4AWOX/U0LLmFxMRz3f+KKBFyho11FflZJ3kx88e49i+TRQW5GMvLGDbL19wPHUT4VF9jC5PPITFAv3aV3xOYL/2mDIAinfyD4Lw2ApWskCzzm4pRzyARx4JBGjbti3Lli0r0Xb33XfTvn17goPPTmLy9NNPs2DBAr7//nvq1avn5iq9W2nTQPzykXNZWfMIevuRAhGzKW0aiJ8/dC5zZR5BX2KzBXD61BH+9+otZKUdxGr1o85lLYm7+zUir7xwIDIxr+6t4MgpWLzZedSvKBAW/f+aaOjWyrj6RC5G5NXOieGP7aTkCxuwWCF6sDkGCRMnjw2BpVm9ejU9evQovr5582aeeuopWrduTXx8fHH7+vXr3V+cl3FlHkAFQRHv5so8gGYKgv5BNbnmz+8ZXYZ4AYsFbugE7ZrAihTYeaZ7WURD6NPW+a+It7H6QexQ52+/fclwco+zvVkXaNoJaoYYWZ3xrh070+gS3MprQmBmZiYpKSmMHz++uC06Otqr+y0baVuia9NAnB8Ed/wAkfHurFRELtbWZa5NA3F+EAyqA23UO1KE1g2dFxFfYbFCw7bOS9G5jFH9jK1JjOE1IbBWrVoUFlZwNqu4rEV355wxl19T8TyARUFw6xJo3s099YnIpWt5BWQcgnbXVdzFpygIpiQ69wqLiIiI7/KaEChVK6gOdB/p+kntdRtXbn0RMV5wXeh+l+vv23rh0H2E3uciIiK+zgfP+hBXVfaHnn4Yingfvc9FRETkfAqBIiIiIiIiJqIQKCIiIiIiYiIKgSIiIiIiIiaiECgiIiIiImIiCoEiIiIiIiImoikiRETEJ4TXN9d2RUSkJCM/j73tu0AhUEREfMIt3YyuQEREjKTvAdepO6iIiIiIiIiJKASKiIiIiIiYiEKgiIiIiIiIiSgEioiIiIiImIhCoIiIiIiIiIkoBIqIiIiIiJiIQqCIiIiIiIiJKASKiIiIiIiYiEKgiIiIiIiIiSgEioiIiIiImIhCoIiIiIiIiIkoBIqIiIiIiJiIQqCIiIiIiIiJ+BldgIiIyMUo/HoNjgNpRpdxAUuTetiGdDW6DBGppK1LIePIxd9+9eyLu13thhDV7+K3K3IxFAJFRMQrOQ6k4dh5Cb/YRETOkXEE0lIv/vaXclsRd1N3UBERERERERNRCBQRERERETERhUARERERERETUQgUERERERExEYVAERExjch37ueT31a43C4iIuKLFAJFRERERERMRCFQREREBHA4oNDu+voFhc7biIh4G4VAERERMT2HA75aC+8lOcNdRfIL4d0kmL9OQVBEvI8mi69GRV8KFouxdYiIiGt2pB3mz4vexuFw4MDBi33/QNewCKPLEjdIz4bVuyArF/7vB/jj1eBnK33d/EJnWNxyEPadgPjLoW4N99YrIlXLbL/bPfpIoN1uZ/r06URGRhIUFERsbCxJSUlERUUxZswYo8srlcMBR7bBms9g6cuw9CX49VM49Lv2FIqUJjcTti+HH94827Z1CZw+aVxN4rv8rTYKCgsuaM+3F+JvtVEvsAZzhjzIsuFP8uY1f2LSso8MqFKMUK8GTOgPNQPhtwPOIFjaEcFzA2DNQPjrAAVAs3poRjyfLH7G5XbxPA6H8zf6r5+e+d3+Mqz+DA6n+P7vdo8+Ejh69GjmzZvHlClT6Nq1KytXrmT48OEcPXqUiRMnGl3eBRwO2LoYUpMBC3DmxZN+ENIPwNEd0GEwWDw6eou4T9Zx5w6TvNMl2/etg/2boPMtUL+ZMbWJb2pR9zK2px0u0ZaZl8OhrDQi6jakQXDt4vZAP39s+sA2lSb1nUHwjSVng+C5RwRLC4CN6xlasohcJIcdNi90hsBzf7enpULaPgiPhcsH+O6RQY/9dps1axYzZ85k/vz5TJo0ib59+zJ58mR69uxJQUEBXbp0MbrECxzYdCYAQvEL6dz/H94Ce9e4uyoRz+Sww/ovIS+79OX2Akj+Egpy3VuX+La7o6/mvQ1LWZG6hUK7nZM5mUxc+iEdQpvTqVHL4vUK7XYmLvmAv195k3HFiiGKguD5RwQVAEV8y961ZwIglPq7fX8yHNjo7qrcx2OPBCYkJDBw4EDi4uJKtLdp0wZ/f39iYmIAGDp0KDt37sRms+Hv709CQgIDBgxwe70OB+xdXfF6e9dA8646GihyfBdkp5WzggMK8uDgb9Css7uqEl83on1vsgvyuH/x++w9dYxaAUH0adqOL2+ehJ/VebjH4XDw50VvM7h1Z65rFWtwxWKE848IvpsEdgekHFIAFPEFDrsLB2YssGc1NOnom0cDPTIEpqamsmnTJh588MELlu3du5fo6GgCAwMBmDlzJvXq1QNg3bp1xMfHc+LECWy2Ms7mPsNSxX/NBnUaM3vKgQrXy82EiCYd2H14c5Vuv6p8/4Jz90dVPz/eTM9J9Zgw5DVu6jUeq7Xs92qhvZCZry/gH+/raExV8pXX9Pd3PEFcs/aVvt3omH6MjulX5vIHlsykdf1GjO10zUXVlZiUyDXj3b8zsir5ymvkUoU268gtjy9lC6EAnD51lI8T+pKQ6pnf4e7iq6+P6X9ZRmzr+Erd5tMlzzInaXqJtuy8TLpEuv4ZkJSUSPfhfSu13argq39HVzRveDnv/f338ldywOkT0Kh+M46mp7qnsCrgcPFkRo8NgQBhYWEl2rOzs0lKSmLQoEHFbUUBECA9PR2LxeLyg69KfrYA19f1c31dEV/l5xdQ4XvVggX/Sry3RC5V0t7feHfDUno2acuyPZupH1yLOUMu3CEp5nDyUAonDmwhPKq38/qB30k7tM3gqsSTjOg/mZEDnijR9tCMeGOKEZfpd7uHhsDQUOcet5SUFAYPHlzcPm3aNA4ePEjXrl1LrD9hwgQWLlxIeno6X3zxBX5+FT+sqg6K9gJIehMK88pfz2KD37avxT+oSjdfZRaf2ZllRJD2VHpOqsfetZCytPx1rFYrt4y4jkf/o+e+KvnKa7pgxhIcO49U6X3GNW9P1sRLGxE0Pi4ex+ypVVSRMXzlNXIpzj0HsEj45Vfz5ne55U4fYQa++vpYPds5KIi7xcXF45jh/ufSV/+OrijIdY5Kbq9gTlBbAOw7tAObRyamS+ORDykiIoKYmBgSEhIICQkhPDycuXPnsmDBAoALQuAbb7wBQFJSEg8++CA//PADtWrVcmvNVj8I71hB/2ILhLXDYwOgiDs1bg/bkyr+AA7XKVki4mbnDwKTdWaAqnMHizF7EBTxZn6Bzt/kBzZTclCY8zTpiE8GQPDQ0UGtVitz5swhOjqacePGMWrUKEJDQ5kwYQI2m614UJjzxcXFYbVa+fHHH91csVPLKyG4Ls5hZs9ngYAa0Poqd1cl4pn8g6Bt2adlAdDyCqgZ4p56RESg9FFAi7gyj6CIeIeIqyCwJmX+bg+qC62udHdV7uOx2bZt27YsW7asRNvdd99N+/btCQ4OBiAzM5Pjx4/TokULwDkwzI4dO2jXrp3b6wVnyOs2ArYshqPbKbFnoUEraDcAgmqXeXMR02kaC34Bzsnic06dbfcPhlY9oJnnzQQjIj6somkgKppHUMzlxXGJlWoXzxJUG7qf+d1+bOc5CyxwWRvnHIEBNQwrr9p5bAgszerVq+nRo0fx9aysLO644w4yMzPx8/MjKCiIjz/+mObNmxtWY2BNiB0CORmw4m1n21V/PnOEUEQuENYOGl3uPA8jN8t5hLB+Myhn0FCREnanH6X3J1O4PCScAJsfC4Y9VmJ5TkEe9y+eye70I7QPbcor/e8t9X4mLv2A5CN7yCnI54W+d9ErPKrE8o82/8DHm5dTaLfzwfUTqB9Uk+HzXyUrP5c6gTWYdeP9BPr5V9fDlGrm6jyACoIiviOoDnS6BbLT4cf/ONt6jzHHQRuvCYGZmZmkpKQwfvz44rZGjRrx008/GVhV2c598SgAipTPYnEGP5GL1b9FRz64fkKpy/699lvubNeLfi06lHsf/4obib/Njz3pR7l/8ft8fevDxcv2Z5xg+b4tfHv75OK2L1N+oXvjNjzR6xae++krvt2dzE1tulXNAxK3y82HtNOuzQN4bhA8mQW5BQqBIt7s3N/qZgiA4EUhsFatWhQWqvO9iIhcKGnfb/Sd9TRDI7vzt26DSyz7Yd9vHMg8QcKqL/lbt8Hc2KZrqffhf+bs/8z8XDo2LNmj5PvdGyh02Lnu82dp1yCcF/v+gYh6jfjl4A4A0nOyaBDk3gHJpGrVCoIJAyAzx7WJ4JvUd4bF2kHO4Cgi4k08cmAYERERVzWuWY/Nf3yR7+94gqV7NrHh6N4Sy3ekHWFQRGe+vuXvJKz6koJyhqS97auXuH7uc/Rv0bFE++HT6eQVFvDt7ZMJ9gtk/vbVRNYP4+eD24h9/++sObyLnuFtq+XxifvUDnItABZpXM8ZHkVEvI1CoIiIeLVAP39qBgThZ7UxuHVnNh/bV2J53cBgrm7ajpoBQbSu34jDWell3tfcoRNZMfKfTFn+Wcn7CKjB1c2cg471bd6eLScO8NHmH7g+ojPJo15gUEQnPvltRdU/OBERkWqgECgiIl4tIy+7+P8r96fQul6jEst7NmnLxqN7KbTb2ZN+lMtq1OFEdibZ+Xkl1sstyAegln8QNf1L9u/rER7JxjNHGJOP7KFl3ctwOCAk2NkFNDS4NqdysxEREfEGXnNOoIiISGlWpG7hqR/nEGjz56rwy7micRsAHlgyk1f638ukK25k9MK3OJWXzeiYvgTY/HhlzQIGtootMQLoiG9eIz3nNIUOO8/0uQOAaT/PZ2T73nRq2JJgvwAGzJ5Kg+Da/K3bYE7n5zLym9f4ZPMK/G02PrnhfkMev4iISGUpBIqIiFcbFNGZQRGdL2gvmgqica36F0wbcSr3ND2aRJZo+2LoQxfcx8NX3lT8/3/FjyyxLMDmx/9ue+z8m4iIiHg8hUARETGdsuYKFBERMQOdEygiIiIiImIiOhIoIiJeydKkntEllMpT6xKR8tVuaK7tirkpBIqIiFeyDSl90ncRkYsR1c/oCkTcR91BRURERERETEQhUERERColMTGRJ554ovj6U089RWJiYrm3mT59OuvWrWP37t00atSI+Ph4rr322hLrnD59muuvv574+HiGDBlCbm5uqesnJyczbdq0Kn9cIiJmoRAoIiIi1cput/Pjjz/SubNzKo9rrrmGxMREvvvuuxLrLVq0iCuvvJLExESuuOIKFi1aVOr6sbGxrFq1CofD4d4HIiKm9M477xAfH098fDxxcXEEBATw6quvXtCWlZVV4nZFO78OHDhAly5dCAoKoqCg4IL7L235pk2b6NWrF3369GHUqFE4HI4q3QGmECgiIiLVKjk5mTZt2hRfX7ZsGX369OHll18usV7r1q2Lf0SlpaXRoEGDMtePjIxk3bp1bqheRMxuzJgxJCYmkpiYyLBhw3jkkUf429/+dkFbzZo1i29z7s6vkJAQlixZQo8ePUq9/9KWR0VFsXLlSpYvXw7A6tWrq3QHmEKgiIiIVKtt27bRsmVLABo3bkxKSgrLli1j8eLFbNiwoXi9yMhIVq1aRXR0NKtXr6ZXr15lrh8REcGWLVuMeDgiYlK7du3ik08+YcqUKeW2QcmdX0FBQdSvX7/M+y1tub+/f/H/AwMDadasGVB1O8AUAkVERKRSgoKCyM3NLb6ek5ODxWJh6NChFQazwMBAatasiZ+fHzfccAObNm0qXvbBBx9w4403snnzZq6//no+/vjjctcXEXEXh8PB2LFj+fe//01AQECZbUXO3fl1sebPn0+HDh04fPhwcc+IqtoBphAoIiIilVK0J9put2O321m7di0dO3Zk6NChZa6/e/duADIyMorbf/zxR1q3bl183eFwEBISAkBoaCjp6ellrr9z504uv/zyKn5kIiKlmzFjBt27d6dr167ltlWlm266iU2bNtG0aVO++eabKr1vhUARERGplAYNGnDrrbfSp08f+vTpw2233VYc3koTGxtLSkoKAMuXL6dr16706tWL8PBwrrzySg4dOsSzzz7LiBEj+Pzzz4mPj+eTTz5h5MiRpa4PkJKSQqdOndzxcEXE5Hbv3s1HH33Ek08+WW7buc7d+XUxzu1tUadOHYKDg4Gq2wGmyeJFRESk0saNG8e4ceOKr+/Zs4fvvvuOlJQUnnzySQIDA4uXWa1W+vTpw7p16xg8eDCDBw8ucV9hYWFMnjwZgG+//bbEstLWT05OpmfPnlit2pctItVv2rRp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ddQdVERERERExEQUAkVERERERExEIVDkEiUmJvLEE08UX3/qqadITEws9zbTp09n3bp1xddffvllevfuXeq6H374If379yc+Pp79+/eXepvk5GSmTZt2CY9CRESkalTX9+LPP/9Mr1696N27Nw8++CAABQUF3HnnnfTt25eHH34Y0HeiiCsUAkXczG638+OPP9K5c2cAcnNzWb9+fanr7t+/n6SkJJYsWUJiYiLh4eGl3iY2NpZVq1bhcDiqu3wREZEq5er3YosWLVi6dCkrVqzgyJEjbNy4kS+//JLY2FiWLVtGdnY2ycnJ+k4UcYFCoIibJScn06ZNm+Lr7733Hvfcc0+p63777bcUFhbSv39/7rvvPgoLC8u8TWRkZIm9qCIiIt7A1e/FsLAwgoKCAPD398dms7Fz505iYmIA6NSpEytXrgT0nShSEYVAETfbtm0bLVu2BCA/P5/ExET69St9GNrDhw+Tl5fHkiVLqFGjBl9//XWZt4mIiGDLli3VXb6IiEiVqsz3IsCGDRs4evQo7du3JyoqiqSkJACWLVtGWloaoO9EkYooBIpcoqCgIHJzc4uv5+TkYLFYGDp0aIVfQB999BEjRowoc3ndunWJi4sDoF+/fvz+++8V3kZERMRI1fm9eOLECf7617/y3nvvAXDjjTeSnZ1N//79CQwMpFGjRlXzIER8nEKgyCUq6nJit9ux2+2sXbuWjh07MnTo0DLX3717NwBbt25lxowZDBw4kM2bN/P666+XWLdXr15s2LABgPXr19OqVasyb7Nz504uv/zyanucIiIirqiu78WCggLuuusupk+fTlhYGAA2m43XX3+dJUuWYLPZuO666wB9J4pURJPFi1yiBg0acOutt9KnTx8A7rnnHkJCQspcPzY2lqeeegqAf/3rX8XtvXv35r777uPQoUO89957TJ48mU6dOhEcHEx8fDyhoaE8+OCDJfaQFt0GICUlhU6dOlX9AxQREamE6vpejIiI4Ndffy0eBfS5556jefPmjBw5EqvVyh/+8IfiAdT0nShSPoVAkSowbtw4xo0bV3x9z549fPfdd6SkpPDkk08SGBhYvMxqtdKnTx/WrVtXPBIawIoVKwDnie+TJ08ubp8+fXqZ2y26TXJyMj179sRq1cF9ERExXnV9Lw4fPvyCbZ0//YS+E0UqphAoUg1atGjBp59+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fZGf6ZeybSMoBFbgxaj2RpcgIiIuiOp38bddPN35b7c7q6YWERFxv1u6XfxtH/jE+e9911RNLZ5O3UFFRERERERMRCFQRERERETERBQCRURERERETEQhUERERERExEQUAkVERERERExEIVBERERERMREFAJFRERERERMRCFQRERERETERBQCRURERERETEQhUERERERExEQUAkVERERERExEIVBERERERMREFAJFRERERERMxM/oAkRERKTqbV0KGUeM2XbthhDVz5hti4hIxRQCRUREfFDGEUhLNboKERHxROoOKiIiIiIiYiIKgSIiIiIiIiai7qAiIiIiIj7OXgjHdkL6/pLnC2+YD3UaQYMIqH2ZcfWJeykEioiIiIj4qMIC2PMLpK6HvNMXLj+S4rxsXw71wiGiF4S0cHuZ4mYKgSIiIiIiPujUYdj8P8g64dr6afth7RwIj4W28WDzr9byxEAKgSIiIiIiPubkPlg/DwrzK3/b/cmQdRw63wK2gKqvTYyngWFMyl4I25IgP9u19fNOO9e3F1ZvXSIiIiJyaU6fhPVfXlwALJKWCpsWgMNRdXWJ51AINKmURNjzq/OQf0VBMO80rP3cuf725W4pT0REREQugsMOmxdBYV756w2Y5LyU5+h2OLi56moTz+HRIdButzN9+nQiIyMJCgoiNjaWpKQkoqKiGDNmjNHlebWWV0BwPefoUOUFwaIAmHkMaoRAi+5uLVNEqlj6wbP/35YIGUcNK8UjnD5ZcufWib3a6y0i3u3wVucIoFVlW+KlHVH0dDn5sCLl7PWftkNegXH1uItHnxM4evRo5s2bx5QpU+jatSsrV65k+PDhHD16lIkTJxpdnlcLqg1d74A1n50Ngl2GlVzn/ADY9Q4IrGlMvSJyaQpyYcN/4cTus217VjsvDdtC9CBzDQBgt0PKEkhNLtm+9nPnUOmxt5jv8+7GybWK/59fkAuAv19gcdt/n810e00iUnn71lXt/eXnwOEUaBJdtffrCTbsg49Xlgx9s3+Gr9bCPb2hXRPjaqtuHhsCZ82axcyZM0lMTCQuLg6Avn37snbtWubNm0eXLl0MrtD7lRYEiygAivgOhwOSv4aTe0tffuTMHtCYm9xXk9G2J10YAIucOvN5eOVdYPXYb8mqd27Ie3HOnygsLODhO2caV5CIVFp2OqQfqPr7Pfy774XAHYdh5vLSe3/k5sO7SfC3a6F5A/fX5g4e2x00ISGBgQMHFgfAIm3atMHf35+YmBgA8vLymDhxIpGRkXTs2JGrr77aiHK9VlEQLOoaWkQBUMR3pKWWHQCLHEmBTJN0Dc3NqmBPuQOyjsGRbW4rSUSkSpw6VE33e9j3usov2ggOnJfzOXD2GPl+k5uLciOPDIGpqals2rSJYcOGXbBs7969REdHExjo7KLy+OOPk5GRwZYtW9i4cSOfffaZu8v1eucGwSIKgCK+4+BmwOLCer9Veyke4fAW58AJ5bJoMAQR8T5Zx6vnfvOzS59o3lulnYZtFQRbB7ApFU7nuq0st/LIji6pqakAhIWFlWjPzs4mKSmJQYMGAXD69Gnefvtt9u3bh81mA6Bx48YubcNiceEXkcm0CuvAOw9tBCAvP4dRj3YidfRWg6sSkUs1ddR/6X75IGxWW5nrFBYW8O5bnzIt/h43VmaMPw5K4M6+j2CxlLMf1AG/rkymy7BObqurqk3/yzJiW8cbsu2kpES6D+9ryLZFzOxPg5/njr6PlGiraATQspYvnl7yesvmERw6sesSqvMcl7XoxIhnKz550gGEt4gk7fD26i+qijhcPGTrkUcCQ0NDAUhJSSnRPm3aNA4ePEjXrl0B2L59O3Xr1uWll17iiiuuoEePHnz++edur9cX1K0ZyqMjPim+HuAfxOMjPqV2cH0DqxKRqnDq9DFK7/BylsViIT3rmHsKMtiprGPlB0DAbi8kLfNIueuIiHiavIKc6rtvVyeX9gLZGa593zkcdnKyTlRzNcbwyCOBERERxMTEkJCQQEhICOHh4cydO5cFCxYAFIfAgoIC9u/fT+PGjfnll1/YvXs3vXr1IjIyks6dO5e7DVdTshmcPwhMh+th438hki58/9YJugwD/2CjqxSRi3V8N6ybW/46VquNaf+ZyFuNfH/k5ZwMWPEO5eZiq9XGXfdfw8PveO93xerZzvNBjRAXF49jhvc+dyLe6tAW2PRNybbzj+gVKToCWNbyc/kFwbH0g/hSR7pXv4PdR8v+KrAA7cKtZGdUUx9bg3nkkUCr1cqcOXOIjo5m3LhxjBo1itDQUCZMmIDNZiseFKZ58+YA3HOPs/tSy5Ytueqqq/jll18Mq93blDYKaJ1GJQeLcWVCeRHxXCEtoG4Fw1w3aOV875tBUG0IjylnBYvz869RlLsqEhGpGnXCKl7nou63ET4VAAGu61h+ALRY4BofGxH1XB4ZAgHatm3LsmXLyMrKYu/evUydOpWNGzfSvn17goOdh6VCQ0MZOHAg//vf/wA4fvw4v/zyC7GxsUaW7jXKmwbi/FFDFQRFvJfFArE3Q92iU6Yt51xwhsSONxpUnEGi+kFYu3Maznk+atR3zptqpnkTRcQ3BNeF2tWwQ88Xd4pd3hju6gW2M2moKPiBs+2e3hDR0LDyqp3F4UX9Itu1a0ePHj14//33i9v27NnD6NGjOXz4MA6HgwkTJjBu3DgDq/QOrs4DmJPhnEcwOw1qN0RdQ0W8mMPhnCri4G/OyX8Dajjnfaob7nt7eF116jAc2Oj8rPMLgIZREBoBVo/dReo6I7uD1msK3e40ZtsiZrd/I/z+bcXrudod1C8Q+owFW8Cl1+aJMnPgpx2w55jzu7DVZXBFBNQMNLqy6uWR5wSWJjMzk5SUFMaPH1+ivUWLFixevNigqrzXzpWuTQNx/oTyu36CthrwTcQrWSzOo34hLYyuxHPUaWSebrAiYg6NoyF1PWQcrpr7a9PHdwMgQK0gGODD3T7L4jUhsFatWhQWFhpdhs+IjHPOkxVxVcXzABYFwV0/Qes+7qlPRERERCrPaoXogfDLx2Av56ezKwPChLSAcJ1l5ZN8oMOLXAybP7S71vWJ4INqQ7trwOY1uw1EREREzKnWZRAzBCxlTw9boTqNIOYm854u4OsUAkVEREREfExohHMsh6A6lb9to8uhy+3O8wHFN+m4joiIiIiID6rfFHrc6xwLYv8GKMwrf/1aoc5ThRpGuqU8MZBCoIiIiIiIj/ILgLbxENELjqRA+gHnoDH5uc6unkXTSoS2Mvdo0WajECgiIiIi4uP8AqBJB+dFROcEioiIiIiImIhCoIiIiIiIiImoO6iIiIgPqt3QnNsWEZGKWRwOh8PoIkRERERERMQ91B1URERERETERBQCRURERERETEQhUERERERExEQUAkVERERERExEIVBERERERMREFAJFRERERERMRCFQRERERETERBQCRURERERETEQhUERERERExEQUAkVERERERExEIVBERERERMREFAJFRERERERMRCFQRERERETERBQCRURERERETEQhUERERERExEQUAkVERERERExEIVBERERERMREFAJFRERERERM5P8BHfUt9Ejgh78AAAAASUVORK5CYII=\n", "text/plain": [ "
" ] @@ -177,6 +179,97 @@ "print(f'difference from state vector {amp_diff}')" ] }, + { + "cell_type": "markdown", + "id": "6fa82949-43e1-4d55-8af5-554aa5c9020e", + "metadata": {}, + "source": [ + "### calculate batch of bistring amplitudes\n", + "\n", + "In this example, we calculate a batch of bistring amplitudes $\\langle 00000ij|\\psi\\rangle$ where the first 5 qubits are fixed at state $00000$ and the last two qubit states are batched. This is equivalent to computing a slice of the full state vector." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "180793ae-8cbd-44ba-ac3b-19d4679989cb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "for bitstring 0000000, amplitude: -0.4516+0.2872j, difference from state vector: 0.0000\n", + "for bitstring 0000001, amplitude: 0.0000+0.0000j, difference from state vector: 0.0000\n", + "for bitstring 0000010, amplitude: -0.1704+0.5073j, difference from state vector: 0.0000\n", + "for bitstring 0000011, amplitude: 0.0000+0.0000j, difference from state vector: 0.0000\n" + ] + } + ], + "source": [ + "fixed_states = '00000'\n", + "fixed_index = tuple(map(int, fixed_states))\n", + "num_fixed = len(fixed_states)\n", + "\n", + "# mapping of the first 5 qubits to the fixed state\n", + "fixed = dict(zip(myconverter.qubits[:num_fixed], fixed_states))\n", + "\n", + "expression, operands = myconverter.batched_amplitudes(fixed)\n", + "batched_amplitudes = contract(expression, *operands)\n", + "\n", + "for ibit, jbit in itertools.product(range(2), repeat=2):\n", + " bitstring = fixed_states + str(ibit) + str(jbit)\n", + " index = fixed_index + (ibit, jbit)\n", + " amplitude = batched_amplitudes[(ibit, jbit)]\n", + " amplitude_from_sv = sv[index]\n", + " amp_diff = abs(amplitude-amplitude_from_sv)\n", + " print(f'for bitstring {bitstring}, amplitude: {amplitude:.4f}, difference from state vector: {amp_diff:.4f}')" + ] + }, + { + "cell_type": "markdown", + "id": "5ae16861-c6eb-4ec0-a527-c65aed4cbcd8", + "metadata": {}, + "source": [ + "### compute expectation value $\\langle \\psi|\\hat{O}| \\psi\\rangle$\n", + "\n", + "In this example, we compute the expectation value for a pauli string $IXXZZII$. For comparision, we compute the same value via contracting reduced density matrix with the operator." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "079beec1-8bc2-4aec-a272-01e160e104f1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "expectation value for IXXZZII: (0.13257764904443228+0j)\n", + "is expectation value in agreement? True\n" + ] + } + ], + "source": [ + "pauli_string = 'IXXZZII'\n", + "expression, operands = myconverter.expectation(pauli_string, lightcone=True)\n", + "expec = contract(expression, *operands)\n", + "print(f'expectation value for {pauli_string}: {expec}')\n", + "\n", + "# expectation value from reduced density matrix\n", + "qubits = myconverter.qubits\n", + "where = qubits[1:5]\n", + "rdm_expression, rdm_operands = myconverter.reduced_density_matrix(where, lightcone=True)\n", + "rdm = contract(rdm_expression, *rdm_operands)\n", + "\n", + "pauli_x = cp.asarray([[0,1],[1,0]], dtype=myconverter.dtype)\n", + "pauli_z = cp.asarray([[1,0],[0,-1]], dtype=myconverter.dtype)\n", + "expec_from_rdm = cp.einsum('abcdABCD,aA,bB,cC,dD->', rdm, pauli_x, pauli_x, pauli_z, pauli_z)\n", + "\n", + "print(f\"is expectation value in agreement?\", cp.allclose(expec, expec_from_rdm))" + ] + }, { "cell_type": "markdown", "id": "24f09f1f-6507-4804-ac12-d96d8ee332da", @@ -189,7 +282,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 8, "id": "af4a503c-2f3e-4093-89ef-06457f68de9c", "metadata": {}, "outputs": [ @@ -234,7 +327,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.10" + "version": "3.9.12" } }, "nbformat": 4, diff --git a/python/samples/cutensornet/coarse/example21.py b/python/samples/cutensornet/coarse/example21.py new file mode 100644 index 0000000..910b58e --- /dev/null +++ b/python/samples/cutensornet/coarse/example21.py @@ -0,0 +1,17 @@ +""" +Example illustrating lazy conjugation using tensor qualifiers. +""" +import numpy as np + +from cuquantum import contract, tensor_qualifiers_dtype + +a = np.random.rand(3, 2) + 1j * np.random.rand(3, 2) +b = np.random.rand(2, 3) + 1j * np.random.rand(2, 3) + +# Specify tensor qualifiers for the second tensor operand 'b'. +qualifiers = np.zeros((2,), dtype=tensor_qualifiers_dtype) +qualifiers[1]['is_conjugate'] = True + +r = contract("ij,jk", a, b, qualifiers=qualifiers) +s = np.einsum("ij,jk", a, b.conj()) +assert np.allclose(r, s), "Incorrect results for a * conjugate(b)" diff --git a/python/samples/cutensornet/coarse/example22_mpi_auto.py b/python/samples/cutensornet/coarse/example22_mpi_auto.py new file mode 100644 index 0000000..76b825c --- /dev/null +++ b/python/samples/cutensornet/coarse/example22_mpi_auto.py @@ -0,0 +1,79 @@ +# Copyright (c) 2021-2022, NVIDIA CORPORATION & AFFILIATES +# +# SPDX-License-Identifier: BSD-3-Clause + +""" +Example illustrating automatically parallelizing slice-based tensor network contraction with cuQuantum using MPI. +Here we use: + + - the buffer interface APIs offered by mpi4py v3.1.0+ for communicating ndarray-like objects + - CUDA-aware MPI (note: as of cuTensorNet v2.0.0 using non-CUDA-aware MPI is not supported + and would cause segfault). + - cuQuantum 22.11+ (cuTensorNet v2.0.0+) for the new distributed contraction feature + +$ mpiexec -n 4 python example22_mpi_auto.py +""" +import os + +import cupy as cp +from cupy.cuda.runtime import getDeviceCount +from mpi4py import MPI # this line initializes MPI + +import cuquantum +from cuquantum import cutensornet as cutn + + +root = 0 +comm = MPI.COMM_WORLD +rank, size = comm.Get_rank(), comm.Get_size() + +# Check if the env var is set +if not "CUTENSORNET_COMM_LIB" in os.environ: + raise RuntimeError("need to set CUTENSORNET_COMM_LIB to the path of the MPI wrapper library") + +if not os.path.isfile(os.environ["CUTENSORNET_COMM_LIB"]): + raise RuntimeError("CUTENSORNET_COMM_LIB does not point to the path of the MPI wrapper library") + +# Assign the device for each process. +device_id = rank % getDeviceCount() +cp.cuda.Device(device_id).use() + +expr = 'ehl,gj,edhg,bif,d,c,k,iklj,cf,a->ba' +shapes = [(8, 2, 5), (5, 7), (8, 8, 2, 5), (8, 6, 3), (8,), (6,), (5,), (6, 5, 5, 7), (6, 3), (3,)] + +# Set the operand data on root. Since we use the buffer interface APIs offered by mpi4py for communicating array +# objects, we can directly use device arrays (cupy.ndarray, for example) as we assume mpi4py is built against +# a CUDA-aware MPI. +if rank == root: + operands = [cp.random.rand(*shape) for shape in shapes] +else: + operands = [cp.empty(shape) for shape in shapes] + +# Broadcast the operand data. Throughout this sample we take advantage of the upper-case mpi4py APIs +# that support communicating CPU & GPU buffers (without staging) to reduce serialization overhead for +# array-like objects. This capability requires mpi4py v3.10+. +for operand in operands: + comm.Bcast(operand, root) + +# Bind the communicator to the library handle +handle = cutn.create() +cutn.distributed_reset_configuration( + handle, *cutn.get_mpi_comm_pointer(comm) +) + +# Compute the contraction (with distributed path finding & contraction execution) +result = cuquantum.contract(expr, *operands, options={'device_id' : device_id, 'handle': handle}) + +# Create a new GPU buffer for verification +result_cp = cp.empty_like(result) + +# Sum the partial contribution from each process on root, with GPU +if rank == root: + comm.Reduce(sendbuf=MPI.IN_PLACE, recvbuf=result_cp, op=MPI.SUM, root=root) +else: + comm.Reduce(sendbuf=result_cp, recvbuf=None, op=MPI.SUM, root=root) + +# Check correctness. +if rank == root: + result_cp = cp.einsum(expr, *operands, optimize=True) + print("Does the cuQuantum parallel contraction result match the cupy.einsum result?", cp.allclose(result, result_cp)) diff --git a/python/samples/cutensornet/fine/example4_mpi_nccl.py b/python/samples/cutensornet/fine/example4_mpi_nccl.py new file mode 100644 index 0000000..c87a509 --- /dev/null +++ b/python/samples/cutensornet/fine/example4_mpi_nccl.py @@ -0,0 +1,99 @@ +# Copyright (c) 2021-2022, NVIDIA CORPORATION & AFFILIATES +# +# SPDX-License-Identifier: BSD-3-Clause + +""" +Example illustrating slice-based parallel tensor network contraction with cuQuantum using NCCL and MPI. Here +we create the input tensors directly on the GPU using CuPy since NCCL only supports GPU buffers. + +The low-level Python wrapper for NCCL is provided by CuPy. MPI (through mpi4py) is only needed to bootstrap +the multiple processes, set up the NCCL communicator, and to communicate data on the CPU. NCCL can be used +without MPI for a "single process multiple GPU" model. + +For users who do not have NCCL installed already, CuPy provides detailed instructions on how to install +it for both pip and conda users when "import cupy.cuda.nccl" fails. + +We recommend that those using CuPy v10+ use CuPy's high-level "cupyx.distributed" module to avoid having to +manipulate GPU pointers in Python. + +Note that with recent NCCL, GPUs cannot be oversubscribed (not more than one process per GPU). Users will +see an NCCL error if the number of processes on a node exceeds the number of GPUs on that node. + +$ mpiexec -n 4 python example4_mpi_nccl.py +""" + +import cupy as cp +from cupy.cuda import nccl +from cupy.cuda.runtime import getDeviceCount +from mpi4py import MPI + +from cuquantum import Network + +# Set up the MPI environment. +root = 0 +comm_mpi = MPI.COMM_WORLD +rank, size = comm_mpi.Get_rank(), comm_mpi.Get_size() + +# Assign the device for each process. +device_id = rank % getDeviceCount() + +# Define the tensor network topology. +expr = 'ehl,gj,edhg,bif,d,c,k,iklj,cf,a->ba' +shapes = [(8, 2, 5), (5, 7), (8, 8, 2, 5), (8, 6, 3), (8,), (6,), (5,), (6, 5, 5, 7), (6, 3), (3,)] + +# Note that all NCCL operations must be performed in the correct device context. +cp.cuda.Device(device_id).use() + +# Set up the NCCL communicator. +nccl_id = nccl.get_unique_id() if rank == root else None +nccl_id = comm_mpi.bcast(nccl_id, root) +comm_nccl = nccl.NcclCommunicator(size, nccl_id, rank) + +# Set the operand data on root. +if rank == root: + operands = [cp.random.rand(*shape) for shape in shapes] +else: + operands = [cp.empty(shape) for shape in shapes] + +# Broadcast the operand data. We pass in the CuPy ndarray data pointers to the NCCL APIs. +stream_ptr = cp.cuda.get_current_stream().ptr +for operand in operands: + comm_nccl.broadcast(operand.data.ptr, operand.data.ptr, operand.size, nccl.NCCL_FLOAT64, root, stream_ptr) + +# Create network object. +network = Network(expr, *operands) + +# Compute the path on all ranks with 8 samples for hyperoptimization. Force slicing to enable parallel contraction. +path, info = network.contract_path(optimize={'samples': 8, 'slicing': {'min_slices': max(16, size)}}) + +# Select the best path from all ranks. Note that we still use the MPI communicator here for simplicity. +opt_cost, sender = comm_mpi.allreduce(sendobj=(info.opt_cost, rank), op=MPI.MINLOC) +if rank == root: + print(f"Process {sender} has the path with the lowest FLOP count {opt_cost}.") + +# Broadcast info from the sender to all other ranks. +info = comm_mpi.bcast(info, sender) + +# Set path and slices. +path, info = network.contract_path(optimize={'path': info.path, 'slicing': info.slices}) + +# Calculate this process's share of the slices. +num_slices = info.num_slices +chunk, extra = num_slices // size, num_slices % size +slice_begin = rank * chunk + min(rank, extra) +slice_end = num_slices if rank == size - 1 else (rank + 1) * chunk + min(rank + 1, extra) +slices = range(slice_begin, slice_end) + +print(f"Process {rank} is processing slice range: {slices}.") + +# Contract the group of slices the process is responsible for. +result = network.contract(slices=slices) + +# Sum the partial contribution from each process on root. +stream_ptr = cp.cuda.get_current_stream().ptr +comm_nccl.reduce(result.data.ptr, result.data.ptr, result.size, nccl.NCCL_FLOAT64, nccl.NCCL_SUM, root, stream_ptr) + +# Check correctness. +if rank == root: + result_cp = cp.einsum(expr, *operands, optimize=True) + print("Does the cuQuantum parallel contraction result match the cupy.einsum result?", cp.allclose(result, result_cp)) diff --git a/python/samples/cutensornet/tensornet_example.py b/python/samples/cutensornet/tensornet_example.py old mode 100644 new mode 100755 index 3461f33..ab030e3 --- a/python/samples/cutensornet/tensornet_example.py +++ b/python/samples/cutensornet/tensornet_example.py @@ -21,59 +21,43 @@ print("GPU-minor:", props["minor"]) print("========================") -########################################################## -# Computing: D_{m,x,n,y} = A_{m,h,k,n} B_{u,k,h} C_{x,u,y} -########################################################## +###################################################################################### +# Computing: R_{k,l} = A_{a,b,c,d,e,f} B_{b,g,h,e,i,j} C_{m,a,g,f,i,k} D_{l,c,h,d,j,m} +###################################################################################### -print("Include headers and define data types") +print("Include headers and define data types.") data_type = cuquantum.cudaDataType.CUDA_R_32F compute_type = cuquantum.ComputeType.COMPUTE_32F -numInputs = 3 +num_inputs = 4 # Create an array of modes -modesA = [ord(c) for c in ('m','h','k','n')] -modesB = [ord(c) for c in ('u','k','h')] -modesC = [ord(c) for c in ('x','u','y')] -modesD = [ord(c) for c in ('m','x','n','y')] +modes_A = [ord(c) for c in ('a','b','c','d','e','f')] +modes_B = [ord(c) for c in ('b','g','h','e','i','j')] +modes_C = [ord(c) for c in ('m','a','g','f','i','k')] +modes_D = [ord(c) for c in ('l','c','h','d','j','m')] +modes_R = [ord(c) for c in ('k','l')] # Create an array of extents (shapes) for each tensor -extentA = (96, 64, 64, 96) -extentB = (96, 64, 64) -extentC = (64, 96, 64) -extentD = (96, 64, 96, 64) - -print("Define network, modes, and extents") - -############################ -# Allocate & initialize data -############################ - -A_d = cp.random.random((np.prod(extentA),), dtype=np.float32) -B_d = cp.random.random((np.prod(extentB),), dtype=np.float32) -C_d = cp.random.random((np.prod(extentC),), dtype=np.float32) -D_d = cp.empty((np.prod(extentD),), dtype=np.float32) -rawDataIn_d = (A_d.data.ptr, B_d.data.ptr, C_d.data.ptr) - -A = cp.asnumpy(A_d) -B = cp.asnumpy(B_d) -C = cp.asnumpy(C_d) -D = np.empty(D_d.shape, dtype=np.float32) - -#################### -# Allocate workspace -#################### - -# this is one way to proceed: query the currently available memory on the -# device, and allocate a big fraction of it... -#freeMem, totalMem = dev.mem_info -#worksize = int(freeMem * 0.9) -# ...but in this case we can set a much tighter upper bound, since we know -# the rough answer already -worksize = 128*1024**2 # = 128 MB, can be smaller -work = cp.cuda.alloc(worksize) - -print("Allocate memory for data and workspace, and initialize data.") +dim = 8 +extent_A = (dim,) * 6 +extent_B = (dim,) * 6 +extent_C = (dim,) * 6 +extent_D = (dim,) * 6 +extent_R = (dim,) * 2 + +print("Define network, modes, and extents.") + +################# +# Initialize data +################# + +A_d = cp.random.random((np.prod(extent_A),), dtype=np.float32) +B_d = cp.random.random((np.prod(extent_B),), dtype=np.float32) +C_d = cp.random.random((np.prod(extent_C),), dtype=np.float32) +D_d = cp.random.random((np.prod(extent_D),), dtype=np.float32) +R_d = cp.zeros((np.prod(extent_R),), dtype=np.float32) +raw_data_in_d = (A_d.data.ptr, B_d.data.ptr, C_d.data.ptr, D_d.data.ptr) ############# # cuTensorNet @@ -82,73 +66,68 @@ stream = cp.cuda.Stream() handle = cutn.create() -nmodeA = len(modesA) -nmodeB = len(modesB) -nmodeC = len(modesC) -nmodeD = len(modesD) +nmode_A = len(modes_A) +nmode_B = len(modes_B) +nmode_C = len(modes_C) +nmode_D = len(modes_D) +nmode_R = len(modes_R) ############################### # Create Contraction Descriptor ############################### -# These also work, but require a bit more keystrokes -#modesA = np.asarray(modesA, dtype=np.int32) -#modesB = np.asarray(modesB, dtype=np.int32) -#modesC = np.asarray(modesC, dtype=np.int32) -#modesIn = (modesA.ctypes.data, modesB.ctypes.data, modesC.ctypes.data) -#extentA = np.asarray(extentA, dtype=np.int64) -#extentB = np.asarray(extentB, dtype=np.int64) -#extentC = np.asarray(extentC, dtype=np.int64) -#extentsIn = (extentA.ctypes.data, extentB.ctypes.data, extentC.ctypes.data) - -modesIn = (modesA, modesB, modesC) -extentsIn = (extentA, extentB, extentC) -numModesIn = (nmodeA, nmodeB, nmodeC) - -# strides are optional; if no stride (0) is provided, then cuTensorNet assumes a generalized column-major data layout -stridesIn = (0, 0, 0) - -# compute the alignments -# we hard-code them here because CuPy arrays are at least 256B aligned -alignmentsIn = (256, 256, 256) -alignmentOut = 256 - -# setup tensor network -descNet = cutn.create_network_descriptor(handle, - numInputs, numModesIn, extentsIn, stridesIn, modesIn, alignmentsIn, # inputs - nmodeD, extentD, 0, modesD, alignmentOut, # output +modes_in = (modes_A, modes_B, modes_C, modes_D) +extents_in = (extent_A, extent_B, extent_C, extent_D) +num_modes_in = (nmode_A, nmode_B, nmode_C, nmode_D) + +# Strides are optional; if no stride (0) is provided, then cuTensorNet assumes a generalized column-major data layout +strides_in = (0, 0, 0, 0) + +# Set up the tensor qualifiers for all input tensors +qualifiers_in = np.zeros(num_inputs, dtype=cutn.tensor_qualifiers_dtype) + +# Set up tensor network +desc_net = cutn.create_network_descriptor(handle, + num_inputs, num_modes_in, extents_in, strides_in, modes_in, qualifiers_in, # inputs + nmode_R, extent_R, 0, modes_R, # output data_type, compute_type) print("Initialize the cuTensorNet library and create a network descriptor.") +##################################################### +# Choose workspace limit based on available resources +##################################################### + +free_mem, total_mem = dev.mem_info +workspace_limit = int(free_mem * 0.9) + ############################################## # Find "optimal" contraction order and slicing ############################################## -optimizerConfig = cutn.create_contraction_optimizer_config(handle) +optimizer_config = cutn.create_contraction_optimizer_config(handle) # Set the value of the partitioner imbalance factor to 30 (if desired) imbalance_dtype = cutn.contraction_optimizer_config_get_attribute_dtype( cutn.ContractionOptimizerConfigAttribute.GRAPH_IMBALANCE_FACTOR) imbalance_factor = np.asarray((30,), dtype=imbalance_dtype) cutn.contraction_optimizer_config_set_attribute( - handle, optimizerConfig, cutn.ContractionOptimizerConfigAttribute.GRAPH_IMBALANCE_FACTOR, + handle, optimizer_config, cutn.ContractionOptimizerConfigAttribute.GRAPH_IMBALANCE_FACTOR, imbalance_factor.ctypes.data, imbalance_factor.dtype.itemsize) -optimizerInfo = cutn.create_contraction_optimizer_info(handle, descNet) +optimizer_info = cutn.create_contraction_optimizer_info(handle, desc_net) -cutn.contraction_optimize( - handle, descNet, optimizerConfig, worksize, optimizerInfo) +cutn.contraction_optimize(handle, desc_net, optimizer_config, workspace_limit, optimizer_info) -numSlices_dtype = cutn.contraction_optimizer_info_get_attribute_dtype( +num_slices_dtype = cutn.contraction_optimizer_info_get_attribute_dtype( cutn.ContractionOptimizerInfoAttribute.NUM_SLICES) -numSlices = np.zeros((1,), dtype=numSlices_dtype) +num_slices = np.zeros((1,), dtype=num_slices_dtype) cutn.contraction_optimizer_info_get_attribute( - handle, optimizerInfo, cutn.ContractionOptimizerInfoAttribute.NUM_SLICES, - numSlices.ctypes.data, numSlices.dtype.itemsize) -numSlices = int(numSlices) + handle, optimizer_info, cutn.ContractionOptimizerInfoAttribute.NUM_SLICES, + num_slices.ctypes.data, num_slices.dtype.itemsize) +num_slices = int(num_slices) -assert numSlices > 0 +assert num_slices > 0 print("Find an optimized contraction path with cuTensorNet optimizer.") @@ -156,20 +135,18 @@ # Initialize all pair-wise contraction plans (for cuTENSOR) ########################################################### -workDesc = cutn.create_workspace_descriptor(handle) -cutn.workspace_compute_sizes(handle, descNet, optimizerInfo, workDesc) -requiredWorkspaceSize = cutn.workspace_get_size( - handle, workDesc, +work_desc = cutn.create_workspace_descriptor(handle) +cutn.workspace_compute_contraction_sizes(handle, desc_net, optimizer_info, work_desc) +required_workspace_size = cutn.workspace_get_size( + handle, work_desc, cutn.WorksizePref.MIN, cutn.Memspace.DEVICE) -if worksize < requiredWorkspaceSize: - raise MemoryError("Not enough workspace memory is available.") +work = cp.cuda.alloc(required_workspace_size) cutn.workspace_set( - handle, workDesc, + handle, work_desc, cutn.Memspace.DEVICE, - work.ptr, worksize) -plan = cutn.create_contraction_plan( - handle, descNet, optimizerInfo, workDesc) + work.ptr, required_workspace_size) +plan = cutn.create_contraction_plan(handle, desc_net, optimizer_info, work_desc) ################################################################################### # Optional: Auto-tune cuTENSOR's cutensorContractionPlan to pick the fastest kernel @@ -177,45 +154,41 @@ pref = cutn.create_contraction_autotune_preference(handle) -numAutotuningIterations = 5 # may be 0 +num_autotuning_iterations = 5 # may be 0 n_iter_dtype = cutn.contraction_autotune_preference_get_attribute_dtype( cutn.ContractionAutotunePreferenceAttribute.MAX_ITERATIONS) -numAutotuningIterations = np.asarray([numAutotuningIterations], dtype=n_iter_dtype) +num_autotuning_iterations = np.asarray([num_autotuning_iterations], dtype=n_iter_dtype) cutn.contraction_autotune_preference_set_attribute( handle, pref, cutn.ContractionAutotunePreferenceAttribute.MAX_ITERATIONS, - numAutotuningIterations.ctypes.data, numAutotuningIterations.dtype.itemsize) + num_autotuning_iterations.ctypes.data, num_autotuning_iterations.dtype.itemsize) -# modify the plan again to find the best pair-wise contractions +# Modify the plan again to find the best pair-wise contractions cutn.contraction_autotune( - handle, plan, rawDataIn_d, D_d.data.ptr, - workDesc, pref, stream.ptr) + handle, plan, raw_data_in_d, R_d.data.ptr, + work_desc, pref, stream.ptr) cutn.destroy_contraction_autotune_preference(pref) print("Create a contraction plan for cuTENSOR and optionally auto-tune it.") -##### -# Run -##### +########### +# Execution +########### minTimeCUTENSOR = 1e100 -numRuns = 3 # to get stable perf results +num_runs = 3 # to get stable perf results e1 = cp.cuda.Event() e2 = cp.cuda.Event() -sliceGroup = cutn.create_slice_group_from_id_range(handle, 0, numSlices, 1) - -for i in range(numRuns): - # restore output - D_d.data.copy_from(D.ctypes.data, D.size * D.dtype.itemsize) - dev.synchronize() +slice_group = cutn.create_slice_group_from_id_range(handle, 0, num_slices, 1) +for i in range(num_runs): # Contract over all slices. # A user may choose to parallelize over the slices across multiple devices. e1.record() cutn.contract_slices( - handle, plan, rawDataIn_d, D_d.data.ptr, False, - workDesc, sliceGroup, stream.ptr) + handle, plan, raw_data_in_d, R_d.data.ptr, False, + work_desc, slice_group, stream.ptr) e2.record() # Synchronize and measure timing @@ -223,16 +196,20 @@ time = cp.cuda.get_elapsed_time(e1, e2) / 1000 # ms -> s minTimeCUTENSOR = minTimeCUTENSOR if minTimeCUTENSOR < time else time - print("Contract the network, each slice uses the same contraction plan.") -# recall that we set strides to null (0), so the data are in F-contiguous layout -A_d = A_d.reshape(extentA, order='F') -B_d = B_d.reshape(extentB, order='F') -C_d = C_d.reshape(extentC, order='F') -D_d = D_d.reshape(extentD, order='F') -out = cp.einsum("mhkn,ukh,xuy->mxny", A_d, B_d, C_d) -if not cp.allclose(out, D_d): +# free up the workspace +del work + +# Recall that we set strides to null (0), so the data are in F-contiguous layout +A_d = A_d.reshape(extent_A, order='F') +B_d = B_d.reshape(extent_B, order='F') +C_d = C_d.reshape(extent_C, order='F') +D_d = D_d.reshape(extent_D, order='F') +R_d = R_d.reshape(extent_R, order='F') +path, _ = cuquantum.einsum_path("abcdef,bgheij,magfik,lchdjm->kl", A_d, B_d, C_d, D_d) +out = cp.einsum("abcdef,bgheij,magfik,lchdjm->kl", A_d, B_d, C_d, D_d, optimize=path) +if not cp.allclose(out, R_d): raise RuntimeError("result is incorrect") print("Check cuTensorNet result against that of cupy.einsum().") @@ -242,20 +219,20 @@ cutn.ContractionOptimizerInfoAttribute.FLOP_COUNT) flops = np.zeros((1,), dtype=flops_dtype) cutn.contraction_optimizer_info_get_attribute( - handle, optimizerInfo, cutn.ContractionOptimizerInfoAttribute.FLOP_COUNT, + handle, optimizer_info, cutn.ContractionOptimizerInfoAttribute.FLOP_COUNT, flops.ctypes.data, flops.dtype.itemsize) flops = float(flops) -print(f"numSlices: {numSlices}") -print(f"{minTimeCUTENSOR * 1000 / numSlices} ms / slice") -print(f"{flops/1e9/minTimeCUTENSOR} GFLOPS/s") +print(f"num_slices: {num_slices}") +print(f"{minTimeCUTENSOR * 1000 / num_slices} ms / slice") +print(f"{flops / 1e9 / minTimeCUTENSOR} GFLOPS/s") -cutn.destroy_slice_group(sliceGroup) +cutn.destroy_slice_group(slice_group) cutn.destroy_contraction_plan(plan) -cutn.destroy_contraction_optimizer_info(optimizerInfo) -cutn.destroy_contraction_optimizer_config(optimizerConfig) -cutn.destroy_network_descriptor(descNet) -cutn.destroy_workspace_descriptor(workDesc) +cutn.destroy_contraction_optimizer_info(optimizer_info) +cutn.destroy_contraction_optimizer_config(optimizer_config) +cutn.destroy_network_descriptor(desc_net) +cutn.destroy_workspace_descriptor(work_desc) cutn.destroy(handle) print("Free resource and exit.") diff --git a/python/samples/cutensornet/tensornet_example_mpi.py b/python/samples/cutensornet/tensornet_example_mpi.py old mode 100644 new mode 100755 index 13c1617..956c63e --- a/python/samples/cutensornet/tensornet_example_mpi.py +++ b/python/samples/cutensornet/tensornet_example_mpi.py @@ -17,11 +17,11 @@ print("*** Printing is done only from the root process to prevent jumbled messages ***") print(f"The number of processes is {size}") -# Get cuTensorNet version and device properties. -numDevices = cp.cuda.runtime.getDeviceCount() -deviceId = rank % numDevices # We assume that the processes are mapped to nodes in contiguous chunks. -dev = cp.cuda.Device(deviceId) +num_devices = cp.cuda.runtime.getDeviceCount() +device_id = rank % num_devices +dev = cp.cuda.Device(device_id) dev.use() + props = cp.cuda.runtime.getDeviceProperties(dev.id) if rank == root: print("cuTensorNet-vers:", cutn.get_version()) @@ -34,60 +34,62 @@ print("GPU-minor:", props["minor"]) print("========================") -########################################################## -# Computing: D_{m,x,n,y} = A_{m,h,k,n} B_{u,k,h} C_{x,u,y} -########################################################## +###################################################################################### +# Computing: R_{k,l} = A_{a,b,c,d,e,f} B_{b,g,h,e,i,j} C_{m,a,g,f,i,k} D_{l,c,h,d,j,m} +###################################################################################### if rank == root: - print("Include headers and define data types") + print("Include headers and define data types.") data_type = cuquantum.cudaDataType.CUDA_R_32F compute_type = cuquantum.ComputeType.COMPUTE_32F -numInputs = 3 +num_inputs = 4 # Create an array of modes -modesA = [ord(c) for c in ('m','h','k','n')] -modesB = [ord(c) for c in ('u','k','h')] -modesC = [ord(c) for c in ('x','u','y')] -modesD = [ord(c) for c in ('m','x','n','y')] +modes_A = [ord(c) for c in ('a','b','c','d','e','f')] +modes_B = [ord(c) for c in ('b','g','h','e','i','j')] +modes_C = [ord(c) for c in ('m','a','g','f','i','k')] +modes_D = [ord(c) for c in ('l','c','h','d','j','m')] +modes_R = [ord(c) for c in ('k','l')] # Create an array of extents (shapes) for each tensor -extentA = (96, 64, 64, 96) -extentB = (96, 64, 64) -extentC = (64, 96, 64) -extentD = (96, 64, 96, 64) +dim = 8 +extent_A = (dim,) * 6 +extent_B = (dim,) * 6 +extent_C = (dim,) * 6 +extent_D = (dim,) * 6 +extent_R = (dim,) * 2 if rank == root: - print("Define network, modes, and extents") + print("Define network, modes, and extents.") -############################ -# Allocate & initialize data -############################ +################# +# Initialize data +################# if rank == root: - A = np.random.random((np.prod(extentA),)).astype(np.float32) - B = np.random.random((np.prod(extentB),)).astype(np.float32) - C = np.random.random((np.prod(extentC),)).astype(np.float32) + A = np.random.random(np.prod(extent_A)).astype(np.float32) + B = np.random.random(np.prod(extent_B)).astype(np.float32) + C = np.random.random(np.prod(extent_C)).astype(np.float32) + D = np.random.random(np.prod(extent_D)).astype(np.float32) else: - A = np.empty((np.prod(extentA),), dtype=np.float32) - B = np.empty((np.prod(extentB),), dtype=np.float32) - C = np.empty((np.prod(extentC),), dtype=np.float32) -D = np.empty(extentD, dtype=np.float32) + A = np.empty(np.prod(extent_A), dtype=np.float32) + B = np.empty(np.prod(extent_B), dtype=np.float32) + C = np.empty(np.prod(extent_C), dtype=np.float32) + D = np.empty(np.prod(extent_D), dtype=np.float32) +R = np.empty(extent_R) -# Broadcast data to all ranks. comm.Bcast(A, root) comm.Bcast(B, root) comm.Bcast(C, root) +comm.Bcast(D, root) -# Copy data onto the device on all ranks. A_d = cp.asarray(A) B_d = cp.asarray(B) C_d = cp.asarray(C) -D_d = cp.empty((np.prod(extentD),), dtype=np.float32) -rawDataIn_d = (A_d.data.ptr, B_d.data.ptr, C_d.data.ptr) - -if rank == root: - print("Allocate memory for data, calculate workspace limit, and initialize data.") +D_d = cp.asarray(D) +R_d = cp.empty(np.prod(extent_R), dtype=np.float32) +raw_data_in_d = (A_d.data.ptr, B_d.data.ptr, C_d.data.ptr, D_d.data.ptr) ############# # cuTensorNet @@ -96,41 +98,30 @@ stream = cp.cuda.Stream() handle = cutn.create() -nmodeA = len(modesA) -nmodeB = len(modesB) -nmodeC = len(modesC) -nmodeD = len(modesD) +nmode_A = len(modes_A) +nmode_B = len(modes_B) +nmode_C = len(modes_C) +nmode_D = len(modes_D) +nmode_R = len(modes_R) ############################### # Create Contraction Descriptor ############################### -# These also work, but require a bit more keystrokes -#modesA = np.asarray(modesA, dtype=np.int32) -#modesB = np.asarray(modesB, dtype=np.int32) -#modesC = np.asarray(modesC, dtype=np.int32) -#modesIn = (modesA.ctypes.data, modesB.ctypes.data, modesC.ctypes.data) -#extentA = np.asarray(extentA, dtype=np.int64) -#extentB = np.asarray(extentB, dtype=np.int64) -#extentC = np.asarray(extentC, dtype=np.int64) -#extentsIn = (extentA.ctypes.data, extentB.ctypes.data, extentC.ctypes.data) - -modesIn = (modesA, modesB, modesC) -extentsIn = (extentA, extentB, extentC) -numModesIn = (nmodeA, nmodeB, nmodeC) - -# strides are optional; if no stride (0) is provided, then cuTensorNet assumes a generalized column-major data layout -stridesIn = (0, 0, 0) - -# compute the alignments -# we hard-code them here because CuPy arrays are at least 256B aligned -alignmentsIn = (256, 256, 256) -alignmentOut = 256 - -# setup tensor network -descNet = cutn.create_network_descriptor(handle, - numInputs, numModesIn, extentsIn, stridesIn, modesIn, alignmentsIn, # inputs - nmodeD, extentD, 0, modesD, alignmentOut, # output +modes_in = (modes_A, modes_B, modes_C, modes_D) +extents_in = (extent_A, extent_B, extent_C, extent_D) +num_modes_in = (nmode_A, nmode_B, nmode_C, nmode_D) + +# Strides are optional; if no stride (0) is provided, then cuTensorNet assumes a generalized column-major data layout +strides_in = (0, 0, 0, 0) + +# Set up the tensor qualifiers for all input tensors +qualifiers_in = np.zeros(num_inputs, dtype=cutn.tensor_qualifiers_dtype) + +# Set up tensor network +desc_net = cutn.create_network_descriptor(handle, + num_inputs, num_modes_in, extents_in, strides_in, modes_in, qualifiers_in, # inputs + nmode_R, extent_R, 0, modes_R, # output data_type, compute_type) if rank == root: @@ -140,38 +131,33 @@ # Choose workspace limit based on available resources ##################################################### -freeMem, totalMem = dev.mem_info -totalMem = comm.allreduce(totalMem, MPI.MIN) -workspaceLimit = int(totalMem * 0.9) +free_mem, total_mem = dev.mem_info +free_mem = comm.allreduce(free_mem, MPI.MIN) +workspace_limit = int(free_mem * 0.9) ############################################## # Find "optimal" contraction order and slicing ############################################## -optimizerConfig = cutn.create_contraction_optimizer_config(handle) -optimizerInfo = cutn.create_contraction_optimizer_info(handle, descNet) - -# Compute the path on all ranks so that we can choose the path with the lowest cost. Note that since this is a tiny -# example with 3 operands, all processes will compute the same globally optimal path. This is not the case for large -# tensor networks. For large networks, hyperoptimization is also beneficial and can be enabled by setting the -# optimizer config attribute cutn.ContractionOptimizerConfigAttribute.HYPER_NUM_SAMPLES. +optimizer_config = cutn.create_contraction_optimizer_config(handle) +optimizer_info = cutn.create_contraction_optimizer_info(handle, desc_net) # Force slicing min_slices_dtype = cutn.contraction_optimizer_config_get_attribute_dtype( cutn.ContractionOptimizerConfigAttribute.SLICER_MIN_SLICES) min_slices_factor = np.asarray((size,), dtype=min_slices_dtype) cutn.contraction_optimizer_config_set_attribute( - handle, optimizerConfig, cutn.ContractionOptimizerConfigAttribute.SLICER_MIN_SLICES, + handle, optimizer_config, cutn.ContractionOptimizerConfigAttribute.SLICER_MIN_SLICES, min_slices_factor.ctypes.data, min_slices_factor.dtype.itemsize) cutn.contraction_optimize( - handle, descNet, optimizerConfig, workspaceLimit, optimizerInfo) + handle, desc_net, optimizer_config, workspace_limit, optimizer_info) flops_dtype = cutn.contraction_optimizer_info_get_attribute_dtype( cutn.ContractionOptimizerInfoAttribute.FLOP_COUNT) flops = np.zeros((1,), dtype=flops_dtype) cutn.contraction_optimizer_info_get_attribute( - handle, optimizerInfo, cutn.ContractionOptimizerInfoAttribute.FLOP_COUNT, + handle, optimizer_info, cutn.ContractionOptimizerInfoAttribute.FLOP_COUNT, flops.ctypes.data, flops.dtype.itemsize) flops = float(flops) @@ -180,9 +166,9 @@ if rank == root: print(f"Process {sender} has the path with the lowest FLOP count {flops}.") -# Get buffer size for optimizerInfo and broadcast it. +# Get buffer size for optimizer_info and broadcast it. if rank == sender: - bufSize = cutn.contraction_optimizer_info_get_packed_size(handle, optimizerInfo) + bufSize = cutn.contraction_optimizer_info_get_packed_size(handle, optimizer_info) else: bufSize = 0 # placeholder bufSize = comm.bcast(bufSize, sender) @@ -190,60 +176,59 @@ # Allocate buffer. buf = np.empty((bufSize,), dtype=np.int8) -# Pack optimizerInfo on sender and broadcast it. +# Pack optimizer_info on sender and broadcast it. if rank == sender: - cutn.contraction_optimizer_info_pack_data(handle, optimizerInfo, buf, bufSize) + cutn.contraction_optimizer_info_pack_data(handle, optimizer_info, buf, bufSize) comm.Bcast(buf, sender) -# Unpack optimizerInfo from buffer. +# Unpack optimizer_info from buffer. if rank != sender: cutn.update_contraction_optimizer_info_from_packed_data( - handle, buf, bufSize, optimizerInfo) + handle, buf, bufSize, optimizer_info) -numSlices_dtype = cutn.contraction_optimizer_info_get_attribute_dtype( +num_slices_dtype = cutn.contraction_optimizer_info_get_attribute_dtype( cutn.ContractionOptimizerInfoAttribute.NUM_SLICES) -numSlices = np.zeros((1,), dtype=numSlices_dtype) +num_slices = np.zeros((1,), dtype=num_slices_dtype) cutn.contraction_optimizer_info_get_attribute( - handle, optimizerInfo, cutn.ContractionOptimizerInfoAttribute.NUM_SLICES, - numSlices.ctypes.data, numSlices.dtype.itemsize) -numSlices = int(numSlices) + handle, optimizer_info, cutn.ContractionOptimizerInfoAttribute.NUM_SLICES, + num_slices.ctypes.data, num_slices.dtype.itemsize) +num_slices = int(num_slices) -assert numSlices > 0 +assert num_slices > 0 # Calculate each process's share of the slices. -procChunk = numSlices / size -extra = numSlices % size -procSliceBegin = rank * procChunk + min(rank, extra) -procSliceEnd = numSlices if rank == size - 1 else (rank + 1) * procChunk + min(rank + 1, extra) +proc_chunk = num_slices / size +extra = num_slices % size +proc_slice_begin = rank * proc_chunk + min(rank, extra) +proc_slice_end = num_slices if rank == size - 1 else (rank + 1) * proc_chunk + min(rank + 1, extra) if rank == root: print("Find an optimized contraction path with cuTensorNet optimizer.") -############################################################# -# Create workspace descriptor, allocate workspace, and set it -############################################################# - -workDesc = cutn.create_workspace_descriptor(handle) -cutn.workspace_compute_sizes(handle, descNet, optimizerInfo, workDesc) -requiredWorkspaceSize = cutn.workspace_get_size( - handle, workDesc, +########################################################### +# Initialize all pair-wise contraction plans (for cuTENSOR) +########################################################### + +work_desc = cutn.create_workspace_descriptor(handle) +cutn.workspace_compute_contraction_sizes(handle, desc_net, optimizer_info, work_desc) +required_workspace_size = cutn.workspace_get_size( + handle, work_desc, cutn.WorksizePref.MIN, cutn.Memspace.DEVICE) -work = cp.cuda.alloc(requiredWorkspaceSize) +work = cp.cuda.alloc(required_workspace_size) cutn.workspace_set( - handle, workDesc, + handle, work_desc, cutn.Memspace.DEVICE, - work.ptr, requiredWorkspaceSize) + work.ptr, required_workspace_size) if rank == root: print("Allocate workspace.") - + ########################################################### # Initialize all pair-wise contraction plans (for cuTENSOR) ########################################################### -plan = cutn.create_contraction_plan( - handle, descNet, optimizerInfo, workDesc) +plan = cutn.create_contraction_plan(handle, desc_net, optimizer_info, work_desc) ################################################################################### # Optional: Auto-tune cuTENSOR's cutensorContractionPlan to pick the fastest kernel @@ -251,49 +236,42 @@ pref = cutn.create_contraction_autotune_preference(handle) -numAutotuningIterations = 5 # may be 0 +num_autotuning_iterations = 5 # may be 0 n_iter_dtype = cutn.contraction_autotune_preference_get_attribute_dtype( cutn.ContractionAutotunePreferenceAttribute.MAX_ITERATIONS) -numAutotuningIterations = np.asarray([numAutotuningIterations], dtype=n_iter_dtype) +num_autotuning_iterations = np.asarray([num_autotuning_iterations], dtype=n_iter_dtype) cutn.contraction_autotune_preference_set_attribute( handle, pref, cutn.ContractionAutotunePreferenceAttribute.MAX_ITERATIONS, - numAutotuningIterations.ctypes.data, numAutotuningIterations.dtype.itemsize) + num_autotuning_iterations.ctypes.data, num_autotuning_iterations.dtype.itemsize) # modify the plan again to find the best pair-wise contractions cutn.contraction_autotune( - handle, plan, rawDataIn_d, D_d.data.ptr, - workDesc, pref, stream.ptr) + handle, plan, raw_data_in_d, R_d.data.ptr, + work_desc, pref, stream.ptr) cutn.destroy_contraction_autotune_preference(pref) -if rank == root: +if rank == root: print("Create a contraction plan for cuTENSOR and optionally auto-tune it.") -##### -# Run -##### +########### +# Execution +########### minTimeCUTENSOR = 1e100 -numRuns = 3 # to get stable perf results +num_runs = 3 # to get stable perf results e1 = cp.cuda.Event() e2 = cp.cuda.Event() +slice_group = cutn.create_slice_group_from_id_range(handle, proc_slice_begin, proc_slice_end, 1) -# Create a cutensornetSliceGroup_t object from a range of slice IDs. -sliceGroup = cutn.create_slice_group_from_id_range(handle, procSliceBegin, procSliceEnd, 1) - -for i in range(numRuns): - dev.synchronize() - - # Contract over the range of slices this process is responsible for. - - # Don't accumulate into output since we use a one-process-per-gpu model. - accumulateOutput = False - +for i in range(num_runs): + # Contract over all slices. + # A user may choose to parallelize over the slices across multiple devices. e1.record() cutn.contract_slices( - handle, plan, rawDataIn_d, D_d.data.ptr, accumulateOutput, - workDesc, sliceGroup, stream.ptr) + handle, plan, raw_data_in_d, R_d.data.ptr, False, + work_desc, slice_group, stream.ptr) e2.record() # Synchronize and measure timing @@ -302,40 +280,53 @@ minTimeCUTENSOR = minTimeCUTENSOR if minTimeCUTENSOR < time else time if rank == root: - print("Contract the network, all slices within the same rank use the same contraction plan.") - print(f"numSlices: {numSlices}") - numSlicesProc = procSliceEnd - procSliceBegin - print(f"numSlices on root process: {numSlicesProc}") - if numSlicesProc > 0: - print(f"{minTimeCUTENSOR * 1000 / numSlicesProc} ms / slice") - -cutn.destroy_slice_group(sliceGroup) -D[...] = cp.asnumpy(D_d).reshape(extentD, order='F') + print("Contract the network, each slice uses the same contraction plan.") + +# free up the workspace +del work + +R[...] = cp.asnumpy(R_d).reshape(extent_R, order='F') # Reduce on root process. if rank == root: - comm.Reduce(MPI.IN_PLACE, D, root=root) + comm.Reduce(MPI.IN_PLACE, R, root=root) else: - comm.Reduce(D, D, root=root) + comm.Reduce(R, R, root=root) # Compute the reference result. if rank == root: - # recall that we set strides to null (0), so the data are in F-contiguous layout - A_d = A_d.reshape(extentA, order='F') - B_d = B_d.reshape(extentB, order='F') - C_d = C_d.reshape(extentC, order='F') - D_d = D_d.reshape(extentD, order='F') - out = cp.einsum("mhkn,ukh,xuy->mxny", A_d, B_d, C_d) - if not cp.allclose(out, D): + # Recall that we set strides to null (0), so the data are in F-contiguous layout + A_d = A_d.reshape(extent_A, order='F') + B_d = B_d.reshape(extent_B, order='F') + C_d = C_d.reshape(extent_C, order='F') + D_d = D_d.reshape(extent_D, order='F') + path, _ = cuquantum.einsum_path("abcdef,bgheij,magfik,lchdjm->kl", A_d, B_d, C_d, D_d) + out = cp.einsum("abcdef,bgheij,magfik,lchdjm->kl", A_d, B_d, C_d, D_d, optimize=path) + + if not cp.allclose(out, R): raise RuntimeError("result is incorrect") print("Check cuTensorNet result against that of cupy.einsum().") ####################################################### +flops_dtype = cutn.contraction_optimizer_info_get_attribute_dtype( + cutn.ContractionOptimizerInfoAttribute.FLOP_COUNT) +flops = np.zeros((1,), dtype=flops_dtype) +cutn.contraction_optimizer_info_get_attribute( + handle, optimizer_info, cutn.ContractionOptimizerInfoAttribute.FLOP_COUNT, + flops.ctypes.data, flops.dtype.itemsize) +flops = float(flops) + +if rank == root: + print(f"num_slices: {num_slices}") + print(f"{minTimeCUTENSOR * 1000 / num_slices} ms / slice") + print(f"{flops / 1e9 / minTimeCUTENSOR} GFLOPS/s") + +cutn.destroy_slice_group(slice_group) cutn.destroy_contraction_plan(plan) -cutn.destroy_contraction_optimizer_info(optimizerInfo) -cutn.destroy_contraction_optimizer_config(optimizerConfig) -cutn.destroy_network_descriptor(descNet) -cutn.destroy_workspace_descriptor(workDesc) +cutn.destroy_contraction_optimizer_info(optimizer_info) +cutn.destroy_contraction_optimizer_config(optimizer_config) +cutn.destroy_network_descriptor(desc_net) +cutn.destroy_workspace_descriptor(work_desc) cutn.destroy(handle) if rank == root: diff --git a/python/samples/cutensornet/tensornet_example_mpi_auto.py b/python/samples/cutensornet/tensornet_example_mpi_auto.py new file mode 100755 index 0000000..1365d44 --- /dev/null +++ b/python/samples/cutensornet/tensornet_example_mpi_auto.py @@ -0,0 +1,293 @@ +# Copyright (c) 2021-2022, NVIDIA CORPORATION & AFFILIATES +# +# SPDX-License-Identifier: BSD-3-Clause + +import cupy as cp +import numpy as np +from mpi4py import MPI + +import cuquantum +from cuquantum import cutensornet as cutn + + +root = 0 +comm = MPI.COMM_WORLD +rank, size = comm.Get_rank(), comm.Get_size() +if rank == root: + print("*** Printing is done only from the root process to prevent jumbled messages ***") + print(f"The number of processes is {size}") + +num_devices = cp.cuda.runtime.getDeviceCount() +device_id = rank % num_devices +dev = cp.cuda.Device(device_id) +dev.use() + +props = cp.cuda.runtime.getDeviceProperties(dev.id) +if rank == root: + print("cuTensorNet-vers:", cutn.get_version()) + print("===== root process 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("========================") + +###################################################################################### +# Computing: R_{k,l} = A_{a,b,c,d,e,f} B_{b,g,h,e,i,j} C_{m,a,g,f,i,k} D_{l,c,h,d,j,m} +###################################################################################### + +if rank == root: + print("Include headers and define data types.") + +data_type = cuquantum.cudaDataType.CUDA_R_32F +compute_type = cuquantum.ComputeType.COMPUTE_32F +num_inputs = 4 + +# Create an array of modes +modes_A = [ord(c) for c in ('a','b','c','d','e','f')] +modes_B = [ord(c) for c in ('b','g','h','e','i','j')] +modes_C = [ord(c) for c in ('m','a','g','f','i','k')] +modes_D = [ord(c) for c in ('l','c','h','d','j','m')] +modes_R = [ord(c) for c in ('k','l')] + +# Create an array of extents (shapes) for each tensor +dim = 8 +extent_A = (dim,) * 6 +extent_B = (dim,) * 6 +extent_C = (dim,) * 6 +extent_D = (dim,) * 6 +extent_R = (dim,) * 2 + +if rank == root: + print("Define network, modes, and extents.") + +################# +# Initialize data +################# + +if rank == root: + A = np.random.random(np.prod(extent_A)).astype(np.float32) + B = np.random.random(np.prod(extent_B)).astype(np.float32) + C = np.random.random(np.prod(extent_C)).astype(np.float32) + D = np.random.random(np.prod(extent_D)).astype(np.float32) +else: + A = np.empty(np.prod(extent_A), dtype=np.float32) + B = np.empty(np.prod(extent_B), dtype=np.float32) + C = np.empty(np.prod(extent_C), dtype=np.float32) + D = np.empty(np.prod(extent_D), dtype=np.float32) + +comm.Bcast(A, root) +comm.Bcast(B, root) +comm.Bcast(C, root) +comm.Bcast(D, root) + +A_d = cp.asarray(A) +B_d = cp.asarray(B) +C_d = cp.asarray(C) +D_d = cp.asarray(D) +R_d = cp.empty(np.prod(extent_R), dtype=np.float32) +raw_data_in_d = (A_d.data.ptr, B_d.data.ptr, C_d.data.ptr, D_d.data.ptr) + +############# +# cuTensorNet +############# + +stream = cp.cuda.Stream() +handle = cutn.create() + +nmode_A = len(modes_A) +nmode_B = len(modes_B) +nmode_C = len(modes_C) +nmode_D = len(modes_D) +nmode_R = len(modes_R) + +############################### +# Create Contraction Descriptor +############################### + +modes_in = (modes_A, modes_B, modes_C, modes_D) +extents_in = (extent_A, extent_B, extent_C, extent_D) +num_modes_in = (nmode_A, nmode_B, nmode_C, nmode_D) + +# Strides are optional; if no stride (0) is provided, then cuTensorNet assumes a generalized column-major data layout +strides_in = (0, 0, 0, 0) + +# Set up the tensor qualifiers for all input tensors +qualifiers_in = np.zeros(num_inputs, dtype=cutn.tensor_qualifiers_dtype) + +# Set up tensor network +desc_net = cutn.create_network_descriptor(handle, + num_inputs, num_modes_in, extents_in, strides_in, modes_in, qualifiers_in, # inputs + nmode_R, extent_R, 0, modes_R, # output + data_type, compute_type) + +if rank == root: + print("Initialize the cuTensorNet library and create a network descriptor.") + +##################################################### +# Choose workspace limit based on available resources +##################################################### + +free_mem, total_mem = dev.mem_info +free_mem = comm.allreduce(free_mem, MPI.MIN) +workspace_limit = int(free_mem * 0.9) + +cutn_comm = comm.Dup() +cutn.distributed_reset_configuration(handle, MPI._addressof(cutn_comm), MPI._sizeof(cutn_comm)) +if rank == root: + print("Reset distributed MPI configuration") + +############################################## +# Find "optimal" contraction order and slicing +############################################## + +optimizer_config = cutn.create_contraction_optimizer_config(handle) +optimizer_info = cutn.create_contraction_optimizer_info(handle, desc_net) + +# Force slicing +min_slices_dtype = cutn.contraction_optimizer_config_get_attribute_dtype( + cutn.ContractionOptimizerConfigAttribute.SLICER_MIN_SLICES) +min_slices_factor = np.asarray((size,), dtype=min_slices_dtype) +cutn.contraction_optimizer_config_set_attribute( + handle, optimizer_config, cutn.ContractionOptimizerConfigAttribute.SLICER_MIN_SLICES, + min_slices_factor.ctypes.data, min_slices_factor.dtype.itemsize) + +cutn.contraction_optimize( + handle, desc_net, optimizer_config, workspace_limit, optimizer_info) + + +num_slices_dtype = cutn.contraction_optimizer_info_get_attribute_dtype( + cutn.ContractionOptimizerInfoAttribute.NUM_SLICES) +num_slices = np.zeros((1,), dtype=num_slices_dtype) +cutn.contraction_optimizer_info_get_attribute( + handle, optimizer_info, cutn.ContractionOptimizerInfoAttribute.NUM_SLICES, + num_slices.ctypes.data, num_slices.dtype.itemsize) +num_slices = int(num_slices) + +assert num_slices > 0 + +if rank == root: + print("Find an optimized contraction path with cuTensorNet optimizer.") + +########################################################### +# Initialize all pair-wise contraction plans (for cuTENSOR) +########################################################### + +work_desc = cutn.create_workspace_descriptor(handle) +cutn.workspace_compute_contraction_sizes(handle, desc_net, optimizer_info, work_desc) +required_workspace_size = cutn.workspace_get_size( + handle, work_desc, + cutn.WorksizePref.MIN, + cutn.Memspace.DEVICE) +work = cp.cuda.alloc(required_workspace_size) +cutn.workspace_set( + handle, work_desc, + cutn.Memspace.DEVICE, + work.ptr, required_workspace_size) + +if rank == root: + print("Allocate workspace.") + +########################################################### +# Initialize all pair-wise contraction plans (for cuTENSOR) +########################################################### + +plan = cutn.create_contraction_plan(handle, desc_net, optimizer_info, work_desc) + +################################################################################### +# Optional: Auto-tune cuTENSOR's cutensorContractionPlan to pick the fastest kernel +################################################################################### + +pref = cutn.create_contraction_autotune_preference(handle) + +num_autotuning_iterations = 5 # may be 0 +n_iter_dtype = cutn.contraction_autotune_preference_get_attribute_dtype( + cutn.ContractionAutotunePreferenceAttribute.MAX_ITERATIONS) +num_autotuning_iterations = np.asarray([num_autotuning_iterations], dtype=n_iter_dtype) +cutn.contraction_autotune_preference_set_attribute( + handle, pref, + cutn.ContractionAutotunePreferenceAttribute.MAX_ITERATIONS, + num_autotuning_iterations.ctypes.data, num_autotuning_iterations.dtype.itemsize) + +# modify the plan again to find the best pair-wise contractions +cutn.contraction_autotune( + handle, plan, raw_data_in_d, R_d.data.ptr, + work_desc, pref, stream.ptr) + +cutn.destroy_contraction_autotune_preference(pref) + +if rank == root: + print("Create a contraction plan for cuTENSOR and optionally auto-tune it.") + +########### +# Execution +########### + +minTimeCUTENSOR = 1e100 +num_runs = 3 # to get stable perf results +e1 = cp.cuda.Event() +e2 = cp.cuda.Event() +slice_group = cutn.create_slice_group_from_id_range(handle, 0, num_slices, 1) + +for i in range(num_runs): + # Contract over all slices. + # A user may choose to parallelize over the slices across multiple devices. + e1.record() + cutn.contract_slices( + handle, plan, raw_data_in_d, R_d.data.ptr, False, + work_desc, slice_group, stream.ptr) + e2.record() + + # Synchronize and measure timing + e2.synchronize() + time = cp.cuda.get_elapsed_time(e1, e2) / 1000 # ms -> s + minTimeCUTENSOR = minTimeCUTENSOR if minTimeCUTENSOR < time else time + +if rank == root: + print("Contract the network, each slice uses the same contraction plan.") + +# free up the workspace +del work + +# Compute the reference result. +if rank == root: + # recall that we set strides to null (0), so the data are in F-contiguous layout + A_d = A_d.reshape(extent_A, order='F') + B_d = B_d.reshape(extent_B, order='F') + C_d = C_d.reshape(extent_C, order='F') + D_d = D_d.reshape(extent_D, order='F') + R_d = R_d.reshape(extent_R, order='F') + path, _ = cuquantum.einsum_path("abcdef,bgheij,magfik,lchdjm->kl", A_d, B_d, C_d, D_d) + out = cp.einsum("abcdef,bgheij,magfik,lchdjm->kl", A_d, B_d, C_d, D_d, optimize=path) + + if not cp.allclose(out, R_d): + raise RuntimeError("result is incorrect") + print("Check cuTensorNet result against that of cupy.einsum().") + +####################################################### + +flops_dtype = cutn.contraction_optimizer_info_get_attribute_dtype( + cutn.ContractionOptimizerInfoAttribute.FLOP_COUNT) +flops = np.zeros((1,), dtype=flops_dtype) +cutn.contraction_optimizer_info_get_attribute( + handle, optimizer_info, cutn.ContractionOptimizerInfoAttribute.FLOP_COUNT, + flops.ctypes.data, flops.dtype.itemsize) +flops = float(flops) + +if rank == root: + print(f"num_slices: {num_slices}") + print(f"{minTimeCUTENSOR * 1000 / num_slices} ms / slice") + print(f"{flops / 1e9 / minTimeCUTENSOR} GFLOPS/s") + +cutn.destroy_slice_group(slice_group) +cutn.destroy_contraction_plan(plan) +cutn.destroy_contraction_optimizer_info(optimizer_info) +cutn.destroy_contraction_optimizer_config(optimizer_config) +cutn.destroy_network_descriptor(desc_net) +cutn.destroy_workspace_descriptor(work_desc) +cutn.destroy(handle) + +if rank == root: + print("Free resource and exit.") diff --git a/python/setup.py b/python/setup.py index 90700a7..2592164 100644 --- a/python/setup.py +++ b/python/setup.py @@ -3,217 +3,82 @@ # SPDX-License-Identifier: BSD-3-Clause import os -import site import sys -from packaging.version import Version -from setuptools import setup, Extension, find_packages from Cython.Build import cythonize +from setuptools import setup, Extension, find_packages - -# Get __version__ variable +# this is tricky: sys.path gets overwritten at different stages of the build +# flow, so we need to hack sys.path ourselves... source_root = os.path.abspath(os.path.dirname(__file__)) -with open(os.path.join(source_root, 'cuquantum', '_version.py')) as f: - exec(f.read()) +sys.path.append(os.path.join(source_root, 'builder')) +import utils # this is builder.utils # Use README for the project long description -with open("README.md") as f: +with open(os.path.join(source_root, "README.md")) as f: long_description = f.read() -# 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 Python patch number here -cuqnt_py_ver = Version(__version__) -cuqnt_ver_major_minor = f"{cuqnt_py_ver.major}.{cuqnt_py_ver.minor}" - - -# search order: -# 1. installed "cuquantum" package -# 2. env var -for path in site.getsitepackages(): - path = os.path.join(path, 'cuquantum') - if os.path.isdir(path): - cuquantum_root = path - using_cuquantum_wheel = True - break -else: - cuquantum_root = os.environ.get('CUQUANTUM_ROOT') - using_cuquantum_wheel = False - - -# We allow setting CUSTATEVEC_ROOT and CUTENSORNET_ROOT separately for the ease -# of development, but users are encouraged to either install cuquantum from PyPI -# or conda, or set CUQUANTUM_ROOT to the existing installation. -try: - custatevec_root = os.environ['CUSTATEVEC_ROOT'] - using_cuquantum_wheel = False -except KeyError as e: - if cuquantum_root is None: - raise RuntimeError('cuStateVec is not found, please install "cuquantum" ' - 'or set $CUQUANTUM_ROOT') from e - else: - custatevec_root = cuquantum_root -try: - cutensornet_root = os.environ['CUTENSORNET_ROOT'] - using_cuquantum_wheel = False -except KeyError as e: - if cuquantum_root is None: - raise RuntimeError('cuTensorNet is not found, please install "cuquantum" ' - 'or set $CUQUANTUM_ROOT') from e - else: - cutensornet_root = cuquantum_root - - -# search order: -# 1. installed "cutensor" package -# 2. env var -for path in site.getsitepackages(): - path = os.path.join(path, 'cutensor') - if os.path.isdir(path): - cutensor_root = path - assert using_cuquantum_wheel # if this raises, the env is corrupted - break -else: - cutensor_root = os.environ.get('CUTENSOR_ROOT') - assert not using_cuquantum_wheel -if cutensor_root is None: - raise RuntimeError('cuTENSOR is not found, please install "cutensor" ' - 'or set $CUTENSOR_ROOT') - - -# We can't assume users to have CTK installed via pip, so we really need this... -# TODO(leofang): try /usr/local/cuda? -try: - cuda_path = os.environ['CUDA_PATH'] -except KeyError as e: - raise RuntimeError('CUDA is not found, please set $CUDA_PATH') from e +# Get test requirements +with open(os.path.join(source_root, "tests/requirements.txt")) as f: + tests_require = f.read().split('\n') -# TODO: use setup.cfg and/or pyproject.toml -setup_requires = [ - 'Cython>=0.29.22,<3', - 'packaging', - ] +# Runtime dependencies +# - cuTENSOR version is constrained in the cutensornet-cuXX package, so we don't +# need to list it install_requires = [ 'numpy', # 'cupy', # TODO: use "cupy-wheel" once it's stablized, see https://github.com/cupy/cupy/issues/6688 # 'torch', # <-- PyTorch is optional; also, the PyPI version does not support GPU... + f'custatevec-cu{utils.cuda_major_ver}~=1.1', # ">=1.1.0,<2" + f'cutensornet-cu{utils.cuda_major_ver}~=2.0', # ">=2.0.0,<3" ] -ignore_cuquantum_dep = bool(os.environ.get('CUQUANTUM_IGNORE_SOLVER', False)) -if not ignore_cuquantum_dep: - assert using_cuquantum_wheel # if this raises, the env is corrupted - # - cuTENSOR version is constrained in the cuquantum package, so we don't - # need to list it - # - here we assume no API breaking across releases, if there's any we must - # bump the lowest supported version; we can't cap the highest supported - # version as we don't use semantic versioning, unfortunately... - setup_requires.append(f'cuquantum>={cuqnt_ver_major_minor}.*') - install_requires.append(f'cuquantum>={cuqnt_ver_major_minor}.*') - - -def check_cuda_version(): - try: - # We cannot do a dlopen and call cudaRuntimeGetVersion, because it - # requires GPUs. We also do not want to rely on the compiler utility - # provided in distutils (deprecated) or setuptools, as this is a very - # simple string parsing task. - # TODO: switch to cudaRuntimeGetVersion once it's fixed (nvbugs 3624208) - cuda_h = os.path.join(cuda_path, 'include', 'cuda.h') - with open(cuda_h, 'r') as f: - cuda_h = f.read().split('\n') - for line in cuda_h: - if "#define CUDA_VERSION" in line: - ver = int(line.split()[-1]) - break - else: - raise RuntimeError("cannot parse CUDA_VERSION") - except: - raise - else: - # 11020 -> "11.2" - return str(ver // 1000) + '.' + str((ver % 100) // 10) -cuda_ver = check_cuda_version() -if cuda_ver in ('10.2', '11.0'): - cutensor_ver = cuda_ver -elif '11.0' < cuda_ver < '12.0': - cutensor_ver = '11' +# Note: the extension attributes are overwritten in build_extension() +ext_modules = [ + Extension( + "cuquantum.custatevec.custatevec", + sources=["cuquantum/custatevec/custatevec.pyx"], + ), + Extension( + "cuquantum.cutensornet.cutensornet", + sources=["cuquantum/cutensornet/cutensornet.pyx"], + ), + Extension( + "cuquantum.utils", + sources=["cuquantum/utils.pyx"], + include_dirs=[os.path.join(utils.cuda_path, 'include')], + ), +] + + +cmdclass = { + 'build_ext': utils.build_ext, + 'bdist_wheel': utils.bdist_wheel, +} + +if utils.cuda_major_ver == '11': + cuda_classifier = [ + "Environment :: GPU :: NVIDIA CUDA :: 11.0", + "Environment :: GPU :: NVIDIA CUDA :: 11.1", + "Environment :: GPU :: NVIDIA CUDA :: 11.2", + "Environment :: GPU :: NVIDIA CUDA :: 11.3", + "Environment :: GPU :: NVIDIA CUDA :: 11.4", + "Environment :: GPU :: NVIDIA CUDA :: 11.5", + "Environment :: GPU :: NVIDIA CUDA :: 11.6", + "Environment :: GPU :: NVIDIA CUDA :: 11.7", + "Environment :: GPU :: NVIDIA CUDA :: 11.8", + ] else: - raise RuntimeError(f"Unsupported CUDA version: {cuda_ver}") - - -def prepare_libs_and_rpaths(): - global cusv_lib_dir, cutn_lib_dir - # we include both lib64 and lib to accommodate all possible sources - cusv_lib_dir = [os.path.join(custatevec_root, 'lib'), - os.path.join(custatevec_root, 'lib64')] - cutn_lib_dir = [os.path.join(cutensornet_root, 'lib'), - os.path.join(cutensornet_root, 'lib64'), - os.path.join(cutensor_root, 'lib', cutensor_ver)] - - global cusv_lib, cutn_lib, extra_linker_flags - if using_cuquantum_wheel: - cusv_lib = [':libcustatevec.so.1'] - cutn_lib = [':libcutensornet.so.1', ':libcutensor.so.1'] - # The rpaths must be adjusted given the following full-wheel installation: - # cuquantum-python: site-packages/cuquantum/{custatevec, cutensornet}/ [=$ORIGIN] - # cusv & cutn: site-packages/cuquantum/lib/ - # cutensor: site-packages/cutensor/lib/CUDA_VER/ - ldflag = "-Wl,--disable-new-dtags," - ldflag += "-rpath,$ORIGIN/../lib," - ldflag += f"-rpath,$ORIGIN/../../cutensor/lib/{cutensor_ver}" - extra_linker_flags = [ldflag] - else: - cusv_lib = ['custatevec'] - cutn_lib = ['cutensornet', 'cutensor'] - extra_linker_flags = [] - - -prepare_libs_and_rpaths() -print("\n****************************************************************") -print("CUDA version:", cuda_ver) -print("CUDA path:", cuda_path) -print("cuStateVec path:", custatevec_root) -print("cuTensorNet path:", cutensornet_root) -print("cuTENSOR path:", cutensor_root) -print("****************************************************************\n") - - -custatevec = Extension( - "cuquantum.custatevec.custatevec", - sources=["cuquantum/custatevec/custatevec.pyx"], - include_dirs=[os.path.join(cuda_path, 'include'), - os.path.join(custatevec_root, 'include')], - library_dirs=cusv_lib_dir, - libraries=cusv_lib, - extra_link_args=extra_linker_flags, -) - - -cutensornet = Extension( - "cuquantum.cutensornet.cutensornet", - sources=["cuquantum/cutensornet/cutensornet.pyx"], - include_dirs=[os.path.join(cuda_path, 'include'), - os.path.join(cutensornet_root, 'include')], - library_dirs=cutn_lib_dir, - libraries=cutn_lib, - extra_link_args=extra_linker_flags, -) - - -utils = Extension( - "cuquantum.utils", - sources=["cuquantum/utils.pyx"], - include_dirs=[os.path.join(cuda_path, 'include')], -) - + cuda_classifier = None +# TODO: move static metadata to pyproject.toml setup( - name="cuquantum-python", - version=__version__, + name=f"cuquantum-python-cu{utils.cuda_major_ver}", + version=utils.cuqnt_py_ver, description="NVIDIA cuQuantum Python", long_description=long_description, long_description_content_type="text/markdown", @@ -234,32 +99,15 @@ def prepare_libs_and_rpaths(): "Programming Language :: Python :: 3.10", "Programming Language :: Python :: Implementation :: CPython", "Environment :: GPU :: NVIDIA CUDA", - "Environment :: GPU :: NVIDIA CUDA :: 11.0", - "Environment :: GPU :: NVIDIA CUDA :: 11.1", - "Environment :: GPU :: NVIDIA CUDA :: 11.2", - "Environment :: GPU :: NVIDIA CUDA :: 11.3", - "Environment :: GPU :: NVIDIA CUDA :: 11.4", - "Environment :: GPU :: NVIDIA CUDA :: 11.5", - "Environment :: GPU :: NVIDIA CUDA :: 11.6", - "Environment :: GPU :: NVIDIA CUDA :: 11.7", - ], - ext_modules=cythonize([custatevec, cutensornet, utils,], + ] + cuda_classifier, + ext_modules=cythonize(ext_modules, verbose=True, language_level=3, compiler_directives={'embedsignature': True}), packages=find_packages(include=['cuquantum', 'cuquantum.*']), package_data={"": ["*.pxd", "*.pyx", "*.py"],}, zip_safe=False, python_requires='>=3.8', - setup_requires=setup_requires, install_requires=install_requires, - tests_require=install_requires + [ - # pytest < 6.2 is slow in collecting tests - 'pytest>=6.2', - 'opt_einsum', - # optional test deps - #'cffi>=1.0.0', - #'nbmake>=1.3.0', # for testing notebooks - #'cirq>=0.6.0', - #'qiskit>=0.24.0', - ] + tests_require=install_requires+tests_require, + cmdclass=cmdclass, ) diff --git a/python/tests/conftest.py b/python/tests/conftest.py new file mode 100644 index 0000000..9bddfdf --- /dev/null +++ b/python/tests/conftest.py @@ -0,0 +1,29 @@ +# Copyright (c) 2021-2022, NVIDIA CORPORATION & AFFILIATES +# +# SPDX-License-Identifier: BSD-3-Clause + +# The following configs are needed to deselect/ignore collected tests for +# various reasons, see pytest-dev/pytest#3730. In particular, this strategy +# is borrowed from https://github.com/pytest-dev/pytest/issues/3730#issuecomment-567142496. + + +def pytest_configure(config): + config.addinivalue_line( + "markers", "uncollect_if(*, func): function to unselect tests from parametrization" + ) + + +def pytest_collection_modifyitems(config, items): + removed = [] + kept = [] + for item in items: + 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) + if removed: + config.hook.pytest_deselected(items=removed) + items[:] = kept diff --git a/python/tests/cuquantum_tests/__init__.py b/python/tests/cuquantum_tests/__init__.py index c08f9b5..e589952 100644 --- a/python/tests/cuquantum_tests/__init__.py +++ b/python/tests/cuquantum_tests/__init__.py @@ -1,3 +1,270 @@ # Copyright (c) 2021-2022, NVIDIA CORPORATION & AFFILIATES # # SPDX-License-Identifier: BSD-3-Clause + +import os +import sys +import tempfile + +try: + import cffi +except ImportError: + cffi = None +import cupy +import numpy +import pytest + +from cuquantum import ComputeType, cudaDataType + + +if cffi: + # if the Python binding is not installed in the editable mode (pip install + # -e .), the cffi tests would fail as the modules cannot be imported + sys.path.append(os.getcwd()) + + +dtype_to_data_type = { + numpy.float16: cudaDataType.CUDA_R_16F, + numpy.float32: cudaDataType.CUDA_R_32F, + numpy.float64: cudaDataType.CUDA_R_64F, + numpy.complex64: cudaDataType.CUDA_C_32F, + numpy.complex128: cudaDataType.CUDA_C_64F, +} + + +dtype_to_compute_type = { + numpy.float16: ComputeType.COMPUTE_16F, + numpy.float32: ComputeType.COMPUTE_32F, + numpy.float64: ComputeType.COMPUTE_64F, + numpy.complex64: ComputeType.COMPUTE_32F, + numpy.complex128: ComputeType.COMPUTE_64F, +} + + +# we don't wanna recompile for every test case... +_cffi_mod1 = None +_cffi_mod2 = None + +def _can_use_cffi(): + if cffi is None or os.environ.get('CUDA_PATH') is None: + return False + else: + return True + + +class MemoryResourceFactory: + + def __init__(self, source): + self.source = source + + def get_dev_mem_handler(self): + if self.source == "py-callable": + return (*self._get_cuda_callable(), self.source) + elif self.source == "cffi": + # ctx is not needed, so set to NULL + return (0, *self._get_functor_address(), self.source) + elif self.source == "cffi_struct": + return self._get_handler_address() + # TODO: add more different memory sources + else: + raise NotImplementedError + + def _get_cuda_callable(self): + def alloc(size, stream): + return cupy.cuda.runtime.mallocAsync(size, stream) + + def free(ptr, size, stream): + cupy.cuda.runtime.freeAsync(ptr, stream) + + return alloc, free + + def _get_functor_address(self): + if not _can_use_cffi(): + raise RuntimeError + + global _cffi_mod1 + if _cffi_mod1 is None: + import importlib + mod_name = f"cuquantum_test_{self.source}" + ffi = cffi.FFI() + ffi.set_source(mod_name, """ + #include + + // cffi limitation: we can't use the actual type cudaStream_t because + // it's considered an "incomplete" type and we can't get the functor + // address by doing so... + + int my_alloc(void* ctx, void** ptr, size_t size, void* stream) { + return (int)cudaMallocAsync(ptr, size, stream); + } + + int my_free(void* ctx, void* ptr, size_t size, void* stream) { + return (int)cudaFreeAsync(ptr, stream); + } + """, + include_dirs=[os.environ['CUDA_PATH']+'/include'], + library_dirs=[os.environ['CUDA_PATH']+'/lib64'], + libraries=['cudart'], + ) + ffi.cdef(""" + int my_alloc(void* ctx, void** ptr, size_t size, void* stream); + int my_free(void* ctx, void* ptr, size_t size, void* stream); + """) + ffi.compile(verbose=True) + self.ffi = ffi + _cffi_mod1 = importlib.import_module(mod_name) + self.ffi_mod = _cffi_mod1 + + alloc_addr = self._get_address("my_alloc") + free_addr = self._get_address("my_free") + return alloc_addr, free_addr + + def _get_handler_address(self): + if not _can_use_cffi(): + raise RuntimeError + + global _cffi_mod2 + if _cffi_mod2 is None: + import importlib + mod_name = f"cuquantum_test_{self.source}" + ffi = cffi.FFI() + ffi.set_source(mod_name, """ + #include + + // cffi limitation: we can't use the actual type cudaStream_t because + // it's considered an "incomplete" type and we can't get the functor + // address by doing so... + + int my_alloc(void* ctx, void** ptr, size_t size, void* stream) { + return (int)cudaMallocAsync(ptr, size, stream); + } + + int my_free(void* ctx, void* ptr, size_t size, void* stream) { + return (int)cudaFreeAsync(ptr, stream); + } + + typedef struct { + void* ctx; + int (*device_alloc)(void* ctx, void** ptr, size_t size, void* stream); + int (*device_free)(void* ctx, void* ptr, size_t size, void* stream); + char name[64]; + } myHandler; + + myHandler* init_myHandler(myHandler* h, const char* name) { + h->ctx = NULL; + h->device_alloc = my_alloc; + h->device_free = my_free; + memcpy(h->name, name, 64); + return h; + } + """, + include_dirs=[os.environ['CUDA_PATH']+'/include'], + library_dirs=[os.environ['CUDA_PATH']+'/lib64'], + libraries=['cudart'], + ) + ffi.cdef(""" + typedef struct { + ...; + } myHandler; + + myHandler* init_myHandler(myHandler* h, const char* name); + """) + ffi.compile(verbose=True) + self.ffi = ffi + _cffi_mod2 = importlib.import_module(mod_name) + self.ffi_mod = _cffi_mod2 + + h = self.handler = self.ffi_mod.ffi.new("myHandler*") + self.ffi_mod.lib.init_myHandler(h, self.source.encode()) + return self._get_address(h) + + def _get_address(self, func_name_or_ptr): + if isinstance(func_name_or_ptr, str): + func_name = func_name_or_ptr + data = str(self.ffi_mod.ffi.addressof(self.ffi_mod.lib, func_name)) + else: + ptr = func_name_or_ptr # ptr to struct + data = str(self.ffi_mod.ffi.addressof(ptr[0])) + # data has this format: "" + return int(data.split()[-1][:-1], base=16) + + +class MemHandlerTestBase: + + mod = None + prefix = None + error = None + + def _test_set_get_device_mem_handler(self, source, handle): + if (isinstance(source, str) and source.startswith('cffi') + and not _can_use_cffi()): + pytest.skip("cannot run cffi tests") + + if source is not None: + mr = MemoryResourceFactory(source) + handler = mr.get_dev_mem_handler() + self.mod.set_device_mem_handler(handle, handler) + # round-trip test + queried_handler = self.mod.get_device_mem_handler(handle) + if source == 'cffi_struct': + # I'm lazy, otherwise I'd also fetch the functor addresses here... + assert queried_handler[0] == 0 # ctx is NULL + assert queried_handler[-1] == source + else: + assert queried_handler == handler + else: + with pytest.raises(self.error) as e: + queried_handler = self.mod.get_device_mem_handler(handle) + assert f'{self.prefix.upper()}_STATUS_NO_DEVICE_ALLOCATOR' in str(e.value) + + +class LoggerTestBase: + + mod = None + prefix = None + + def test_logger_set_level(self): + self.mod.logger_set_level(6) # on + self.mod.logger_set_level(0) # off + + def test_logger_set_mask(self): + self.mod.logger_set_mask(16) # should not raise + + def test_logger_set_callback_data(self): + # we also test logger_open_file() here to avoid polluting stdout + + def callback(level, name, message, my_data, is_ok=False): + log = f"{level}, {name}, {message} (is_ok={is_ok}) -> logged\n" + my_data.append(log) + + handle = None + my_data = [] + is_ok = True + + with tempfile.TemporaryDirectory() as temp: + file_name = os.path.join(temp, f"{self.prefix}_test") + self.mod.logger_open_file(file_name) + self.mod.logger_set_callback_data(callback, my_data, is_ok=is_ok) + self.mod.logger_set_level(6) + + try: + handle = self.mod.create() + self.mod.destroy(handle) + except: + if handle: + self.mod.destroy(handle) + raise + finally: + self.mod.logger_force_disable() # to not affect the rest of tests + + with open(file_name) as f: + log_from_f = f.read() + + # check the log file + assert f'[{self.prefix}Create]' in log_from_f + assert f'[{self.prefix}Destroy]' in log_from_f + + # check the captured data (note we log 2 APIs) + log = ''.join(my_data) + assert log.count("-> logged") >= 2 + assert log.count("is_ok=True") >= 2 diff --git a/python/tests/cuquantum_tests/custatevec_tests/test_custatevec.py b/python/tests/cuquantum_tests/custatevec_tests/test_custatevec.py index b85ae20..abd6b87 100644 --- a/python/tests/cuquantum_tests/custatevec_tests/test_custatevec.py +++ b/python/tests/cuquantum_tests/custatevec_tests/test_custatevec.py @@ -3,14 +3,7 @@ # SPDX-License-Identifier: BSD-3-Clause import copy -import os -import sys -import tempfile - -try: - import cffi -except ImportError: - cffi = None + import cupy from cupy import testing import numpy @@ -20,6 +13,9 @@ from cuquantum import ComputeType, cudaDataType from cuquantum import custatevec as cusv +from .. import (_can_use_cffi, dtype_to_compute_type, dtype_to_data_type, + MemHandlerTestBase, MemoryResourceFactory, LoggerTestBase) + ################################################################### # @@ -30,23 +26,6 @@ # ################################################################### -if cffi: - # if the Python binding is not installed in the editable mode (pip install - # -e .), the cffi tests would fail as the modules cannot be imported - sys.path.append(os.getcwd()) - -dtype_to_data_type = { - numpy.dtype(numpy.complex64): cudaDataType.CUDA_C_32F, - numpy.dtype(numpy.complex128): cudaDataType.CUDA_C_64F, -} - - -dtype_to_compute_type = { - numpy.dtype(numpy.complex64): ComputeType.COMPUTE_32F, - numpy.dtype(numpy.complex128): ComputeType.COMPUTE_64F, -} - - @pytest.fixture() def handle(): h = cusv.create() @@ -192,155 +171,6 @@ def test_get_property(self): cuquantum.libraryPropertyType.PATCH_LEVEL) -# we don't wanna recompile for every test case... -_cffi_mod1 = None -_cffi_mod2 = None - -def _can_use_cffi(): - if cffi is None or os.environ.get('CUDA_PATH') is None: - return False - else: - return True - - -class MemoryResourceFactory: - - def __init__(self, source, name=None): - self.source = source - self.name = source if name is None else name - - def get_dev_mem_handler(self): - if self.source == "py-callable": - return (*self._get_cuda_callable(), self.name) - elif self.source == "cffi": - # ctx is not needed, so set to NULL - return (0, *self._get_functor_address(), self.name) - elif self.source == "cffi_struct": - return self._get_handler_address() - # TODO: add more different memory sources - else: - raise NotImplementedError - - def _get_cuda_callable(self): - def alloc(size, stream): - return cupy.cuda.runtime.mallocAsync(size, stream) - - def free(ptr, size, stream): - cupy.cuda.runtime.freeAsync(ptr, stream) - - return alloc, free - - def _get_functor_address(self): - if not _can_use_cffi(): - raise RuntimeError - - global _cffi_mod1 - if _cffi_mod1 is None: - import importlib - mod_name = f"cusv_test_{self.source}" - ffi = cffi.FFI() - ffi.set_source(mod_name, """ - #include - - // cffi limitation: we can't use the actual type cudaStream_t because - // it's considered an "incomplete" type and we can't get the functor - // address by doing so... - - int my_alloc(void* ctx, void** ptr, size_t size, void* stream) { - return (int)cudaMallocAsync(ptr, size, stream); - } - - int my_free(void* ctx, void* ptr, size_t size, void* stream) { - return (int)cudaFreeAsync(ptr, stream); - } - """, - include_dirs=[os.environ['CUDA_PATH']+'/include'], - library_dirs=[os.environ['CUDA_PATH']+'/lib64'], - libraries=['cudart'], - ) - ffi.cdef(""" - int my_alloc(void* ctx, void** ptr, size_t size, void* stream); - int my_free(void* ctx, void* ptr, size_t size, void* stream); - """) - ffi.compile(verbose=True) - self.ffi = ffi - _cffi_mod1 = importlib.import_module(mod_name) - self.ffi_mod = _cffi_mod1 - - alloc_addr = self._get_address("my_alloc") - free_addr = self._get_address("my_free") - return alloc_addr, free_addr - - def _get_handler_address(self): - if not _can_use_cffi(): - raise RuntimeError - - global _cffi_mod2 - if _cffi_mod2 is None: - import importlib - mod_name = f"cusv_test_{self.source}" - ffi = cffi.FFI() - ffi.set_source(mod_name, """ - #include - - // cffi limitation: we can't use the actual type cudaStream_t because - // it's considered an "incomplete" type and we can't get the functor - // address by doing so... - - int my_alloc(void* ctx, void** ptr, size_t size, void* stream) { - return (int)cudaMallocAsync(ptr, size, stream); - } - - int my_free(void* ctx, void* ptr, size_t size, void* stream) { - return (int)cudaFreeAsync(ptr, stream); - } - - typedef struct { - void* ctx; - int (*device_alloc)(void* ctx, void** ptr, size_t size, void* stream); - int (*device_free)(void* ctx, void* ptr, size_t size, void* stream); - char name[64]; - } myHandler; - - myHandler* init_myHandler(myHandler* h, const char* name) { - h->ctx = NULL; - h->device_alloc = my_alloc; - h->device_free = my_free; - memcpy(h->name, name, 64); - return h; - } - """, - include_dirs=[os.environ['CUDA_PATH']+'/include'], - library_dirs=[os.environ['CUDA_PATH']+'/lib64'], - libraries=['cudart'], - ) - ffi.cdef(""" - typedef struct { - ...; - } myHandler; - - myHandler* init_myHandler(myHandler* h, const char* name); - """) - ffi.compile(verbose=True) - self.ffi = ffi - _cffi_mod2 = importlib.import_module(mod_name) - self.ffi_mod = _cffi_mod2 - - h = self.handler = self.ffi_mod.ffi.new("myHandler*") - self.ffi_mod.lib.init_myHandler(h, self.name.encode()) - return self._get_address(h) - - def _get_address(self, func_name_or_ptr): - if isinstance(func_name_or_ptr, str): - func_name = func_name_or_ptr - data = str(self.ffi_mod.ffi.addressof(self.ffi_mod.lib, func_name)) - else: - ptr = func_name_or_ptr # ptr to struct - data = str(self.ffi_mod.ffi.addressof(ptr[0])) - # data has this format: "" - return int(data.split()[-1][:-1], base=16) - - class TestHandle: def test_handle_create_destroy(self, handle): @@ -380,7 +210,7 @@ def test_abs2sum_on_z_basis(self, handle, input_form): basis_bits = list(range(self.n_qubits)) basis_bits, basis_bits_len = self._return_data( basis_bits, 'basis_bits', *input_form['basis_bits']) - data_type = dtype_to_data_type[sv.dtype] + data_type = dtype_to_data_type[self.dtype] # case 1: both are computed sum0, sum1 = cusv.abs2sum_on_z_basis( @@ -425,7 +255,7 @@ def test_abs2sum_array_no_mask(self, handle, xp, input_form): sv[1] = 1./numpy.sqrt(2) sv[4] = 1./numpy.sqrt(2) - data_type = dtype_to_data_type[sv.dtype] + data_type = dtype_to_data_type[self.dtype] bit_ordering = list(range(self.n_qubits)) bit_ordering, bit_ordering_len = self._return_data( bit_ordering, 'bit_ordering', *input_form['bit_ordering']) @@ -458,7 +288,7 @@ def test_collapse_on_z_basis(self, handle, parity, input_form): basis_bits = list(range(self.n_qubits)) basis_bits, basis_bits_len = self._return_data( basis_bits, 'basis_bits', *input_form['basis_bits']) - data_type = dtype_to_data_type[sv.dtype] + data_type = dtype_to_data_type[self.dtype] cusv.collapse_on_z_basis( handle, sv.data.ptr, data_type, self.n_qubits, @@ -489,7 +319,7 @@ def test_collapse_by_bitstring(self, handle, input_form): bit_ordering = list(range(self.n_qubits)) bit_ordering, _ = self._return_data( bit_ordering, 'bit_ordering', *input_form['bit_ordering']) - data_type = dtype_to_data_type[sv.dtype] + data_type = dtype_to_data_type[self.dtype] norm = 0.5 # the sv after collapse is normalized as sv -> sv / \sqrt{norm} @@ -528,7 +358,7 @@ def test_measure_on_z_basis(self, handle, rand, collapse, input_form): basis_bits = list(range(self.n_qubits)) basis_bits, basis_bits_len = self._return_data( basis_bits, 'basis_bits', *input_form['basis_bits']) - data_type = dtype_to_data_type[sv.dtype] + data_type = dtype_to_data_type[self.dtype] orig_sv = sv.copy() parity = cusv.measure_on_z_basis( @@ -561,7 +391,7 @@ def test_batch_measure(self, handle, rand, collapse, input_form): sv[-1] = numpy.sqrt(0.5) orig_sv = sv.copy() - data_type = dtype_to_data_type[sv.dtype] + data_type = dtype_to_data_type[self.dtype] bitstring = numpy.empty(self.n_qubits, dtype=numpy.int32) bit_ordering = list(range(self.n_qubits)) bit_ordering, _ = self._return_data( @@ -606,7 +436,7 @@ def test_apply_pauli_rotation(self, handle, input_form): sv[0] = 0 sv[4] = 1 - data_type = dtype_to_data_type[sv.dtype] + data_type = dtype_to_data_type[self.dtype] targets = [0, 1] targets, targets_len = self._return_data( targets, 'targets', *input_form['targets']) @@ -646,8 +476,8 @@ def test_apply_matrix(self, handle, xp, input_form, mempool): pytest.skip("cannot run cffi tests") sv = self.get_sv() - data_type = dtype_to_data_type[sv.dtype] - compute_type = dtype_to_compute_type[sv.dtype] + data_type = dtype_to_data_type[self.dtype] + compute_type = dtype_to_compute_type[self.dtype] targets = [0, 1, 2] targets, targets_len = self._return_data( targets, 'targets', *input_form['targets']) @@ -710,8 +540,8 @@ def test_apply_generalized_permutation_matrix( sv = self.get_sv() sv[:] = 1 # invalid sv just to make math checking easier - data_type = dtype_to_data_type[sv.dtype] - compute_type = dtype_to_compute_type[sv.dtype] + data_type = dtype_to_data_type[self.dtype] + compute_type = dtype_to_compute_type[self.dtype] # TODO(leofang): test permutation on either host or device permutation = list(numpy.random.permutation(2**self.n_qubits)) @@ -787,8 +617,8 @@ def test_compute_expectation(self, handle, xp, expect_dtype, input_form, mempool sv = self.get_sv() sv[:] = numpy.sqrt(1/(2**self.n_qubits)) - data_type = dtype_to_data_type[sv.dtype] - compute_type = dtype_to_compute_type[sv.dtype] + data_type = dtype_to_data_type[self.dtype] + compute_type = dtype_to_compute_type[self.dtype] basis_bits = list(range(self.n_qubits)) basis_bits, basis_bits_len = self._return_data( basis_bits, 'basis_bits', *input_form['basis_bits']) @@ -835,8 +665,8 @@ def test_compute_expectations_on_pauli_basis(self, handle): # create a uniform sv sv = self.get_sv() sv[:] = numpy.sqrt(1/(2**self.n_qubits)) - data_type = dtype_to_data_type[sv.dtype] - compute_type = dtype_to_compute_type[sv.dtype] + data_type = dtype_to_data_type[self.dtype] + compute_type = dtype_to_compute_type[self.dtype] # measure XX...X, YY..Y, ZZ...Z paulis = [[cusv.Pauli.X for i in range(self.n_qubits)], @@ -877,8 +707,8 @@ def test_sampling(self, handle, input_form, mempool): sv = self.get_sv() sv[:] = numpy.sqrt(1/(2**self.n_qubits)) - data_type = dtype_to_data_type[sv.dtype] - compute_type = dtype_to_compute_type[sv.dtype] + data_type = dtype_to_data_type[self.dtype] + compute_type = dtype_to_compute_type[self.dtype] shots = 4096 bitstrings = numpy.empty((shots,), dtype=numpy.int64) @@ -959,8 +789,8 @@ def test_accessor_get(self, handle, readonly, input_form, mempool): data /= cupy.sqrt(data**2) sv[:] = data - data_type = dtype_to_data_type[sv.dtype] - compute_type = dtype_to_compute_type[sv.dtype] + data_type = dtype_to_data_type[self.dtype] + compute_type = dtype_to_compute_type[self.dtype] # measure all qubits bit_ordering = list(range(self.n_qubits)) @@ -1021,8 +851,8 @@ def test_accessor_set(self, handle, readonly, input_form, mempool): data /= cupy.sqrt(data**2) sv[:] = data - data_type = dtype_to_data_type[sv.dtype] - compute_type = dtype_to_compute_type[sv.dtype] + data_type = dtype_to_data_type[self.dtype] + compute_type = dtype_to_compute_type[self.dtype] # measure all qubits bit_ordering = list(range(self.n_qubits)) @@ -1109,8 +939,8 @@ def test_apply_matrix_type( and not _can_use_cffi()): pytest.skip("cannot run cffi tests") - data_type = dtype_to_data_type[xp.dtype(dtype)] - compute_type = dtype_to_compute_type[xp.dtype(dtype)] + data_type = dtype_to_data_type[dtype] + compute_type = dtype_to_compute_type[dtype] n_targets = 4 # matrix can live on host or device @@ -1170,7 +1000,7 @@ def test_batch_measure_with_offset( self, multi_gpu_handles, rand, collapse, input_form): handles = multi_gpu_handles sub_sv = self.get_sv() - data_type = dtype_to_data_type[sub_sv[0].dtype] + data_type = dtype_to_data_type[self.dtype] bit_ordering = list(range(self.n_local_bits)) bit_ordering, bit_ordering_len = self._return_data( bit_ordering, 'bit_ordering', *input_form['bit_ordering']) @@ -1260,7 +1090,7 @@ class TestSwap: def test_swap_index_bits(self, handle, dtype, input_form): n_qubits = 4 sv = cupy.zeros(2**n_qubits, dtype=dtype) - data_type = dtype_to_data_type[sv.dtype] + data_type = dtype_to_data_type[dtype] # set sv to |0110> sv[6] = 1 @@ -1317,7 +1147,7 @@ def test_multi_device_swap_index_bits( handles = multi_gpu_handles n_handles = len(handles) sub_sv = self.get_sv() - data_type = dtype_to_data_type[sub_sv[0].dtype] + data_type = dtype_to_data_type[self.dtype] # set sv to |0110> (up to normalization) with cupy.cuda.Device(0): @@ -1365,79 +1195,21 @@ def test_multi_device_swap_index_bits( assert sub_sv[1][4] == 1 -class TestMemHandler: +class TestMemHandler(MemHandlerTestBase): + + mod = cusv + prefix = "custatevec" + error = cusv.cuStateVecError # TODO: add more different memory sources @pytest.mark.parametrize( 'source', (None, "py-callable", 'cffi', 'cffi_struct') ) - def test_set_get_device_mem_handler(self, handle, source): - if (isinstance(source, str) and source.startswith('cffi') - and not _can_use_cffi()): - pytest.skip("cannot run cffi tests") + def test_set_get_device_mem_handler(self, source, handle): + self._test_set_get_device_mem_handler(source, handle) - if source is not None: - mr = MemoryResourceFactory(source) - handler = mr.get_dev_mem_handler() - cusv.set_device_mem_handler(handle, handler) - # round-trip test - queried_handler = cusv.get_device_mem_handler(handle) - if source == 'cffi_struct': - # I'm lazy, otherwise I'd also fetch the functor addresses here... - assert queried_handler[0] == 0 # ctx is NULL - assert queried_handler[-1] == source - else: - assert queried_handler == handler - else: - with pytest.raises(cusv.cuStateVecError) as e: - queried_handler = cusv.get_device_mem_handler(handle) - assert 'CUSTATEVEC_STATUS_NO_DEVICE_ALLOCATOR' in str(e.value) - - -class TestLogger: - - def test_logger_set_level(self): - cusv.logger_set_level(6) # on - cusv.logger_set_level(0) # off - - def test_logger_set_mask(self): - cusv.logger_set_mask(16) # should not raise - - def test_logger_set_callback_data(self): - # we also test logger_open_file() here to avoid polluting stdout - - def callback(level, name, message, my_data, is_ok=False): - log = f"{level}, {name}, {message} (is_ok={is_ok}) -> logged\n" - my_data.append(log) - - handle = None - my_data = [] - is_ok = True - - with tempfile.TemporaryDirectory() as temp: - file_name = os.path.join(temp, "cusv_test") - cusv.logger_open_file(file_name) - cusv.logger_set_callback_data(callback, my_data, is_ok=is_ok) - cusv.logger_set_level(6) - - try: - handle = cusv.create() - cusv.destroy(handle) - except: - if handle: - cusv.destroy(handle) - raise - finally: - cusv.logger_force_disable() # to not affect the rest of tests - - with open(file_name) as f: - log_from_f = f.read() - - # check the log file - assert '[custatevecCreate]' in log_from_f - assert '[custatevecDestroy]' in log_from_f - - # check the captured data (note we log 2 APIs) - log = ''.join(my_data) - assert log.count("-> logged") >= 2 - assert log.count("is_ok=True") >= 2 + +class TestLogger(LoggerTestBase): + + mod = cusv + prefix = "custatevec" diff --git a/python/tests/cuquantum_tests/cutensornet_tests/circuit_utils.py b/python/tests/cuquantum_tests/cutensornet_tests/circuit_utils.py index 28d081c..be63d71 100644 --- a/python/tests/cuquantum_tests/cutensornet_tests/circuit_utils.py +++ b/python/tests/cuquantum_tests/cutensornet_tests/circuit_utils.py @@ -2,6 +2,7 @@ # # SPDX-License-Identifier: BSD-3-Clause +import itertools from types import MappingProxyType try: @@ -20,8 +21,11 @@ qiskit = None from cuquantum import contract, CircuitToEinsum +from cuquantum.cutensornet._internal.circuit_converter_utils import convert_mode_labels_to_expression from cuquantum.cutensornet._internal.circuit_converter_utils import EINSUM_SYMBOLS_BASE -from .testutils import atol_mapper, rtol_mapper +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 .test_utils import atol_mapper, rtol_mapper # note: this implementation would cause pytorch tests being silently skipped @@ -67,6 +71,11 @@ def where_fixed_generator(qubits, nfix_max, nsite_max=None): yield where, fixed +def random_pauli_string_generator(n_qubits, num_strings=4): + for _ in range(num_strings): + 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), @@ -189,23 +198,24 @@ def __init__(self, circuit, dtype, backend, nsample, nsite_max, nfix_max): 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) - def get_state_vector_from_simulator(self, fixed=EMPTY_DICT): + def get_state_vector_from_simulator(self): if self.sv is None: self.sv = self._get_state_vector_from_simulator() - if fixed: - partial_indices = get_partial_indices(self.qubits, fixed) - sv = self.sv[tuple(partial_indices)] - return sv.reshape((2,)*(self.n_qubits-len(fixed))) - else: - return self.sv + 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_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_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, ...}` """ @@ -229,19 +239,43 @@ def get_reduced_density_matrix_from_simulator(self, where, fixed=EMPTY_DICT): else: rdm = contract(expression, sv, sv.conj()) return rdm + + 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() + if self.backend is torch: + operands = [sv] + gate_operands + [sv.conj().resolve_conj()] + else: + operands = [sv] + gate_operands + [sv.conj()] + expec = contract(expression, *operands) + return expec + def _get_state_vector_from_simulator(self): raise NotImplementedError def test_state_vector(self): - for fixed in where_fixed_generator(self.qubits, self.nfix_max): - expression, operands = self.converter.state_vector(fixed=fixed) - sv1 = contract(expression, *operands) - sv2 = self.get_state_vector_from_simulator(fixed=fixed) - self.backend.allclose( - sv1, sv2, atol=atol_mapper[self.dtype], rtol=rtol_mapper[self.dtype]) + expression, operands = self.converter.state_vector() + sv1 = contract(expression, *operands) + sv2 = self.get_state_vector_from_simulator() + self.backend.allclose( + sv1, sv2, atol=atol_mapper[self.dtype], rtol=rtol_mapper[self.dtype]) - def test_bitstrings(self): + def test_amplitude(self): for bitstring in bitstring_generator(self.n_qubits, self.nsample): expression, operands = self.converter.amplitude(bitstring) amp1 = contract(expression, *operands) @@ -249,7 +283,15 @@ def test_bitstrings(self): self.backend.allclose( amp1, amp2, atol=atol_mapper[self.dtype], rtol=rtol_mapper[self.dtype]) - def test_reduced_density_matrices(self): + 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) + self.backend.allclose( + batched_amps1, batched_amps2, atol=atol_mapper[self.dtype], rtol=rtol_mapper[self.dtype]) + + 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) @@ -262,11 +304,27 @@ def test_reduced_density_matrices(self): rdm1, rdm2, atol=atol_mapper[self.dtype], rtol=rtol_mapper[self.dtype]) self.backend.allclose( rdm1, rdm3, atol=atol_mapper[self.dtype], rtol=rtol_mapper[self.dtype]) + + 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) + assert len(operands1) <= len(operands2) + expec1 = contract(expression1, *operands1) + expec2 = contract(expression2, *operands2) + expec3 = self.get_expectation_from_sv(pauli_string) + + self.backend.allclose( + expec1, expec2, atol=atol_mapper[self.dtype], rtol=rtol_mapper[self.dtype]) + self.backend.allclose( + expec1, expec3, atol=atol_mapper[self.dtype], rtol=rtol_mapper[self.dtype]) def run_tests(self): self.test_state_vector() - self.test_bitstrings() - self.test_reduced_density_matrices() + self.test_amplitude() + self.test_batched_amplitudes() + self.test_reduced_density_matrix() + self.test_expectation() class CirqTester(BaseTester): diff --git a/python/tests/cuquantum_tests/cutensornet_tests/data.py b/python/tests/cuquantum_tests/cutensornet_tests/data.py index 1ce7224..ae9cd6b 100644 --- a/python/tests/cuquantum_tests/cutensornet_tests/data.py +++ b/python/tests/cuquantum_tests/cutensornet_tests/data.py @@ -2,20 +2,17 @@ # # SPDX-License-Identifier: BSD-3-Clause -try: - import torch -except ImportError: - torch = None - import cuquantum -# note: this implementation would cause pytorch tests being silently skipped -# if pytorch is not available, which is the desired effect since otherwise -# it'd be too noisy -backend_names = ("numpy", "cupy") -if torch: - backend_names += ("torch-cpu", "torch-gpu") +# We include torch tests here unconditionally, and use pytest deselect to +# exclude them if torch is not present. +backend_names = ( + "numpy", + "cupy", + "torch-cpu", + "torch-gpu", +) dtype_names = ( diff --git a/python/tests/cuquantum_tests/cutensornet_tests/test_contract.py b/python/tests/cuquantum_tests/cutensornet_tests/test_contract.py index 02a6688..25d73fd 100644 --- a/python/tests/cuquantum_tests/cutensornet_tests/test_contract.py +++ b/python/tests/cuquantum_tests/cutensornet_tests/test_contract.py @@ -15,12 +15,14 @@ from cuquantum.cutensornet._internal.utils import infer_object_package from .data import backend_names, dtype_names, einsum_expressions -from .testutils import atol_mapper, EinsumFactory, rtol_mapper -from .testutils import compute_and_normalize_numpy_path -from .testutils import set_path_to_optimizer_options +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 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) ) @@ -46,9 +48,7 @@ def _test_runner( stream, use_numpy_path, **kwargs): einsum_expr = copy.deepcopy(einsum_expr_pack) if isinstance(einsum_expr, list): - einsum_expr, network_opts, optimizer_opts, overwrite_dtype = einsum_expr - if dtype != overwrite_dtype: - pytest.skip(f"skipping {dtype} is requested") + einsum_expr, network_opts, optimizer_opts, _ = einsum_expr else: network_opts = optimizer_opts = None assert isinstance(einsum_expr, (str, tuple)) @@ -83,9 +83,9 @@ def _test_runner( *data, options=network_opts, optimize=optimizer_opts, stream=stream, return_info=return_info) if return_info: - out, info = out - assert isinstance(info[0], list) # path - assert isinstance(info[1], cuquantum.OptimizerInfo) + out, (path, info) = out + assert isinstance(path, list) + assert isinstance(info, cuquantum.OptimizerInfo) else: # cuquantum.einsum() optimize = kwargs.pop('optimize') if optimize == 'path': 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 ebd8ad0..cb1a4e4 100644 --- a/python/tests/cuquantum_tests/cutensornet_tests/test_contract_path.py +++ b/python/tests/cuquantum_tests/cutensornet_tests/test_contract_path.py @@ -8,9 +8,9 @@ import cuquantum from .data import einsum_expressions -from .testutils import compute_and_normalize_numpy_path -from .testutils import EinsumFactory -from .testutils import set_path_to_optimizer_options +from .test_utils import compute_and_normalize_numpy_path +from .test_utils import EinsumFactory +from .test_utils import set_path_to_optimizer_options @pytest.mark.parametrize( diff --git a/python/tests/cuquantum_tests/cutensornet_tests/test_cutensornet.py b/python/tests/cuquantum_tests/cutensornet_tests/test_cutensornet.py index 4a6dc87..52d6955 100644 --- a/python/tests/cuquantum_tests/cutensornet_tests/test_cutensornet.py +++ b/python/tests/cuquantum_tests/cutensornet_tests/test_cutensornet.py @@ -5,21 +5,24 @@ from collections import abc import functools import os -import sys -import tempfile -try: - import cffi -except ImportError: - cffi = None import cupy from cupy import testing import numpy +try: + import mpi4py + from mpi4py import MPI # init! +except ImportError: + mpi4py = MPI = None import pytest import cuquantum from cuquantum import ComputeType, cudaDataType from cuquantum import cutensornet as cutn +from .test_utils import atol_mapper, rtol_mapper + +from .. import (_can_use_cffi, dtype_to_compute_type, dtype_to_data_type, + MemHandlerTestBase, MemoryResourceFactory, LoggerTestBase) ################################################################### @@ -31,29 +34,6 @@ # ################################################################### -if cffi: - # if the Python binding is not installed in the editable mode (pip install - # -e .), the cffi tests would fail as the modules cannot be imported - sys.path.append(os.getcwd()) - -dtype_to_data_type = { - numpy.float16: cudaDataType.CUDA_R_16F, - numpy.float32: cudaDataType.CUDA_R_32F, - numpy.float64: cudaDataType.CUDA_R_64F, - numpy.complex64: cudaDataType.CUDA_C_32F, - numpy.complex128: cudaDataType.CUDA_C_64F, -} - - -dtype_to_compute_type = { - numpy.float16: ComputeType.COMPUTE_16F, - numpy.float32: ComputeType.COMPUTE_32F, - numpy.float64: ComputeType.COMPUTE_64F, - numpy.complex64: ComputeType.COMPUTE_32F, - numpy.complex128: ComputeType.COMPUTE_64F, -} - - def manage_resource(name): def decorator(impl): @functools.wraps(impl) @@ -65,22 +45,40 @@ def test_func(self, *args, **kwargs): tn, dtype, input_form, output_form = self.tn, self.dtype, self.input_form, self.output_form einsum, shapes = tn # unpack tn = TensorNetworkFactory(einsum, shapes, dtype) - i_n_inputs, i_n_modes, i_extents, i_strides, i_modes, i_alignments = \ + i_n_inputs, i_n_modes, i_extents, i_strides, i_modes = \ tn.get_input_metadata(**input_form) - o_n_modes, o_extents, o_strides, o_modes, o_alignments = \ + o_n_modes, o_extents, o_strides, o_modes = \ tn.get_output_metadata(**output_form) + i_qualifiers = numpy.zeros(i_n_inputs, dtype=cutn.tensor_qualifiers_dtype) h = cutn.create_network_descriptor( self.handle, - i_n_inputs, i_n_modes, i_extents, i_strides, i_modes, i_alignments, - o_n_modes, o_extents, o_strides, o_modes, o_alignments, + i_n_inputs, i_n_modes, i_extents, i_strides, i_modes, i_qualifiers, + o_n_modes, o_extents, o_strides, o_modes, dtype_to_data_type[dtype], dtype_to_compute_type[dtype]) # we also need to keep the tn data alive self.tn = tn + elif name == 'tensor_decom': + tn, dtype, tensor_form = self.tn, self.dtype, self.tensor_form + einsum, shapes = tn # unpack + tn = TensorDecompositionFactory(einsum, shapes, dtype) + h = [] + for t in tn.tensor_names: + t = cutn.create_tensor_descriptor( + self.handle, + *tn.get_tensor_metadata(t, **tensor_form), + dtype_to_data_type[dtype]) + h.append(t) + # we also need to keep the tn data alive + self.tn = tn elif name == 'config': h = cutn.create_contraction_optimizer_config(self.handle) elif name == 'info': h = cutn.create_contraction_optimizer_info( self.handle, self.dscr) + elif name == 'svd_config': + h = cutn.create_tensor_svd_config(self.handle) + elif name == 'svd_info': + h = cutn.create_tensor_svd_info(self.handle) elif name == 'autotune': h = cutn.create_contraction_autotune_preference(self.handle) elif name == 'workspace': @@ -103,12 +101,22 @@ def test_func(self, *args, **kwargs): elif name == 'dscr' and hasattr(self, name): cutn.destroy_network_descriptor(self.dscr) del self.dscr + elif name == 'tensor_decom' and hasattr(self, name): + for t in self.tensor_decom: + cutn.destroy_tensor_descriptor(t) + del self.tensor_decom elif name == 'config' and hasattr(self, name): cutn.destroy_contraction_optimizer_config(self.config) del self.config elif name == 'info' and hasattr(self, name): cutn.destroy_contraction_optimizer_info(self.info) del self.info + elif name == 'svd_config' and hasattr(self, name): + cutn.destroy_tensor_svd_config(self.svd_config) + del self.svd_config + elif name == 'svd_info' and hasattr(self, name): + cutn.destroy_tensor_svd_info(self.svd_info) + del self.svd_info elif name == 'autotune' and hasattr(self, name): cutn.destroy_contraction_autotune_preference(self.autotune) del self.autotune @@ -122,155 +130,6 @@ def test_func(self, *args, **kwargs): return decorator -# we don't wanna recompile for every test case... -_cffi_mod1 = None -_cffi_mod2 = None - -def _can_use_cffi(): - if cffi is None or os.environ.get('CUDA_PATH') is None: - return False - else: - return True - - -class MemoryResourceFactory: - - def __init__(self, source, name=None): - self.source = source - self.name = source if name is None else name - - def get_dev_mem_handler(self): - if self.source == "py-callable": - return (*self._get_cuda_callable(), self.name) - elif self.source == "cffi": - # ctx is not needed, so set to NULL - return (0, *self._get_functor_address(), self.name) - elif self.source == "cffi_struct": - return self._get_handler_address() - # TODO: add more different memory sources - else: - raise NotImplementedError - - def _get_cuda_callable(self): - def alloc(size, stream): - return cupy.cuda.runtime.mallocAsync(size, stream) - - def free(ptr, size, stream): - cupy.cuda.runtime.freeAsync(ptr, stream) - - return alloc, free - - def _get_functor_address(self): - if not _can_use_cffi(): - raise RuntimeError - - global _cffi_mod1 - if _cffi_mod1 is None: - import importlib - mod_name = f"cutn_test_{self.source}" - ffi = cffi.FFI() - ffi.set_source(mod_name, """ - #include - - // cffi limitation: we can't use the actual type cudaStream_t because - // it's considered an "incomplete" type and we can't get the functor - // address by doing so... - - int my_alloc(void* ctx, void** ptr, size_t size, void* stream) { - return (int)cudaMallocAsync(ptr, size, stream); - } - - int my_free(void* ctx, void* ptr, size_t size, void* stream) { - return (int)cudaFreeAsync(ptr, stream); - } - """, - include_dirs=[os.environ['CUDA_PATH']+'/include'], - library_dirs=[os.environ['CUDA_PATH']+'/lib64'], - libraries=['cudart'], - ) - ffi.cdef(""" - int my_alloc(void* ctx, void** ptr, size_t size, void* stream); - int my_free(void* ctx, void* ptr, size_t size, void* stream); - """) - ffi.compile(verbose=True) - self.ffi = ffi - _cffi_mod1 = importlib.import_module(mod_name) - self.ffi_mod = _cffi_mod1 - - alloc_addr = self._get_address("my_alloc") - free_addr = self._get_address("my_free") - return alloc_addr, free_addr - - def _get_handler_address(self): - if not _can_use_cffi(): - raise RuntimeError - - global _cffi_mod2 - if _cffi_mod2 is None: - import importlib - mod_name = f"cutn_test_{self.source}" - ffi = cffi.FFI() - ffi.set_source(mod_name, """ - #include - - // cffi limitation: we can't use the actual type cudaStream_t because - // it's considered an "incomplete" type and we can't get the functor - // address by doing so... - - int my_alloc(void* ctx, void** ptr, size_t size, void* stream) { - return (int)cudaMallocAsync(ptr, size, stream); - } - - int my_free(void* ctx, void* ptr, size_t size, void* stream) { - return (int)cudaFreeAsync(ptr, stream); - } - - typedef struct { - void* ctx; - int (*device_alloc)(void* ctx, void** ptr, size_t size, void* stream); - int (*device_free)(void* ctx, void* ptr, size_t size, void* stream); - char name[64]; - } myHandler; - - myHandler* init_myHandler(myHandler* h, const char* name) { - h->ctx = NULL; - h->device_alloc = my_alloc; - h->device_free = my_free; - memcpy(h->name, name, 64); - return h; - } - """, - include_dirs=[os.environ['CUDA_PATH']+'/include'], - library_dirs=[os.environ['CUDA_PATH']+'/lib64'], - libraries=['cudart'], - ) - ffi.cdef(""" - typedef struct { - ...; - } myHandler; - - myHandler* init_myHandler(myHandler* h, const char* name); - """) - ffi.compile(verbose=True) - self.ffi = ffi - _cffi_mod2 = importlib.import_module(mod_name) - self.ffi_mod = _cffi_mod2 - - h = self.handler = self.ffi_mod.ffi.new("myHandler*") - self.ffi_mod.lib.init_myHandler(h, self.name.encode()) - return self._get_address(h) - - def _get_address(self, func_name_or_ptr): - if isinstance(func_name_or_ptr, str): - func_name = func_name_or_ptr - data = str(self.ffi_mod.ffi.addressof(self.ffi_mod.lib, func_name)) - else: - ptr = func_name_or_ptr # ptr to struct - data = str(self.ffi_mod.ffi.addressof(ptr[0])) - # data has this format: "" - return int(data.split()[-1][:-1], base=16) - - class TestLibHelper: def test_get_version(self): @@ -310,20 +169,21 @@ def __init__(self, einsum, shapes, dtype): assert all([len(i) == len(s) for i, s in zip(inputs, i_shapes)]) assert len(output) == len(o_shape) + # xp strides in bytes, cutn strides in counts + itemsize = cupy.dtype(dtype).itemsize + self.input_tensors = [ testing.shaped_random(s, cupy, dtype) for s in i_shapes] self.input_n_modes = [len(i) for i in inputs] self.input_extents = i_shapes - self.input_strides = [arr.strides for arr in self.input_tensors] + self.input_strides = [[stride // itemsize for stride in arr.strides] for arr in self.input_tensors] self.input_modes = [tuple([ord(m) for m in i]) for i in inputs] - self.input_alignments = [256] * len(i_shapes) self.output_tensor = cupy.empty(o_shape, dtype=dtype) self.output_n_modes = len(o_shape) self.output_extent = o_shape - self.output_stride = self.output_tensor.strides + self.output_stride = [stride // itemsize for stride in self.output_tensor.strides] self.output_mode = tuple([ord(m) for m in output]) - self.output_alignment = 256 def _get_data_type(self, category): if 'n_modes' in category: @@ -334,8 +194,6 @@ def _get_data_type(self, category): return numpy.int64 elif 'mode' in category: return numpy.int32 - elif 'alignment' in category: - return numpy.uint32 elif 'tensor' in category: return None # unused else: @@ -397,17 +255,14 @@ def get_input_metadata(self, **kwargs): extents = self._return_data('input_extents', kwargs.pop('extent')) strides = self._return_data('input_strides', kwargs.pop('stride')) modes = self._return_data('input_modes', kwargs.pop('mode')) - alignments = self._return_data( - 'input_alignments', kwargs.pop('alignment')) - return n_inputs, n_modes, extents, strides, modes, alignments + return n_inputs, n_modes, extents, strides, modes def get_output_metadata(self, **kwargs): n_modes = self.output_n_modes extent = self._return_data('output_extent', kwargs.pop('extent')) stride = self._return_data('output_stride', kwargs.pop('stride')) mode = self._return_data('output_mode', kwargs.pop('mode')) - alignment = self.output_alignment - return n_modes, extent, stride, mode, alignment + return n_modes, extent, stride, mode def get_input_tensors(self, **kwargs): data = self._return_data('input_tensors', kwargs['data']) @@ -429,11 +284,11 @@ def get_output_tensor(self): ), 'input_form': ( {'n_modes': 'int', 'extent': 'int', 'stride': 'int', - 'mode': 'int', 'alignment': 'int', 'data': 'int'}, + 'mode': 'int', 'data': 'int'}, {'n_modes': 'int', 'extent': 'seq', 'stride': 'seq', - 'mode': 'seq', 'alignment': 'int', 'data': 'seq'}, + 'mode': 'seq', 'data': 'seq'}, {'n_modes': 'seq', 'extent': 'nested_seq', 'stride': 'nested_seq', - 'mode': 'seq', 'alignment': 'seq', 'data': 'seq'}, + 'mode': 'seq', 'data': 'seq'}, ), 'output_form': ( {'extent': 'int', 'stride': 'int', 'mode': 'int'}, @@ -448,18 +303,33 @@ class TestTensorNetworkBase: class TestTensorNetworkDescriptor(TestTensorNetworkBase): + @pytest.mark.parametrize( + 'API', ('old', 'new') + ) @manage_resource('handle') @manage_resource('dscr') - def test_descriptor_create_destroy(self): + def test_descriptor_create_destroy(self, API): # we could just do a simple round-trip test, but let's also get # this helper API tested handle, dscr = self.handle, self.dscr - num_modes, modes, extents, strides = cutn.get_output_tensor_details(handle, dscr) + + if API == 'old': + # TODO: remove this branch + num_modes, modes, extents, strides = cutn.get_output_tensor_details( + handle, dscr) + else: + tensor_dscr = cutn.get_output_tensor_descriptor(handle, dscr) + num_modes, modes, extents, strides = cutn.get_tensor_details( + handle, tensor_dscr) + assert num_modes == self.tn.output_n_modes assert (modes == numpy.asarray(self.tn.output_mode, dtype=numpy.int32)).all() assert (extents == numpy.asarray(self.tn.output_extent, dtype=numpy.int64)).all() assert (strides == numpy.asarray(self.tn.output_stride, dtype=numpy.int64)).all() + if API == 'new': + cutn.destroy_tensor_descriptor(tensor_dscr) + class TestOptimizerInfo(TestTensorNetworkBase): @@ -468,12 +338,11 @@ def _get_path(self, handle, info): def _set_path(self, handle, info, path): attr = cutn.ContractionOptimizerInfoAttribute.PATH + dtype = cutn.contraction_optimizer_info_get_attribute_dtype(attr) if not isinstance(path, numpy.ndarray): path = numpy.ascontiguousarray(path, dtype=numpy.int32) - num_contraction = path.shape[0] - p = cutn.ContractionPath(num_contraction, path.ctypes.data) - cutn.contraction_optimizer_info_set_attribute( - handle, info, attr, p.get_path(), p.get_size()) + path_obj = numpy.asarray((path.shape[0], path.ctypes.data), dtype=dtype) + self._set_scalar_attr(handle, info, attr, path_obj) def _get_scalar_attr(self, handle, info, attr): dtype = cutn.contraction_optimizer_info_get_attribute_dtype(attr) @@ -507,6 +376,7 @@ def test_optimizer_info_create_destroy(self): def test_optimizer_info_get_set_attribute(self, attr): if attr in ( cutn.ContractionOptimizerInfoAttribute.NUM_SLICES, + cutn.ContractionOptimizerInfoAttribute.NUM_SLICED_MODES, cutn.ContractionOptimizerInfoAttribute.PHASE1_FLOP_COUNT, cutn.ContractionOptimizerInfoAttribute.FLOP_COUNT, cutn.ContractionOptimizerInfoAttribute.LARGEST_TENSOR, @@ -519,6 +389,7 @@ def test_optimizer_info_get_set_attribute(self, attr): cutn.ContractionOptimizerInfoAttribute.PATH, cutn.ContractionOptimizerInfoAttribute.SLICED_MODE, cutn.ContractionOptimizerInfoAttribute.SLICED_EXTENT, + cutn.ContractionOptimizerInfoAttribute.SLICING_CONFIG, cutn.ContractionOptimizerInfoAttribute.INTERMEDIATE_MODES, cutn.ContractionOptimizerInfoAttribute.NUM_INTERMEDIATE_MODES, ): @@ -697,10 +568,16 @@ def test_contraction_workflow( # manage workspace if mempool is None: cutn.workspace_compute_sizes(handle, dscr, info, workspace) + required_size_deprecated = cutn.workspace_get_size( + handle, workspace, + getattr(cutn.WorksizePref, f"{workspace_pref.upper()}"), + cutn.Memspace.DEVICE) # TODO: parametrize memspace? + cutn.workspace_compute_contraction_sizes(handle, dscr, info, workspace) required_size = cutn.workspace_get_size( handle, workspace, getattr(cutn.WorksizePref, f"{workspace_pref.upper()}"), cutn.Memspace.DEVICE) # TODO: parametrize memspace? + assert required_size == required_size_deprecated if workspace_size < required_size: assert False, \ f"wrong assumption on the workspace size " \ @@ -780,77 +657,601 @@ def test_slice_group(self, source): @pytest.mark.parametrize( 'source', (None, "py-callable", 'cffi', 'cffi_struct') ) -class TestMemHandler: +class TestMemHandler(MemHandlerTestBase): + + mod = cutn + prefix = "cutensornet" + error = cutn.cuTensorNetError @manage_resource('handle') def test_set_get_device_mem_handler(self, source): - if (isinstance(source, str) and source.startswith('cffi') - and not _can_use_cffi()): - pytest.skip("cannot run cffi tests") + self._test_set_get_device_mem_handler(source, self.handle) - handle = self.handle - if source is not None: - mr = MemoryResourceFactory(source) - handler = mr.get_dev_mem_handler() - cutn.set_device_mem_handler(handle, handler) - # round-trip test - queried_handler = cutn.get_device_mem_handler(handle) - if source == 'cffi_struct': - # I'm lazy, otherwise I'd also fetch the functor addresses here... - assert queried_handler[0] == 0 # ctx is NULL - assert queried_handler[-1] == source + +class TensorDecompositionFactory: + + # QR Example: "ab->ax,xb" + # SVD Example: "ab->ax,x,xb" + # Gate Example: "ijk,klm,jkpq->->ipk,k,kqm" for indirect gate with singular values returned. + # "ijk,klm,jkpq->ipk,-,kqm" for direct gate algorithm with singular values equally partitioned onto u and v + + # self.reconstruct must be a valid einsum expr and can be used to reconstruct + # the input tensor if no/little truncation was done + + # This factory CANNOT be reused; once a tensor descriptor uses it, it must + # be discarded. + + svd_partitioned = ('<', '-', '>') # reserved symbols + + def __init__(self, einsum, shapes, dtype): + if len(shapes) == 3: + self.tensor_names = ['input', 'left', 'right'] + self.einsum = einsum + elif len(shapes) == 5: + self.tensor_names = ['inputA', 'inputB', 'inputG', 'left', 'right'] + if einsum.count("->") == 1: + self.gate_algorithm = cutn.GateSplitAlgo.DIRECT + self.einsum = einsum + elif einsum.count("->") == 2: + self.gate_algorithm = cutn.GateSplitAlgo.REDUCED + self.einsum = einsum.replace("->->", "->") else: - assert queried_handler == handler + raise NotImplementedError + else: + raise NotImplementedError + + inputs, output = self.einsum.split('->') + output = output.split(',') + if len(output) == 2: # QR + left, right = output + self.reconstruct = f"{left},{right}->{inputs}" + all_modes = [inputs, left, right] + elif len(output) == 3: # SVD or Gate + left, mid_mode, right = output + common_mode = set(left).intersection(right).pop() + assert len(common_mode) == 1 + idx_left = left.find(common_mode) + idx_right = right.find(common_mode) + self.mid_mode = mid_mode + + if len(shapes) == 3: # svd + all_modes = [inputs, left, right] + assert shapes[1][idx_left] == shapes[2][idx_right] + self.mid_extent = shapes[1][idx_left] + self.reference_einsum = None + if mid_mode in self.svd_partitioned: + # s is already merged into left, both, or right + self.reconstruct = f"{left},{right}->{inputs}" + else: + assert mid_mode == common_mode + self.reconstruct = f"{left},{common_mode},{right}->{inputs}" + else: # Gate + all_modes = list(inputs.split(","))+[left, right] + assert shapes[3][idx_left] == shapes[4][idx_right] + self.mid_extent = shapes[3][idx_left] + contracted_output_modes = "".join((set(left) | set(right)) - (set(left) & set(right))) + self.reference_einsum = f"{inputs}->{contracted_output_modes}" + if mid_mode in self.svd_partitioned: + # s is already merged into left, both, or right + self.reconstruct = f"{left},{right}->{contracted_output_modes}" + else: + assert mid_mode == common_mode + self.reconstruct = f"{left},{common_mode},{right}->{contracted_output_modes}" else: - with pytest.raises(cutn.cuTensorNetError) as e: - queried_handler = cutn.get_device_mem_handler(handle) - assert 'CUTENSORNET_STATUS_NO_DEVICE_ALLOCATOR' in str(e.value) + assert False + del output + + # xp strides in bytes, cutn strides in counts + dtype = cupy.dtype(dtype) + itemsize = dtype.itemsize + + for name, shape, modes in zip(self.tensor_names, shapes, all_modes): + real_dtype = dtype.char.lower() + if name.startswith('input'): + if dtype.char != real_dtype: # complex + arr = (cupy.random.random(shape, dtype=real_dtype) + + 1j*cupy.random.random(shape, dtype=real_dtype)).astype(dtype) + else: + arr = cupy.random.random(shape, dtype=dtype) + else: + arr = cupy.empty(shape, dtype=dtype, order='F') + setattr(self, f'{name}_tensor', arr) + setattr(self, f'{name}_n_modes', len(arr.shape)) + setattr(self, f'{name}_extent', arr.shape) + setattr(self, f'{name}_stride', [stride // itemsize for stride in arr.strides]) + setattr(self, f'{name}_mode', tuple([ord(m) for m in modes])) + def _get_data_type(self, category): + if 'n_modes' in category: + return numpy.int32 + elif 'extent' in category: + return numpy.int64 + elif 'stride' in category: + return numpy.int64 + elif 'mode' in category: + return numpy.int32 + elif 'tensor' in category: + return None # unused + else: + assert False -class TestLogger: + def _return_data(self, category, return_value): + data = getattr(self, category) - def test_logger_set_level(self): - cutn.logger_set_level(6) # on - cutn.logger_set_level(0) # off + if return_value == 'int': + if len(data) == 0: + # empty, give it a NULL + return 0 + else: + # return int as void* + data = numpy.asarray(data, dtype=self._get_data_type(category)) + setattr(self, category, data) # keep data alive + return data.ctypes.data + elif return_value == 'seq': + return data + else: + assert False - def test_logger_set_mask(self): - cutn.logger_set_mask(16) # should not raise + def get_tensor_metadata(self, name, **kwargs): + assert name in self.tensor_names + n_modes = getattr(self, f'{name}_n_modes') + extent = self._return_data(f'{name}_extent', kwargs.pop('extent')) + stride = self._return_data(f'{name}_stride', kwargs.pop('stride')) + mode = self._return_data(f'{name}_mode', kwargs.pop('mode')) + return n_modes, extent, stride, mode - def test_logger_set_callback_data(self): - # we also test logger_open_file() here to avoid polluting stdout + def get_tensor_ptr(self, name): + return getattr(self, f'{name}_tensor').data.ptr - def callback(level, name, message, my_data, is_ok=False): - log = f"{level}, {name}, {message} (is_ok={is_ok}) -> logged\n" - my_data.append(log) - handle = None - my_data = [] - is_ok = True +@testing.parameterize(*testing.product({ + 'tn': ( + ('ab->ax,xb', [(8, 8), (8, 8), (8, 8)]), + ('ab->ax,bx', [(8, 8), (8, 8), (8, 8)]), + ('ab->xa,xb', [(8, 8), (8, 8), (8, 8)]), + ('ab->xa,bx', [(8, 8), (8, 8), (8, 8)]), + ('ab->ax,xb', [(6, 8), (6, 6), (6, 8)]), + ('ab->ax,bx', [(6, 8), (6, 6), (8, 6)]), + ('ab->xa,xb', [(6, 8), (6, 6), (6, 8)]), + ('ab->xa,bx', [(6, 8), (6, 6), (8, 6)]), + ('ab->ax,xb', [(8, 6), (8, 6), (6, 6)]), + ('ab->ax,bx', [(8, 6), (8, 6), (6, 6)]), + ('ab->xa,xb', [(8, 6), (6, 8), (6, 6)]), + ('ab->xa,bx', [(8, 6), (6, 8), (6, 6)]), + ), + 'dtype': ( + numpy.float32, numpy.float64, numpy.complex64, numpy.complex128 + ), + 'tensor_form': ( + {'extent': 'int', 'stride': 'int', 'mode': 'int'}, + {'extent': 'seq', 'stride': 'seq', 'mode': 'seq'}, + ), +})) +class TestTensorQR: - with tempfile.TemporaryDirectory() as temp: - file_name = os.path.join(temp, "cutn_test") - cutn.logger_open_file(file_name) - cutn.logger_set_callback_data(callback, my_data, is_ok=is_ok) - cutn.logger_set_level(6) + # There is no easy way for us to test each API independently, so we instead + # parametrize the steps and test the whole workflow + @manage_resource('handle') + @manage_resource('tensor_decom') + @manage_resource('workspace') + def test_tensor_qr(self): + # unpack + handle, tn, workspace = self.handle, self.tn, self.workspace + tensor_in, tensor_q, tensor_r = self.tensor_decom + dtype = cupy.dtype(self.dtype) + + # prepare workspace + cutn.workspace_compute_qr_sizes( + handle, tensor_in, tensor_q, tensor_r, workspace) + # for now host workspace is always 0, so just query device one + # also, it doesn't matter which one (min/recommended/max) is queried + required_size = cutn.workspace_get_size( + handle, workspace, cutn.WorksizePref.MIN, + cutn.Memspace.DEVICE) # TODO: parametrize memspace? + if required_size > 0: + workspace_ptr = cupy.cuda.alloc(required_size) + cutn.workspace_set( + handle, workspace, cutn.Memspace.DEVICE, + workspace_ptr.ptr, required_size) + # round-trip check + assert (workspace_ptr.ptr, required_size) == cutn.workspace_get( + handle, workspace, cutn.Memspace.DEVICE) + + # perform QR + stream = cupy.cuda.get_current_stream().ptr # TODO + cutn.tensor_qr( + handle, tensor_in, tn.get_tensor_ptr('input'), + tensor_q, tn.get_tensor_ptr('left'), + tensor_r, tn.get_tensor_ptr('right'), + workspace, stream) + + # we add a minimal correctness check here as we are not protected by + # any high-level API yet + out = cupy.einsum(tn.reconstruct, tn.left_tensor, tn.right_tensor) + assert cupy.allclose(out, tn.input_tensor, + rtol=rtol_mapper[dtype.name], + atol=atol_mapper[dtype.name]) + + +# TODO: expand tests: +# - add truncation +# - use config (cutoff & normalization) +@testing.parameterize(*testing.product({ + 'tn': ( + # no truncation, no partition + ('ab->ax,x,xb', [(8, 8), (8, 8), (8, 8)]), + ('ab->ax,x,bx', [(8, 8), (8, 8), (8, 8)]), + ('ab->xa,x,xb', [(8, 8), (8, 8), (8, 8)]), + ('ab->xa,x,bx', [(8, 8), (8, 8), (8, 8)]), + ('ab->ax,x,xb', [(6, 8), (6, 6), (6, 8)]), + ('ab->ax,x,bx', [(6, 8), (6, 6), (8, 6)]), + ('ab->xa,x,xb', [(6, 8), (6, 6), (6, 8)]), + ('ab->xa,x,bx', [(6, 8), (6, 6), (8, 6)]), + ('ab->ax,x,xb', [(8, 6), (8, 6), (6, 6)]), + ('ab->ax,x,bx', [(8, 6), (8, 6), (6, 6)]), + ('ab->xa,x,xb', [(8, 6), (6, 8), (6, 6)]), + ('ab->xa,x,bx', [(8, 6), (6, 8), (6, 6)]), + # no truncation, partition to u + ('ab->ax,<,xb', [(8, 8), (8, 8), (8, 8)]), + ('ab->ax,<,bx', [(8, 8), (8, 8), (8, 8)]), + ('ab->xa,<,xb', [(8, 8), (8, 8), (8, 8)]), + ('ab->xa,<,bx', [(8, 8), (8, 8), (8, 8)]), + # no truncation, partition to v + ('ab->ax,>,xb', [(8, 8), (8, 8), (8, 8)]), + ('ab->ax,>,bx', [(8, 8), (8, 8), (8, 8)]), + ('ab->xa,>,xb', [(8, 8), (8, 8), (8, 8)]), + ('ab->xa,>,bx', [(8, 8), (8, 8), (8, 8)]), + # no truncation, partition to both + ('ab->ax,-,xb', [(8, 8), (8, 8), (8, 8)]), + ('ab->ax,-,bx', [(8, 8), (8, 8), (8, 8)]), + ('ab->xa,-,xb', [(8, 8), (8, 8), (8, 8)]), + ('ab->xa,-,bx', [(8, 8), (8, 8), (8, 8)]), + ), + 'dtype': ( + numpy.float32, numpy.float64, numpy.complex64, numpy.complex128 + ), + 'tensor_form': ( + {'extent': 'int', 'stride': 'int', 'mode': 'int'}, + {'extent': 'seq', 'stride': 'seq', 'mode': 'seq'}, + ), +})) +class TestTensorSVD: + + def _get_scalar_attr(self, handle, obj_type, obj, attr): + if obj_type == 'config': + dtype_getter = cutn.tensor_svd_config_get_attribute_dtype + getter = cutn.tensor_svd_config_get_attribute + elif obj_type == 'info': + dtype_getter = cutn.tensor_svd_info_get_attribute_dtype + getter = cutn.tensor_svd_info_get_attribute + else: + assert False - try: - handle = cutn.create() - cutn.destroy(handle) - except: - if handle: - cutn.destroy(handle) - raise - finally: - cutn.logger_force_disable() # to not affect the rest of tests + dtype = dtype_getter(attr) + data = numpy.empty((1,), dtype=dtype) + getter(handle, obj, attr, data.ctypes.data, data.dtype.itemsize) + return data + + def _set_scalar_attr(self, handle, obj_type, obj, attr, data): + assert obj_type == 'config' # svd info has no setter + dtype_getter = cutn.tensor_svd_config_get_attribute_dtype + setter = cutn.tensor_svd_config_set_attribute + + dtype = dtype_getter(attr) + if not isinstance(data, numpy.ndarray): + data = numpy.asarray(data, dtype=dtype) + setter(handle, obj, attr, data.ctypes.data, data.dtype.itemsize) + + # There is no easy way for us to test each API independently, so we instead + # parametrize the steps and test the whole workflow + @manage_resource('handle') + @manage_resource('tensor_decom') + @manage_resource('svd_config') + @manage_resource('svd_info') + @manage_resource('workspace') + def test_tensor_svd(self): + # unpack + handle, tn, workspace = self.handle, self.tn, self.workspace + tensor_in, tensor_u, tensor_v = self.tensor_decom + svd_config, svd_info = self.svd_config, self.svd_info + dtype = cupy.dtype(self.dtype) + + # prepare workspace + cutn.workspace_compute_svd_sizes( + handle, tensor_in, tensor_u, tensor_v, svd_config, workspace) + # for now host workspace is always 0, so just query device one + # also, it doesn't matter which one (min/recommended/max) is queried + required_size = cutn.workspace_get_size( + handle, workspace, cutn.WorksizePref.MIN, + cutn.Memspace.DEVICE) # TODO: parametrize memspace? + if required_size > 0: + workspace_ptr = cupy.cuda.alloc(required_size) + cutn.workspace_set( + handle, workspace, cutn.Memspace.DEVICE, + workspace_ptr.ptr, required_size) + # round-trip check + assert (workspace_ptr.ptr, required_size) == cutn.workspace_get( + handle, workspace, cutn.Memspace.DEVICE) + + # set singular value partitioning, if requested + if tn.mid_mode in tn.svd_partitioned: + if tn.mid_mode == '<': + data = cutn.TensorSVDPartition.US + elif tn.mid_mode == '-': + data = cutn.TensorSVDPartition.UV_EQUAL + else: # = '<': + data = cutn.TensorSVDPartition.SV + self._set_scalar_attr( + handle, 'config', svd_config, + cutn.TensorSVDConfigAttribute.S_PARTITION, + data) + # do a round-trip test as a sanity check + factor = self._get_scalar_attr( + handle, 'config', svd_config, + cutn.TensorSVDConfigAttribute.S_PARTITION) + assert factor == data + + # perform SVD + stream = cupy.cuda.get_current_stream().ptr # TODO + if tn.mid_mode in tn.svd_partitioned: + s_ptr = 0 + else: + s = cupy.empty(tn.mid_extent, dtype=dtype.char.lower()) + s_ptr = s.data.ptr + cutn.tensor_svd( + handle, tensor_in, tn.get_tensor_ptr('input'), + tensor_u, tn.get_tensor_ptr('left'), + s_ptr, + tensor_v, tn.get_tensor_ptr('right'), + svd_config, svd_info, workspace, stream) + + # sanity checks (only valid for no truncation) + assert tn.mid_extent == self._get_scalar_attr( + handle, 'info', svd_info, + cutn.TensorSVDInfoAttribute.FULL_EXTENT) + assert tn.mid_extent == self._get_scalar_attr( + handle, 'info', svd_info, + cutn.TensorSVDInfoAttribute.REDUCED_EXTENT) + assert 0 == self._get_scalar_attr( + handle, 'info', svd_info, + cutn.TensorSVDInfoAttribute.DISCARDED_WEIGHT) + + # we add a minimal correctness check here as we are not protected by + # any high-level API yet + if tn.mid_mode in tn.svd_partitioned: + out = cupy.einsum( + tn.reconstruct, tn.left_tensor, tn.right_tensor) + else: + out = cupy.einsum( + tn.reconstruct, tn.left_tensor, s, tn.right_tensor) + assert cupy.allclose(out, tn.input_tensor, + rtol=rtol_mapper[dtype.name], + atol=atol_mapper[dtype.name]) + + +# TODO: expand tests: +# - add truncation +# - use config (cutoff & normalization) +@testing.parameterize(*testing.product({ + 'tn': ( + # direct algorithm, no truncation, no partition + ('ijk,klm,jlpq->ipk,k,kqm', [(4, 2, 4), (4, 2, 4), (2, 2, 2, 2), (4, 2, 8), (8, 2, 4)]), + ('ijk,klm,jlpq->kpi,k,qmk', [(4, 2, 4), (4, 2, 4), (2, 2, 2, 2), (8, 2, 4), (2, 4, 8)]), + ('ijk,klm,jlpq->pki,k,mkq', [(4, 2, 4), (4, 2, 4), (2, 2, 2, 2), (2, 8, 4), (4, 8, 2)]), + # direct algorithm, no truncation, partition onto u + ('ijk,klm,jlpq->ipk,<,kqm', [(4, 2, 4), (4, 2, 4), (2, 2, 2, 2), (4, 2, 8), (8, 2, 4)]), + ('ijk,klm,jlpq->kpi,<,qmk', [(4, 2, 4), (4, 2, 4), (2, 2, 2, 2), (8, 2, 4), (2, 4, 8)]), + ('ijk,klm,jlpq->pki,<,mkq', [(4, 2, 4), (4, 2, 4), (2, 2, 2, 2), (2, 8, 4), (4, 8, 2)]), + # direct algorithm, no truncation, partition onto v + ('ijk,klm,jlpq->ipk,>,kqm', [(4, 2, 4), (4, 2, 4), (2, 2, 2, 2), (4, 2, 8), (8, 2, 4)]), + ('ijk,klm,jlpq->kpi,>,qmk', [(4, 2, 4), (4, 2, 4), (2, 2, 2, 2), (8, 2, 4), (2, 4, 8)]), + ('ijk,klm,jlpq->pki,>,mkq', [(4, 2, 4), (4, 2, 4), (2, 2, 2, 2), (2, 8, 4), (4, 8, 2)]), + # direct algorithm, no truncation, partition onto u and v equally + ('ijk,klm,jlpq->ipk,-,kqm', [(4, 2, 4), (4, 2, 4), (2, 2, 2, 2), (4, 2, 8), (8, 2, 4)]), + ('ijk,klm,jlpq->kpi,-,qmk', [(4, 2, 4), (4, 2, 4), (2, 2, 2, 2), (8, 2, 4), (2, 4, 8)]), + ('ijk,klm,jlpq->pki,-,mkq', [(4, 2, 4), (4, 2, 4), (2, 2, 2, 2), (2, 8, 4), (4, 8, 2)]), + # reduced algorithm, no truncation, no partition + ('ijk,klm,jlpq->->ipk,k,kqm', [(4, 2, 4), (4, 2, 4), (2, 2, 2, 2), (4, 2, 8), (8, 2, 4)]), + ('ijk,klm,jlpq->->kpi,k,qmk', [(4, 2, 4), (4, 2, 4), (2, 2, 2, 2), (8, 2, 4), (2, 4, 8)]), + ('ijk,klm,jlpq->->pki,k,mkq', [(4, 2, 4), (4, 2, 4), (2, 2, 2, 2), (2, 8, 4), (4, 8, 2)]), + # reduced algorithm, no truncation, partition onto u + ('ijk,klm,jlpq->->ipk,<,kqm', [(4, 2, 4), (4, 2, 4), (2, 2, 2, 2), (4, 2, 8), (8, 2, 4)]), + ('ijk,klm,jlpq->->kpi,<,qmk', [(4, 2, 4), (4, 2, 4), (2, 2, 2, 2), (8, 2, 4), (2, 4, 8)]), + ('ijk,klm,jlpq->->pki,<,mkq', [(4, 2, 4), (4, 2, 4), (2, 2, 2, 2), (2, 8, 4), (4, 8, 2)]), + # reduced algorithm, no truncation, partition onto v + ('ijk,klm,jlpq->->ipk,>,kqm', [(4, 2, 4), (4, 2, 4), (2, 2, 2, 2), (4, 2, 8), (8, 2, 4)]), + ('ijk,klm,jlpq->->kpi,>,qmk', [(4, 2, 4), (4, 2, 4), (2, 2, 2, 2), (8, 2, 4), (2, 4, 8)]), + ('ijk,klm,jlpq->->pki,>,mkq', [(4, 2, 4), (4, 2, 4), (2, 2, 2, 2), (2, 8, 4), (4, 8, 2)]), + # reduced algorithm, no truncation, partition onto u and v equally + ('ijk,klm,jlpq->->ipk,-,kqm', [(4, 2, 4), (4, 2, 4), (2, 2, 2, 2), (4, 2, 8), (8, 2, 4)]), + ('ijk,klm,jlpq->->kpi,-,qmk', [(4, 2, 4), (4, 2, 4), (2, 2, 2, 2), (8, 2, 4), (2, 4, 8)]), + ('ijk,klm,jlpq->->pki,-,mkq', [(4, 2, 4), (4, 2, 4), (2, 2, 2, 2), (2, 8, 4), (4, 8, 2)]), + ), + 'dtype': ( + numpy.float32, numpy.float64, numpy.complex64, numpy.complex128 + ), + 'tensor_form': ( + {'extent': 'int', 'stride': 'int', 'mode': 'int'}, + {'extent': 'seq', 'stride': 'seq', 'mode': 'seq'}, + ), +})) +class TestTensorGate: + + def _get_scalar_attr(self, handle, obj_type, obj, attr): + if obj_type == 'config': + dtype_getter = cutn.tensor_svd_config_get_attribute_dtype + getter = cutn.tensor_svd_config_get_attribute + elif obj_type == 'info': + dtype_getter = cutn.tensor_svd_info_get_attribute_dtype + getter = cutn.tensor_svd_info_get_attribute + else: + assert False + + dtype = dtype_getter(attr) + data = numpy.empty((1,), dtype=dtype) + getter(handle, obj, attr, data.ctypes.data, data.dtype.itemsize) + return data + + def _set_scalar_attr(self, handle, obj_type, obj, attr, data): + assert obj_type == 'config' # svd info has no setter + dtype_getter = cutn.tensor_svd_config_get_attribute_dtype + setter = cutn.tensor_svd_config_set_attribute + + dtype = dtype_getter(attr) + if not isinstance(data, numpy.ndarray): + data = numpy.asarray(data, dtype=dtype) + setter(handle, obj, attr, data.ctypes.data, data.dtype.itemsize) + + # There is no easy way for us to test each API independently, so we instead + # parametrize the steps and test the whole workflow + @manage_resource('handle') + @manage_resource('tensor_decom') + @manage_resource('svd_config') + @manage_resource('svd_info') + @manage_resource('workspace') + def test_gate_split(self): + # unpack + handle, tn, workspace = self.handle, self.tn, self.workspace + tensor_in_a, tensor_in_b, tensor_in_g, tensor_u, tensor_v = self.tensor_decom + gate_algorithm = tn.gate_algorithm + svd_config, svd_info = self.svd_config, self.svd_info + dtype = cupy.dtype(self.dtype) + compute_type = dtype_to_compute_type[self.dtype] + # prepare workspace + cutn.workspace_compute_gate_split_sizes(handle, + tensor_in_a, tensor_in_b, tensor_in_g, tensor_u, tensor_v, + gate_algorithm, svd_config, compute_type, workspace) + # for now host workspace is always 0, so just query device one + # also, it doesn't matter which one (min/recommended/max) is queried + required_size = cutn.workspace_get_size( + handle, workspace, cutn.WorksizePref.MIN, + cutn.Memspace.DEVICE) # TODO: parametrize memspace? + if required_size > 0: + workspace_ptr = cupy.cuda.alloc(required_size) + cutn.workspace_set( + handle, workspace, cutn.Memspace.DEVICE, + workspace_ptr.ptr, required_size) + # round-trip check + assert (workspace_ptr.ptr, required_size) == cutn.workspace_get( + handle, workspace, cutn.Memspace.DEVICE) + + # set singular value partitioning, if requested + if tn.mid_mode in tn.svd_partitioned: + if tn.mid_mode == '<': + data = cutn.TensorSVDPartition.US + elif tn.mid_mode == '-': + data = cutn.TensorSVDPartition.UV_EQUAL + else: # = '<': + data = cutn.TensorSVDPartition.SV + self._set_scalar_attr( + handle, 'config', svd_config, + cutn.TensorSVDConfigAttribute.S_PARTITION, + data) + # do a round-trip test as a sanity check + factor = self._get_scalar_attr( + handle, 'config', svd_config, + cutn.TensorSVDConfigAttribute.S_PARTITION) + assert factor == data + + # perform gate split + stream = cupy.cuda.get_current_stream().ptr # TODO + if tn.mid_mode in tn.svd_partitioned: + s_ptr = 0 + else: + s = cupy.empty(tn.mid_extent, dtype=dtype.char.lower()) + s_ptr = s.data.ptr + cutn.gate_split(handle, tensor_in_a, tn.get_tensor_ptr('inputA'), + tensor_in_b, tn.get_tensor_ptr('inputB'), + tensor_in_g, tn.get_tensor_ptr('inputG'), + tensor_u, tn.get_tensor_ptr('left'), s_ptr, + tensor_v, tn.get_tensor_ptr('right'), + gate_algorithm, svd_config, compute_type, + svd_info, workspace, stream) + + # sanity checks (only valid for no truncation) + assert tn.mid_extent == self._get_scalar_attr( + handle, 'info', svd_info, + cutn.TensorSVDInfoAttribute.FULL_EXTENT) + assert tn.mid_extent == self._get_scalar_attr( + handle, 'info', svd_info, + cutn.TensorSVDInfoAttribute.REDUCED_EXTENT) + assert 0 == self._get_scalar_attr( + handle, 'info', svd_info, + cutn.TensorSVDInfoAttribute.DISCARDED_WEIGHT) + + # we add a minimal correctness check here as we are not protected by + # any high-level API yet + if tn.mid_mode in tn.svd_partitioned: + out = cupy.einsum( + tn.reconstruct, tn.left_tensor, tn.right_tensor) + else: + out = cupy.einsum( + tn.reconstruct, tn.left_tensor, s, tn.right_tensor) + reference = cupy.einsum(tn.reference_einsum, tn.inputA_tensor, tn.inputB_tensor, tn.inputG_tensor) + error = cupy.linalg.norm(out - reference) + assert cupy.allclose(out, reference, + rtol=rtol_mapper[dtype.name], + atol=atol_mapper[dtype.name]) + + +class TestTensorSVDConfig: + + @manage_resource('handle') + @manage_resource('svd_config') + def test_tensor_svd_config_create_destroy(self): + # simple round-trip test + pass + + @pytest.mark.parametrize( + 'attr', [val for val in cutn.TensorSVDConfigAttribute] + ) + @manage_resource('handle') + @manage_resource('svd_config') + def test_tensor_svd_config_get_set_attribute(self, attr): + handle, svd_config = self.handle, self.svd_config + dtype = cutn.tensor_svd_config_get_attribute_dtype(attr) + # Hack: assume this is a valid value for all attrs + factor = numpy.asarray([0.8], dtype=dtype) + cutn.tensor_svd_config_set_attribute( + handle, svd_config, attr, + factor.ctypes.data, factor.dtype.itemsize) + # do a round-trip test as a sanity check + factor2 = numpy.zeros_like(factor) + cutn.tensor_svd_config_get_attribute( + handle, svd_config, attr, + factor2.ctypes.data, factor2.dtype.itemsize) + assert factor == factor2 + + +@pytest.mark.skipif(mpi4py is None, reason="need mpi4py") +@pytest.mark.skipif(os.environ.get("CUTENSORNET_COMM_LIB") is None, + reason="wrapper lib not set") +class TestDistributed: + + def _get_comm(self, comm): + if comm == 'world': + return MPI.COMM_WORLD.Dup() + elif comm == 'self': + return MPI.COMM_SELF.Dup() + else: + assert False + + @pytest.mark.parametrize( + 'comm', ('world', 'self'), + ) + @manage_resource('handle') + def test_distributed(self, comm): + handle = self.handle + comm = self._get_comm(comm) + cutn.distributed_reset_configuration( + handle, *cutn.get_mpi_comm_pointer(comm)) + assert comm.Get_size() == cutn.distributed_get_num_ranks(handle) + assert comm.Get_rank() == cutn.distributed_get_proc_rank(handle) + cutn.distributed_synchronize(handle) + # no need to free the comm, for world/self mpi4py does it for us... - with open(file_name) as f: - log_from_f = f.read() - # check the log file - assert '[cutensornetCreate]' in log_from_f - assert '[cutensornetDestroy]' in log_from_f +class TestLogger(LoggerTestBase): - # check the captured data (note we log 2 APIs) - log = ''.join(my_data) - assert log.count("-> logged") >= 2 - assert log.count("is_ok=True") >= 2 + mod = cutn + prefix = "cutensornet" diff --git a/python/tests/cuquantum_tests/cutensornet_tests/test_internal.py b/python/tests/cuquantum_tests/cutensornet_tests/test_internal.py new file mode 100644 index 0000000..4e97467 --- /dev/null +++ b/python/tests/cuquantum_tests/cutensornet_tests/test_internal.py @@ -0,0 +1,87 @@ +import threading + +import cupy as cp +from cupy.cuda.runtime import getDevice, setDevice +import pytest + +from cuquantum.cutensornet._internal import utils + + +class TestDeviceCtx: + + @pytest.mark.skipif( + cp.cuda.runtime.getDeviceCount() < 2, reason='not enough GPUs') + def test_device_ctx(self): + assert getDevice() == 0 + with utils.device_ctx(0): + assert getDevice() == 0 + with utils.device_ctx(1): + assert getDevice() == 1 + with utils.device_ctx(0): + assert getDevice() == 0 + assert getDevice() == 1 + assert getDevice() == 0 + assert getDevice() == 0 + + with utils.device_ctx(1): + assert getDevice() == 1 + setDevice(0) + with utils.device_ctx(1): + assert getDevice() == 1 + assert getDevice() == 0 + assert getDevice() == 0 + + @pytest.mark.skipif( + cp.cuda.runtime.getDeviceCount() < 2, reason='not enough GPUs') + def test_thread_safe(self): + # adopted from https://github.com/cupy/cupy/blob/master/tests/cupy_tests/cuda_tests/test_device.py + # recall that the CUDA context is maintained per-thread, so when each thread + # starts it is on the default device (=device 0). + t0_setup = threading.Event() + t1_setup = threading.Event() + t0_first_exit = threading.Event() + + t0_exit_device = [] + t1_exit_device = [] + + def t0_seq(): + with utils.device_ctx(0): + with utils.device_ctx(1): + t0_setup.set() + t1_setup.wait() + t0_exit_device.append(getDevice()) + t0_exit_device.append(getDevice()) + t0_first_exit.set() + assert getDevice() == 0 + + def t1_seq(): + t0_setup.wait() + with utils.device_ctx(1): + with utils.device_ctx(0): + t1_setup.set() + t0_first_exit.wait() + t1_exit_device.append(getDevice()) + t1_exit_device.append(getDevice()) + assert getDevice() == 0 + + try: + cp.cuda.runtime.setDevice(1) + t0 = threading.Thread(target=t0_seq) + t1 = threading.Thread(target=t1_seq) + t1.start() + t0.start() + t0.join() + t1.join() + assert t0_exit_device == [1, 0] + assert t1_exit_device == [0, 1] + finally: + cp.cuda.runtime.setDevice(0) + + def test_one_shot(self): + dev = utils.device_ctx(0) + with dev: + pass + # CPython raises AttributeError, but we should not care here + with pytest.raises(Exception): + with dev: + pass diff --git a/python/tests/cuquantum_tests/cutensornet_tests/test_network.py b/python/tests/cuquantum_tests/cutensornet_tests/test_network.py index 74489cb..2b5f610 100644 --- a/python/tests/cuquantum_tests/cutensornet_tests/test_network.py +++ b/python/tests/cuquantum_tests/cutensornet_tests/test_network.py @@ -2,6 +2,7 @@ # # SPDX-License-Identifier: BSD-3-Clause +import functools import copy import re import sys @@ -15,12 +16,15 @@ from cuquantum.cutensornet._internal.utils import infer_object_package from .data import backend_names, dtype_names, einsum_expressions -from .testutils import atol_mapper, EinsumFactory, rtol_mapper -from .testutils import compute_and_normalize_numpy_path -from .testutils import set_path_to_optimizer_options +from .test_utils import atol_mapper, EinsumFactory, rtol_mapper +from .test_utils import check_intermediate_modes +from .test_utils import compute_and_normalize_numpy_path +from .test_utils import deselect_contract_tests +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) ) @@ -49,9 +53,7 @@ def test_network( stream, use_numpy_path): einsum_expr = copy.deepcopy(einsum_expr_pack) if isinstance(einsum_expr, list): - einsum_expr, network_opts, optimizer_opts, overwrite_dtype = einsum_expr - if dtype != overwrite_dtype: - pytest.skip(f"skipping {dtype} is requested") + einsum_expr, network_opts, optimizer_opts, _ = einsum_expr else: network_opts = optimizer_opts = None assert isinstance(einsum_expr, (str, tuple)) @@ -60,21 +62,24 @@ def test_network( operands = factory.generate_operands( factory.input_shapes, xp, dtype, order) backend = sys.modules[infer_object_package(operands[0])] + data = factory.convert_by_format(operands) if stream: if backend is numpy: stream = cupy.cuda.Stream() # implementation detail else: stream = backend.cuda.Stream() - data = factory.convert_by_format(operands) tn = Network(*data, options=network_opts) # 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: - _, info = tn.contract_path(optimize=optimizer_opts) + path, info = tn.contract_path(optimize=optimizer_opts) uninit_f_str = re.compile("{.*}") assert uninit_f_str.search(str(info)) is None + check_intermediate_modes( + info.intermediate_modes, factory.input_modes, + factory.output_modes, path) else: try: path_ref = compute_and_normalize_numpy_path( @@ -87,22 +92,37 @@ def test_network( optimizer_opts = set_path_to_optimizer_options( optimizer_opts, path_ref) path, _ = tn.contract_path(optimizer_opts) - assert path == path_ref # round-trip test + # 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)) if autotune: tn.autotune(iterations=autotune, stream=stream) - out = tn.contract(stream=stream) - if stream: - stream.synchronize() - backend_out = sys.modules[infer_object_package(out)] - assert backend_out is backend - assert out.dtype == operands[0].dtype - - 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]) + # check the result + self._verify_contract( + tn, operands, backend, data, xp, dtype, stream) - # TODO: test tn.reset_operands() + # generate new data and bind them to the TN + operands = factory.generate_operands( + factory.input_shapes, xp, dtype, order) + data = factory.convert_by_format(operands) + tn.reset_operands(*operands) + # check the result + self._verify_contract( + tn, operands, backend, data, xp, dtype, stream) finally: tn.free() + + 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 + assert out.dtype == operands[0].dtype + + 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]) diff --git a/python/tests/cuquantum_tests/cutensornet_tests/testutils.py b/python/tests/cuquantum_tests/cutensornet_tests/test_utils.py similarity index 69% rename from python/tests/cuquantum_tests/cutensornet_tests/testutils.py rename to python/tests/cuquantum_tests/cutensornet_tests/test_utils.py index 8496a5a..756dd3a 100644 --- a/python/tests/cuquantum_tests/cutensornet_tests/testutils.py +++ b/python/tests/cuquantum_tests/cutensornet_tests/test_utils.py @@ -2,6 +2,7 @@ # # SPDX-License-Identifier: BSD-3-Clause +import re import sys import cupy @@ -75,6 +76,71 @@ def compute_and_normalize_numpy_path(data, num_operands): return norm_path +def convert_linear_to_ssa(path): + n_inputs = len(path)+1 + remaining = [*range(n_inputs)] + ssa_path = [] + counter = n_inputs + + for first, second in path: + idx1 = remaining[first] + idx2 = remaining[second] + ssa_path.append((idx1, idx2)) + remaining.remove(idx1) + remaining.remove(idx2) + remaining.append(counter) + counter += 1 + + return ssa_path + + +def check_ellipsis(modes): + # find ellipsis, record the position, remove it, and modify the modes + if isinstance(modes, str): + ellipsis = modes.find("...") + if ellipsis >= 0: + modes = modes.replace("...", "") + else: + try: + ellipsis = modes.index(Ellipsis) + except ValueError: + ellipsis = -1 + if ellipsis >= 0: + modes = modes[:ellipsis] + modes[ellipsis+1:] + return ellipsis, modes + + +def check_intermediate_modes( + intermediate_modes, input_modes, output_modes, path): + + # remove ellipsis, if any, since it's singleton + input_modes = list(map( + lambda modes: (lambda modes: check_ellipsis(modes))(modes)[1], + input_modes + )) + _, output_modes = check_ellipsis(output_modes) + # peek at the very first element + if (isinstance(intermediate_modes[0], tuple) + and isinstance(intermediate_modes[0][0], str)): + # this is our internal mode label for ellipsis + custom_label = re.compile(r'\b__\d+__\b') + intermediate_modes = list(map( + lambda modes: list(filter(lambda mode: not custom_label.match(mode), modes)), + intermediate_modes + )) + + ssa_path = convert_linear_to_ssa(path) + contraction_list = input_modes + contraction_list += intermediate_modes + + for k, (i, j) in enumerate(ssa_path): + modesA = set(contraction_list[i]) + modesB = set(contraction_list[j]) + modesOut = set(intermediate_modes[k]) + assert modesOut.issubset(modesA.union(modesB)) + assert set(output_modes) == set(intermediate_modes[-1]) + + class EinsumFactory: """Take a valid einsum expression and compute shapes, modes, etc for testing.""" @@ -99,17 +165,7 @@ def _gen_shape(self, modes): shape = [] # find ellipsis, record the position, and remove it - if isinstance(modes, str): - ellipsis = modes.find("...") - if ellipsis >= 0: - modes = modes.replace("...", "") - else: - try: - ellipsis = modes.index(Ellipsis) - except ValueError: - ellipsis = -1 - if ellipsis >= 0: - modes = modes[:ellipsis] + modes[ellipsis+1:] + ellipsis, modes = check_ellipsis(modes) # generate extents for remaining modes for mode in modes: @@ -210,3 +266,21 @@ def convert_by_format(self, operands, *, dummy=False): data.append(tuple(self.output_modes)) return data + + +# We use the pytest marker hook to deselect/ignore collected tests +# that we do not want to run. This is better than showing a ton of +# tests as "skipped" at the end, since technically they never get +# tested. +# +# Note the arguments here must be named and ordered in exactly the +# same way as the tests being marked by @pytest.mark.uncollect_if(). +def deselect_contract_tests( + einsum_expr_pack, xp, dtype, *args, **kwargs): + if xp.startswith('torch') and torch is None: + return True + if isinstance(einsum_expr_pack, list): + _, _, _, overwrite_dtype = einsum_expr_pack + if dtype != overwrite_dtype: + return True + return False diff --git a/python/tests/cuquantum_tests/test_cuquantum.py b/python/tests/cuquantum_tests/test_cuquantum.py new file mode 100644 index 0000000..3214a9c --- /dev/null +++ b/python/tests/cuquantum_tests/test_cuquantum.py @@ -0,0 +1,55 @@ +# Copyright (c) 2021-2022, NVIDIA CORPORATION & AFFILIATES +# +# SPDX-License-Identifier: BSD-3-Clause + +import os +import subprocess +import sys + +import pytest + + +# TODO: mark this test as slow and don't run it every time +class TestModuleUtils: + + @pytest.mark.parametrize( + 'includes', (True, False) + ) + @pytest.mark.parametrize( + 'libs', (True, False) + ) + @pytest.mark.parametrize( + 'target', (None, 'custatevec', 'cutensornet', True) + ) + def test_cuquantum(self, includes, libs, target): + # We need to launch a subprocess to have a clean ld state + cmd = [sys.executable, '-m', 'cuquantum'] + if includes: + cmd.append('--includes') + if libs: + cmd.append('--libs') + if target: + if target is True: + cmd.extend(('--target', 'custatevec')) + cmd.extend(('--target', 'cutensornet')) + else: + cmd.extend(('--target', target)) + + result = subprocess.run(cmd, capture_output=True, env=os.environ) + if result.returncode: + if includes is False and libs is False and target is None: + assert result.returncode == 1 + assert 'usage' in result.stdout.decode() + return + msg = f'Got error:\n' + msg += f'stdout: {result.stdout.decode()}\n' + msg += f'stderr: {result.stderr.decode()}\n' + assert False, msg + + out = result.stdout.decode().split() + if includes: + assert any([s.startswith('-I') for s in out]) + if libs: + assert any([s.startswith('-L') for s in out]) + if target: + assert any([s.startswith('-l') for s in out]) diff --git a/python/tests/run_python_tests.sh b/python/tests/run_python_tests.sh new file mode 100755 index 0000000..1ac9050 --- /dev/null +++ b/python/tests/run_python_tests.sh @@ -0,0 +1,116 @@ +#!/bin/bash +# +# Unified launch script for cuquantum-Python tests +# TODO: unify this scripts with others + +set -x + +# The (path to) the MPI launcher. +MPIEXEC=mpirun + +# Open MPI needs this to run inside the docker +export OMPI_ALLOW_RUN_AS_ROOT=1 +export OMPI_ALLOW_RUN_AS_ROOT_CONFIRM=1 + +if [ -z "${CUTENSORNET_BUILD_BINARY_DIR}" ]; then + echo "Error: CUTENSORNET_BUILD_BINARY_DIR is not set" + exit -1 +fi + +echo "Build directory: ${CUTENSORNET_BUILD_BINARY_DIR}" + +# The path to the cuTensorNet-MPI wrapper library. +export CUTENSORNET_COMM_LIB=${CUTENSORNET_BUILD_BINARY_DIR}/../distributed_interfaces/libcutensornet_distributed_interface_mpi.so + +# Show Open MPI info +ompi_info + +# The Python3 executable. +PYTHON3=python3 + +# The path to the Python 'samples' directory. +SAMPLES_DIR=../samples + +# The path to the directory from which pytest collects "cuquantum" tests. +PYTEST_CUQUANTUM_DIR=./cuquantum_tests + +# The path to the directory from which pytest collects "samples" tests. +PYTEST_SAMPLES_DIR=./samples_tests + +########################################################### +# Error function adopted from the Google Shell Style Guide. +########################################################### +error() { + echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $*" >&2 +} + +################################################################################ +# Function to run the specified MPI samples. In case of error, the script name +# and the error code are logged to stderr and a non-zero status corresponding +# to the number of test failures is returned. +# +# Globals: +# MPIEXEC +# PYTHON3 +# Arguments: +# The number of processes to use. +# The list of MPI samples (with path) to run. +################################################################################ +run_python_mpi_samples() { + nproc=$1 + mpi_samples="${@:2}" + status=0 + for mpi_sample in ${mpi_samples}; do + # WAR: our CI only has single GPU, so we cannot launch more than 1 NCCL rank + if [[ "${mpi_sample}" = *"nccl"* ]]; then + nproc=1 + fi + ${MPIEXEC} -np ${nproc} ${PYTHON3} ${mpi_sample} + test_status=$? + if [ ${test_status} -ne 0 ]; then + error "Test \"${mpi_sample}\" exited with status ${test_status}." + fi + status=$((status+${test_status})) + done + return ${status} +} + +STATUS=0 + +################################################################################ +# Tests using pytest. +################################################################################ + +${PYTHON3} -m pytest ${PYTEST_CUQUANTUM_DIR} +test_status=$? +if [ ${test_status} -ne 0 ]; then + error "pytest \"${PYTEST_CUQUANTUM_DIR}\" exited with status ${test_status}." +fi +STATUS=$((STATUS+${test_status})) + +${PYTHON3} -m pytest -n 2 ${PYTEST_SAMPLES_DIR} +test_status=$? +if [ ${test_status} -ne 0 ]; then + error "pytest \"${PYTEST_SAMPLES_DIR}\" exited with status ${test_status}." +fi +STATUS=$((STATUS+${test_status})) + +################################################################################ +# Test MPI samples. +################################################################################ + +# The path to the cuTensorNet-MPI wrapper library. +export CUTENSORNET_COMM_LIB=${CUTENSORNET_BUILD_BINARY_DIR}/../distributed_interfaces/libcutensornet_distributed_interface_mpi.so + +# Find all the MPI sample programs. +mpi_samples=$(find ${SAMPLES_DIR} -name "*_mpi*.py") + +run_python_mpi_samples 2 ${mpi_samples} +test_status=$? +if [ ${test_status} -ne 0 ]; then + error "The MPI samples tests in \"${SAMPLES_DIR}\" exited with status ${test_status}." +fi +STATUS=$((STATUS+${test_status})) + +unset CUTENSORNET_COMM_LIB +exit ${STATUS} diff --git a/samples/custatevec/CMakeLists.txt b/samples/custatevec/CMakeLists.txt index e234579..3b59879 100644 --- a/samples/custatevec/CMakeLists.txt +++ b/samples/custatevec/CMakeLists.txt @@ -69,7 +69,8 @@ set(CMAKE_CUDA_EXTENSIONS OFF) set(CMAKE_CUDA_FLAGS_ARCH_SM70 "-gencode arch=compute_70,code=sm_70") set(CMAKE_CUDA_FLAGS_ARCH_SM75 "-gencode arch=compute_75,code=sm_75") set(CMAKE_CUDA_FLAGS_ARCH_SM80 "-gencode arch=compute_80,code=sm_80 -gencode arch=compute_80,code=compute_80") -set(CMAKE_CUDA_FLAGS_ARCH "${CMAKE_CUDA_FLAGS_ARCH_SM70} ${CMAKE_CUDA_FLAGS_ARCH_SM75} ${CMAKE_CUDA_FLAGS_ARCH_SM80}") +set(CMAKE_CUDA_FLAGS_ARCH_SM90 "-gencode arch=compute_90,code=sm_90 -gencode arch=compute_90,code=compute_90") +set(CMAKE_CUDA_FLAGS_ARCH "${CMAKE_CUDA_FLAGS_ARCH_SM70} ${CMAKE_CUDA_FLAGS_ARCH_SM75} ${CMAKE_CUDA_FLAGS_ARCH_SM80} ${CMAKE_CUDA_FLAGS_ARCH_SM90}") set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} ${CMAKE_CUDA_FLAGS_ARCH}") # ########################################## @@ -104,7 +105,7 @@ function(add_custatevec_example GROUP_TARGET EXAMPLE_NAME EXAMPLE_SOURCES) ${EXAMPLE_TARGET} PROPERTIES CUDA_ARCHITECTURES - "70;75;80" + "70;75;80;90" ) # Install example install( diff --git a/samples/custatevec/Makefile b/samples/custatevec/Makefile index 57ad27a..a96ea9a 100644 --- a/samples/custatevec/Makefile +++ b/samples/custatevec/Makefile @@ -13,7 +13,8 @@ LINKER_FLAGS := -lcudart -lcustatevec ARCH_FLAGS_SM70 = -gencode arch=compute_70,code=sm_70 ARCH_FLAGS_SM75 = -gencode arch=compute_75,code=sm_75 ARCH_FLAGS_SM80 = -gencode arch=compute_80,code=sm_80 -gencode arch=compute_80,code=compute_80 -ARCH_FLAGS = $(ARCH_FLAGS_SM70) $(ARCH_FLAGS_SM75) $(ARCH_FLAGS_SM80) +ARCH_FLAGS_SM90 = -gencode arch=compute_90,code=sm_90 -gencode arch=compute_90,code=compute_90 +ARCH_FLAGS = $(ARCH_FLAGS_SM70) $(ARCH_FLAGS_SM75) $(ARCH_FLAGS_SM80) $(ARCH_FLAGS_SM90) CXX_FLAGS = -std=c++11 $(INCLUDE_DIRS) $(LIBRARY_DIRS) $(ARCH_FLAGS) $(LINKER_FLAGS) diff --git a/samples/custatevec/README.md b/samples/custatevec/README.md index 6f5ec4b..20f2436 100644 --- a/samples/custatevec/README.md +++ b/samples/custatevec/README.md @@ -23,12 +23,12 @@ make -j8 # Support -* **Supported SM Architectures:** SM 7.0, SM 7.5, SM 8.0, SM 8.6 +* **Supported GPU Architectures:** any NVIDIA GPU with compute capability 7.0 or later * **Supported OSes:** Linux * **Supported CPU Architectures**: x86_64, arm64, ppc64le * **Language**: `C++11` # Prerequisites -* [CUDA 11.4 toolkit](https://developer.nvidia.com/cuda-downloads) (or above) and compatible driver (see [CUDA Driver Release Notes](https://docs.nvidia.com/cuda/cuda-toolkit-release-notes/index.html#cuda-major-component-versions)). +* [CUDA 11.8 toolkit](https://developer.nvidia.com/cuda-downloads) (or above) and compatible driver (see [CUDA Driver Release Notes](https://docs.nvidia.com/cuda/cuda-toolkit-release-notes/index.html#cuda-major-component-versions)). * [CMake 3.13](https://cmake.org/download/) or above diff --git a/samples/cutensornet/CMakeLists.txt b/samples/cutensornet/CMakeLists.txt index 0014f62..b372298 100644 --- a/samples/cutensornet/CMakeLists.txt +++ b/samples/cutensornet/CMakeLists.txt @@ -100,6 +100,7 @@ function(add_cutensornet_example GROUP_TARGET EXAMPLE_NAME EXAMPLE_SOURCES) cutensornet cutensor cudart + cusolver cublasLt $<$:MPI::MPI_CXX> ) @@ -128,10 +129,15 @@ endfunction() add_custom_target(cutensornet_examples) add_cutensornet_example(cutensornet_examples "cuTENSORNet.example.tensornet" tensornet_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) +add_cutensornet_example(cutensornet_examples "cuTENSORNet.example.tensornet.mps" approxTN/mps_example.cu) 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) else () - message(WARNING "An MPI installation was not detected. Please install MPI if you would like to build the distributed example(s).") + 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 8bc4189..3ec739a 100644 --- a/samples/cutensornet/Makefile +++ b/samples/cutensornet/Makefile @@ -10,19 +10,26 @@ MPI_ROOT := ${MPI_ROOT} INCLUDE_DIRS := -I${CUTENSORNET_ROOT}/include -I${CUTENSOR_ROOT}/include -I${MPI_ROOT}/include LIBRARY_DIRS := -L${CUTENSORNET_ROOT}/lib -L${CUTENSORNET_ROOT}/lib64 -L${CUTENSOR_ROOT}/lib/11 -LINKER_FLAGS := -lcutensornet -lcutensor -lcudart +LINKER_FLAGS := -lcutensornet -lcutensor -lcudart -lcusolver ARCH_FLAGS_SM70 = -gencode arch=compute_70,code=sm_70 ARCH_FLAGS_SM75 = -gencode arch=compute_75,code=sm_75 ARCH_FLAGS_SM80 = -gencode arch=compute_80,code=sm_80 -gencode arch=compute_80,code=compute_80 -ARCH_FLAGS = $(ARCH_FLAGS_SM70) $(ARCH_FLAGS_SM75) $(ARCH_FLAGS_SM80) +ARCH_FLAGS_SM86 = -gencode arch=compute_86,code=sm_86 -gencode arch=compute_86,code=compute_86 +ARCH_FLAGS_SM90 = -gencode arch=compute_90,code=sm_90 -gencode arch=compute_90,code=compute_90 +ARCH_FLAGS = $(ARCH_FLAGS_SM70) $(ARCH_FLAGS_SM75) $(ARCH_FLAGS_SM80) $(ARCH_FLAGS_SM86) $(ARCH_FLAGS_SM90) CXX_FLAGS = -std=c++11 $(INCLUDE_DIRS) $(LIBRARY_DIRS) $(LINKER_FLAGS) $(ARCH_FLAGS) all: check-env ${CUDA_PATH}/bin/nvcc tensornet_example.cu -o tensornet_example ${CXX_FLAGS} + ${CUDA_PATH}/bin/nvcc approxTN/tensor_svd_example.cu -o tensor_svd_example ${CXX_FLAGS} + ${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} 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.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 endif check-env: @@ -52,4 +59,6 @@ check-env: fi clean: - rm -f tensornet_example tensornet_example.o tensornet_example_mpi tensornet_example_mpi.o + rm -f tensornet_example tensornet_example.o tensornet_example_mpi tensornet_example_mpi.o tensornet_example_mpi_auto tensornet_example_mpi_auto.o + rm -f tensor_qr_example tensor_qr_example.o tensor_svd_example tensor_svd_example.o + rm -f gatesplit_example gatesplit_example.o mps_example mps_example.o diff --git a/samples/cutensornet/README.md b/samples/cutensornet/README.md index 5e59b7f..ff40e0a 100644 --- a/samples/cutensornet/README.md +++ b/samples/cutensornet/README.md @@ -30,15 +30,29 @@ To execute the serial sample in a command shell, simply use: ``` ./tensornet_example ``` -To execute the parallel MPI sample, run: +To execute the parallel MPI sample with automatic MPI parallelization, run: +``` +mpiexec -n N ./tensornet_example_mpi_auto +``` +where `N` is the desired number of processes. You will need to define +the environment variable CUTENSORNET_COMM_LIB as described in the Getting Started +section of the cuTensorNet library documentation (Installation and Compilation). + +To execute the parallel MPI sample with explicit MPI parallelization, run: ``` mpiexec -n N ./tensornet_example_mpi ``` where `N` is the desired number of processes. In this example, `N` can be larger than the number of GPUs in your system. +The tensor SVD sample can be easily executed in a command shell using: +``` +./tensor_svd_example +``` +The sample for tensor QR, gate split and MPS can also be executed in the same fashion. + ## Support -* **Supported SM Architectures:** SM 7.0, SM 7.5, SM 8.0, SM 8.6 +* **Supported SM Architectures:** SM 7.0, SM 7.5, SM 8.0, SM 8.6, SM 9.0 * **Supported OSes:** Linux * **Supported CPU Architectures**: x86_64, aarch64-sbsa, ppc64le * **Language**: C++11 or above @@ -64,9 +78,27 @@ This sample consists of: * Performing the computation of the contraction using `cutensornetContractSlices` for a group of slices (in this case, all of the slices) created (destroyed) using the `cutensornetCreateSliceGroupFromIDRange` (`cutensornetDestroySliceGroup`) API. * Freeing the cuTensorNet resources. -### 2. Parallel execution (`tensornet_example_mpi.cu`) +### 2. Parallel execution (`tensornet_example_mpi_auto.cu`) -The parallel MPI sample illustrates advanced usage of cuTensorNet. Specifically, it demonstrates how to find a contraction path in parallel and how to exploit slice-based parallelism by contracting a subset of slices on each process. +This parallel MPI sample enables automatic distributed parallelization across multiple/many GPUs. +Specifically, it demonstrates how to activate an automatic distributed parallelization inside +the cuTensorNet library such that it will find a contraction path and subsequently contract +the tensor network in parallel using exactly the same source code as in a serial (single-GPU) run. +Currently one will need a CUDA-aware MPI library implementation to run this sample. Please refer +to the Getting Started section of the cuTensorNet library documenation for full details. + +This sample consists of: +* A basic skeleton setting up a simple MPI+CUDA computation using a one GPU per MPI process model. +* Activation call that enables automatic distributed parallelization inside the cuTensorNet library. +* Parallel execution of the tensor network contraction path finder (`cutensornetContractionOptimize`). +* Parallel execution of the tensor network contraction (`cutensornetContractSlices`). + +### 3. Parallel execution via explicit MPI calls (`tensornet_example_mpi.cu`) + +This parallel MPI sample illustrates advanced usage of cuTensorNet. Specifically, it demonstrates +how to find a contraction path in parallel and how to exploit slice-based parallelism by contracting +a subset of slices on each process using manual MPI instrumentation. Note that the previous parallel +sample will do all these for you automatically without any chages to the original (serial) source code. This sample consists of: * A basic skeleton setting up a simple MPI+CUDA computation using a one GPU per process model. @@ -74,3 +106,60 @@ This sample consists of: * Finding an optimal path with `cutensornetContractionOptimize` in parallel, and using global reduction (`MPI_MINLOC`) to find the best path and the owning process's identity. Note that the contraction optimizer on each process sets a different random seed, so each process typically computes a different optimal path for sufficiently large tensor networks. * Broadcasting the winner's `optimizerInfo` object by serializing it using the `cutensornetContractionOptimizerInfoGetPackedSize` and `cutensornetContractionOptimizerInfoPackData` APIs, and deserializing it into an existing `optimizerInfo` object using the `cutensornetUpdateContractionOptimizerInfoFromPackedData` API. * Computing the subset of slice IDs (in a relatively load-balanced fashion) for which each process is responsible, contracting them, and performing a global reduction (sum) to get the final result on the root process. + +### 4. Tensor QR (`approxTN/tensor_qr_example.cu`) + +This sample demonstrates how to use cuTensorNet to perform tensor QR operation. + +This sample consists of: +* Defining input and output tensors using `cutensornetCreateTensorDescriptor`. +* Querying the required workspace for the computation using `cutensornetWorkspaceComputeQRSizes`. +* Performing the computation of tensor QR using `cutensornetTensorQR`. +* Freeing the cuTensorNet resources. + +### 5. Tensor SVD (`approxTN/tensor_svd_example.cu`) + +This sample demonstrates how to use cuTensorNet to perform tensor SVD operation. + +This sample consists of: +* Defining input and output tensors using `cutensornetCreateTensorDescriptor`. Fixed extent truncation can be directly specified by modifying the corresponding extent in the output tensor descriptor. +* Setting up the SVD truncation options using the `cutensornetTensorSVDConfigSetAttribute` function of the `svdConfig` object created by `cutensornetCreateTensorSVDConfig`. +* Optionally, calling `cutensornetCreateTensorSVDInfo` and `cutensornetTensorSVDInfoGetAttribute` to store and retrieve runtime SVD truncation information. +* Querying the required workspace for the computation using `cutensornetWorkspaceComputeSVDSizes`. +* Performing the computation of tensor SVD using `cutensornetTensorSVD`. +* Freeing the cuTensorNet resources. + +### 6. Gate Split (`approxTN/gate_split_example.cu`) + +This sample demonstrates how to use cuTensorNet to perform a single gate split operation. + +This sample consists of: +* Defining input and output tensors using `cutensornetCreateTensorDescriptor`. Fixed extent truncation can be directly specified by modifying the corresponding extent in the output tensor descriptor. +* Setting up the SVD truncation options using the `cutensornetTensorSVDConfigSetAttribute` function of the `svdConfig` object created by `cutensornetCreateTensorSVDConfig`. +* Optionally, calling `cutensornetCreateTensorSVDInfo` and `cutensornetTensorSVDInfoGetAttribute` to store and retrieve runtime SVD truncation information. +* Querying the required workspace for the computation using `cutensornetWorkspaceComputeGateSplitSizes`. The gate split algorithm is specified in `cutensornetGateSplitAlgo_t`. +* Performing the computation of tensor SVD using `cutensornetTensorGateSplit`. +* Freeing the cuTensorNet resources. + +### 7. MPS (`approxTN/mps_example.cu`) + +This sample demonstrates how to integrate cuTensorNet into matrix product states (MPS) simulator. + +This sample is based on an ``MPSHelper`` that can systematically manage the MPS metadata and cuTensorNet library objects. +Following functionalities are encapsulated in this class: +* Dynamically updating the `cutensornetTensorDescriptor_t` for all MPS tensors by calling `cutensornetCreateTensorDescriptor` and `cutensornetDestroyTensorDescriptor`. +* Querying the maximal data size needed for each MPS tensor. +* Setting up the SVD truncation options using the `cutensornetTensorSVDConfigSetAttribute` function of the `svdConfig` object created by `cutensornetCreateTensorSVDConfig`. +* Querying the required workspace size for all gate split operations by calling `cutensornetWorkspaceComputeGateSplitSizes` on the largest problem. +* Optionally, calling `cutensornetCreateTensorSVDInfo` and `cutensornetTensorSVDInfoGetAttribute` to store and retrieve runtime SVD truncation information. +* Performing gate split operations for all gates using `cutensornetTensorGateSplit`. +* Freeing the cuTensorNet resources. +* Finding an optimal contraction path with `cutensornetContractionOptimize` in parallel, + and using global reduction (`MPI_MINLOC`) to find the best path and the owning process's identity. + Note that the contraction optimizer on each process sets a different random seed, so each process + typically computes a different optimal path for sufficiently large tensor networks. +* Broadcasting the winner's `optimizerInfo` object by serializing it using the `cutensornetContractionOptimizerInfoGetPackedSize` + and `cutensornetContractionOptimizerInfoPackData` APIs, and deserializing it into an existing `optimizerInfo` + object using the `cutensornetUpdateContractionOptimizerInfoFromPackedData` API function. +* Computing the subset of slice IDs (in a relatively load-balanced fashion) for which each process is responsible, + contracting them, and performing a global reduction (sum) to get the final result on the root process. diff --git a/samples/cutensornet/approxTN/gate_split_example.cu b/samples/cutensornet/approxTN/gate_split_example.cu new file mode 100644 index 0000000..6658007 --- /dev/null +++ b/samples/cutensornet/approxTN/gate_split_example.cu @@ -0,0 +1,380 @@ +/* + * Copyright (c) 2021-2022, NVIDIA CORPORATION & AFFILIATES. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +// Sphinx: #1 +#include +#include + +#include +#include +#include + +#include +#include + +#define HANDLE_ERROR(x) \ +{ const auto err = x; \ +if( err != CUTENSORNET_STATUS_SUCCESS ) \ +{ printf("Error: %s in line %d\n", cutensornetGetErrorString(err), __LINE__); return err; } \ +}; + +#define HANDLE_CUDA_ERROR(x) \ +{ const auto err = x; \ + if( err != cudaSuccess ) \ + { printf("Error: %s in line %d\n", cudaGetErrorString(err), __LINE__); return err; } \ +}; + +struct GPUTimer +{ + GPUTimer(cudaStream_t stream): stream_(stream) + { + cudaEventCreate(&start_); + cudaEventCreate(&stop_); + } + + ~GPUTimer() + { + cudaEventDestroy(start_); + cudaEventDestroy(stop_); + } + + void start() + { + cudaEventRecord(start_, stream_); + } + + float seconds() + { + cudaEventRecord(stop_, stream_); + cudaEventSynchronize(stop_); + float time; + cudaEventElapsedTime(&time, start_, stop_); + return time * 1e-3; + } + + private: + cudaEvent_t start_, stop_; + cudaStream_t stream_; +}; + +int main() +{ + const size_t cuTensornetVersion = cutensornetGetVersion(); + printf("cuTensorNet-vers:%ld\n",cuTensornetVersion); + + cudaDeviceProp prop; + int deviceId{-1}; + HANDLE_CUDA_ERROR( cudaGetDevice(&deviceId) ); + HANDLE_CUDA_ERROR( cudaGetDeviceProperties(&prop, deviceId) ); + + printf("===== device info ======\n"); + printf("GPU-name:%s\n", prop.name); + printf("GPU-clock:%d\n", prop.clockRate); + printf("GPU-memoryClock:%d\n", prop.memoryClockRate); + printf("GPU-nSM:%d\n", prop.multiProcessorCount); + printf("GPU-major:%d\n", prop.major); + printf("GPU-minor:%d\n", prop.minor); + printf("========================\n"); + + // Sphinx: #2 + /************************************************************************************ + * Gate Split: A_{i,j,k,l} B_{k,o,p,q} G_{m,n,l,o}-> A'_{i,j,x,m} S_{x} B'_{x,n,p,q} + *************************************************************************************/ + typedef float floatType; + cudaDataType_t typeData = CUDA_R_32F; + cutensornetComputeType_t typeCompute = CUTENSORNET_COMPUTE_32F; + + // Create vector of modes + std::vector modesAIn{'i','j','k','l'}; + std::vector modesBIn{'k','o','p','q'}; + std::vector modesGIn{'m','n','l','o'}; // input, G is the gate operator + + std::vector modesAOut{'i','j','x','m'}; + std::vector modesBOut{'x','n','p','q'}; // SVD output + + // Extents + std::unordered_map extent; + extent['i'] = 16; + extent['j'] = 16; + extent['k'] = 16; + extent['l'] = 2; + extent['m'] = 2; + extent['n'] = 2; + extent['o'] = 2; + extent['p'] = 16; + extent['q'] = 16; + + const int64_t maxExtent = 16; //truncate to a maximal extent of 16 + extent['x'] = maxExtent; + + // Create a vector of extents for each tensor + std::vector extentAIn; + for (auto mode : modesAIn) + extentAIn.push_back(extent[mode]); + std::vector extentBIn; + for (auto mode : modesBIn) + extentBIn.push_back(extent[mode]); + std::vector extentGIn; + for (auto mode : modesGIn) + extentGIn.push_back(extent[mode]); + std::vector extentAOut; + for (auto mode : modesAOut) + extentAOut.push_back(extent[mode]); + std::vector extentBOut; + for (auto mode : modesBOut) + extentBOut.push_back(extent[mode]); + + // Sphinx: #3 + /*********************************** + * Allocating data on host and device + ************************************/ + + size_t elementsAIn = 1; + for (auto mode : modesAIn) + elementsAIn *= extent[mode]; + size_t elementsBIn = 1; + for (auto mode : modesBIn) + elementsBIn *= extent[mode]; + size_t elementsGIn = 1; + for (auto mode : modesGIn) + elementsGIn *= extent[mode]; + size_t elementsAOut = 1; + for (auto mode : modesAOut) + elementsAOut *= extent[mode]; + size_t elementsBOut = 1; + for (auto mode : modesBOut) + elementsBOut *= extent[mode]; + + size_t sizeAIn = sizeof(floatType) * elementsAIn; + size_t sizeBIn = sizeof(floatType) * elementsBIn; + size_t sizeGIn = sizeof(floatType) * elementsGIn; + size_t sizeAOut = sizeof(floatType) * elementsAOut; + size_t sizeBOut = sizeof(floatType) * elementsBOut; + size_t sizeS = sizeof(floatType) * extent['x']; + + printf("Total memory: %.2f GiB\n", (sizeAIn + sizeBIn + sizeGIn + sizeAOut + sizeBOut + sizeS)/1024./1024./1024); + + void* D_AIn; + void* D_BIn; + void* D_GIn; + void* D_AOut; + void* D_BOut; + void* D_S; + + HANDLE_CUDA_ERROR( cudaMalloc((void**) &D_AIn, sizeAIn) ); + HANDLE_CUDA_ERROR( cudaMalloc((void**) &D_BIn, sizeBIn) ); + HANDLE_CUDA_ERROR( cudaMalloc((void**) &D_GIn, sizeGIn) ); + HANDLE_CUDA_ERROR( cudaMalloc((void**) &D_AOut, sizeAOut) ); + HANDLE_CUDA_ERROR( cudaMalloc((void**) &D_BOut, sizeBOut) ); + HANDLE_CUDA_ERROR( cudaMalloc((void**) &D_S, sizeS) ); + + floatType *AIn = (floatType*) malloc(sizeAIn); + floatType *BIn = (floatType*) malloc(sizeBIn); + floatType *GIn = (floatType*) malloc(sizeGIn); + + if (AIn == NULL || BIn == NULL || GIn == NULL) + { + printf("Error: Host allocation of tensor data.\n"); + return -1; + } + + /********************** + * Initialize input data + ***********************/ + for (uint64_t i = 0; i < elementsAIn; i++) + AIn[i] = ((floatType) rand())/RAND_MAX; + for (uint64_t i = 0; i < elementsBIn; i++) + BIn[i] = ((floatType) rand())/RAND_MAX; + for (uint64_t i = 0; i < elementsGIn; i++) + GIn[i] = ((floatType) rand())/RAND_MAX; + + HANDLE_CUDA_ERROR( cudaMemcpy(D_AIn, AIn, sizeAIn, cudaMemcpyHostToDevice) ); + HANDLE_CUDA_ERROR( cudaMemcpy(D_BIn, BIn, sizeBIn, cudaMemcpyHostToDevice) ); + HANDLE_CUDA_ERROR( cudaMemcpy(D_GIn, GIn, sizeGIn, cudaMemcpyHostToDevice) ); + + printf("Allocate memory for data, and initialize data.\n"); + + // Sphinx: #4 + /****************** + * cuTensorNet + *******************/ + + cudaStream_t stream; + HANDLE_CUDA_ERROR( cudaStreamCreate(&stream) ); + + cutensornetHandle_t handle; + HANDLE_ERROR( cutensornetCreate(&handle) ); + + /************************** + * Create tensor descriptors + ***************************/ + + cutensornetTensorDescriptor_t descTensorAIn; + cutensornetTensorDescriptor_t descTensorBIn; + cutensornetTensorDescriptor_t descTensorGIn; + cutensornetTensorDescriptor_t descTensorAOut; + cutensornetTensorDescriptor_t descTensorBOut; + + const int32_t numModesAIn = modesAIn.size(); + const int32_t numModesBIn = modesBIn.size(); + const int32_t numModesGIn = modesGIn.size(); + const int32_t numModesAOut = modesAOut.size(); + const int32_t numModesBOut = modesBOut.size(); + + const int64_t* strides = NULL; // assuming fortran layout for all tensors + HANDLE_ERROR( cutensornetCreateTensorDescriptor(handle, numModesAIn, extentAIn.data(), strides, modesAIn.data(), typeData, &descTensorAIn) ); + HANDLE_ERROR( cutensornetCreateTensorDescriptor(handle, numModesBIn, extentBIn.data(), strides, modesBIn.data(), typeData, &descTensorBIn) ); + HANDLE_ERROR( cutensornetCreateTensorDescriptor(handle, numModesGIn, extentGIn.data(), strides, modesGIn.data(), typeData, &descTensorGIn) ); + HANDLE_ERROR( cutensornetCreateTensorDescriptor(handle, numModesAOut, extentAOut.data(), strides, modesAOut.data(), typeData, &descTensorAOut) ); + HANDLE_ERROR( cutensornetCreateTensorDescriptor(handle, numModesBOut, extentBOut.data(), strides, modesBOut.data(), typeData, &descTensorBOut) ); + + printf("Initialize the cuTensorNet library and create tensor descriptors.\n"); + + // Sphinx: #5 + /************************************************** + * Setup gate split truncation options and algorithm + ***************************************************/ + + cutensornetTensorSVDConfig_t svdConfig; + HANDLE_ERROR( cutensornetCreateTensorSVDConfig(handle, &svdConfig) ); + double absCutoff = 1e-2; + HANDLE_ERROR( cutensornetTensorSVDConfigSetAttribute(handle, + svdConfig, + CUTENSORNET_TENSOR_SVD_CONFIG_ABS_CUTOFF, + &absCutoff, + sizeof(absCutoff)) ); + double relCutoff = 1e-2; + HANDLE_ERROR( cutensornetTensorSVDConfigSetAttribute(handle, + svdConfig, + CUTENSORNET_TENSOR_SVD_CONFIG_REL_CUTOFF, + &relCutoff, + sizeof(relCutoff)) ); + + cutensornetGateSplitAlgo_t gateAlgo = CUTENSORNET_GATE_SPLIT_ALGO_REDUCED; + /******************************************************** + * Create SVDInfo to record runtime SVD truncation details + *********************************************************/ + + cutensornetTensorSVDInfo_t svdInfo; + HANDLE_ERROR( cutensornetCreateTensorSVDInfo(handle, &svdInfo)) ; + + // Sphinx: #6 + /************************************** + * Query and allocate required workspace + ***************************************/ + + cutensornetWorkspaceDescriptor_t workDesc; + HANDLE_ERROR( cutensornetCreateWorkspaceDescriptor(handle, &workDesc) ); + + HANDLE_ERROR( cutensornetWorkspaceComputeGateSplitSizes(handle, + descTensorAIn, descTensorBIn, descTensorGIn, + descTensorAOut, descTensorBOut, + gateAlgo, + svdConfig, typeCompute, + workDesc) ); + uint64_t requiredWorkspaceSize = 0; + HANDLE_ERROR( cutensornetWorkspaceGetSize(handle, + workDesc, + CUTENSORNET_WORKSIZE_PREF_MIN, + CUTENSORNET_MEMSPACE_DEVICE, + &requiredWorkspaceSize) ); + void *work = nullptr; + HANDLE_CUDA_ERROR( cudaMalloc(&work, requiredWorkspaceSize) ); + + HANDLE_ERROR( cutensornetWorkspaceSet(handle, + workDesc, + CUTENSORNET_MEMSPACE_DEVICE, + work, + requiredWorkspaceSize) ); + + printf("Allocate workspace.\n"); + + // Sphinx: #7 + /********************** + * Execution + **********************/ + + GPUTimer timer{stream}; + double minTimeCUTENSOR = 1e100; + const int numRuns = 3; // to get stable perf results + for (int i=0; i < numRuns; ++i) + { + // restore output + cudaMemsetAsync(D_AOut, 0, sizeAOut, stream); + cudaMemsetAsync(D_S, 0, sizeS, stream); + cudaMemsetAsync(D_BOut, 0, sizeBOut, stream); + + // With value-based truncation, `cutensornetGateSplit` can potentially update the shared extent in descTensorA/BOut. + // We here restore descTensorA/BOut to the original problem. + HANDLE_ERROR( cutensornetDestroyTensorDescriptor(descTensorAOut) ); + HANDLE_ERROR( cutensornetDestroyTensorDescriptor(descTensorBOut) ); + HANDLE_ERROR( cutensornetCreateTensorDescriptor(handle, numModesAOut, extentAOut.data(), strides, modesAOut.data(), typeData, &descTensorAOut) ); + HANDLE_ERROR( cutensornetCreateTensorDescriptor(handle, numModesBOut, extentBOut.data(), strides, modesBOut.data(), typeData, &descTensorBOut) ); + + cudaDeviceSynchronize(); + timer.start(); + HANDLE_ERROR( cutensornetGateSplit(handle, + descTensorAIn, D_AIn, + descTensorBIn, D_BIn, + descTensorGIn, D_GIn, + descTensorAOut, D_AOut, + D_S, + descTensorBOut, D_BOut, + gateAlgo, + svdConfig, typeCompute, svdInfo, + workDesc, stream) ); + // Synchronize and measure timing + auto time = timer.seconds(); + minTimeCUTENSOR = (minTimeCUTENSOR < time) ? minTimeCUTENSOR : time; + } + + printf("Performing Gate Split\n"); + + // Sphinx: #8 + /************************************* + * Query runtime truncation information + **************************************/ + + double discardedWeight{0}; + int64_t reducedExtent{0}; + cudaDeviceSynchronize(); // device synchronization. + HANDLE_ERROR( cutensornetTensorSVDInfoGetAttribute( handle, svdInfo, CUTENSORNET_TENSOR_SVD_INFO_DISCARDED_WEIGHT, &discardedWeight, sizeof(discardedWeight)) ); + HANDLE_ERROR( cutensornetTensorSVDInfoGetAttribute( handle, svdInfo, CUTENSORNET_TENSOR_SVD_INFO_REDUCED_EXTENT, &reducedExtent, sizeof(reducedExtent)) ); + + printf("elapsed time: %.2f ms\n", minTimeCUTENSOR * 1000.f); + printf("reduced extent found at runtime: %lu\n", reducedExtent); + printf("discarded weight: %.6f\n", discardedWeight); + + // Sphinx: #9 + /*************** + * Free resources + ****************/ + + HANDLE_ERROR( cutensornetDestroyTensorDescriptor(descTensorAIn) ); + HANDLE_ERROR( cutensornetDestroyTensorDescriptor(descTensorBIn) ); + HANDLE_ERROR( cutensornetDestroyTensorDescriptor(descTensorGIn) ); + HANDLE_ERROR( cutensornetDestroyTensorDescriptor(descTensorAOut) ); + HANDLE_ERROR( cutensornetDestroyTensorDescriptor(descTensorBOut) ); + HANDLE_ERROR( cutensornetDestroyTensorSVDConfig(svdConfig) ); + HANDLE_ERROR( cutensornetDestroyTensorSVDInfo(svdInfo) ); + HANDLE_ERROR( cutensornetDestroyWorkspaceDescriptor(workDesc) ); + HANDLE_ERROR( cutensornetDestroy(handle) ); + + if (AIn) free(AIn); + if (BIn) free(BIn); + if (GIn) free(GIn); + if (D_AIn) cudaFree(D_AIn); + if (D_BIn) cudaFree(D_BIn); + if (D_GIn) cudaFree(D_GIn); + if (D_AOut) cudaFree(D_AOut); + if (D_BOut) cudaFree(D_BOut); + if (D_S) cudaFree(D_S); + if (work) cudaFree(work); + + printf("Free resource and exit.\n"); + + return 0; +} diff --git a/samples/cutensornet/approxTN/mps_example.cu b/samples/cutensornet/approxTN/mps_example.cu new file mode 100644 index 0000000..02c1e74 --- /dev/null +++ b/samples/cutensornet/approxTN/mps_example.cu @@ -0,0 +1,690 @@ +/* + * Copyright (c) 2021-2022, NVIDIA CORPORATION & AFFILIATES. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +/**************************************************************** + * Basic Matrix Product State (MPS) Algorithm + * + * Input: + * 1. A-J are MPS tensors + * 2. XXXXX are rank-4 gate tensors: + * + * A---B---C---D---E---F---G---H---I---J MPS tensors + * | | | | | | | | | | + * XXXXX XXXXX XXXXX XXXXX XXXXX gate cycle 0 + * | | | | | | | | | | + * | XXXXX XXXXX XXXXX XXXXX | gate cycle 1 + * | | | | | | | | | | + * XXXXX XXXXX XXXXX XXXXX XXXXX gate cycle 2 + * | | | | | | | | | | + * | XXXXX XXXXX XXXXX XXXXX | gate cycle 3 + * | | | | | | | | | | + * XXXXX XXXXX XXXXX XXXXX XXXXX gate cycle 4 + * | | | | | | | | | | + * | XXXXX XXXXX XXXXX XXXXX | gate cycle 5 + * | | | | | | | | | | + * XXXXX XXXXX XXXXX XXXXX XXXXX gate cycle 6 + * | | | | | | | | | | + * | XXXXX XXXXX XXXXX XXXXX | gate cycle 7 + * | | | | | | | | | | + * + * + * Output: + * 1. maximal virtual extent of the bonds (===) is `maxVirtualExtent` (set by user). + * + * A===B===C===D===E===F===G===H===I===J MPS tensors + * | | | | | | | | | | + * + * + * Algorithm: + * Iterative over the gate cycles, within each cycle, perform gate split operation below for all relevant tensors + * ---A---B---- + * | | GateSplit ---A===B--- + * XXXXX -------> | | + * | | +******************************************************************/ + +// Sphinx: #1 +#define HANDLE_ERROR(x) \ +{ const auto err = x; \ +if( err != CUTENSORNET_STATUS_SUCCESS ) \ +{ std::cout << "Error: " << cutensornetGetErrorString(err) << " in line " << __LINE__ << std::endl; return err;} \ +}; + +#define HANDLE_CUDA_ERROR(x) \ +{ const auto err = x; \ + if( err != cudaSuccess ) \ + { std::cout << "Error: " << cudaGetErrorString(err) << " in line " << __LINE__ << std::endl; return err; } \ +}; + +// Sphinx: #2 +class MPSHelper +{ + public: + /** + * \brief Construct an MPSHelper object for gate splitting algorithm. + * i j k + * -------A-------B------- i j k + * p| |q -------> -------A`-------B`------- + * GGGGGGGGG r| |s + * r| |s + * \param[in] numSites The number of sites in the MPS + * \param[in] physExtent The extent for the physical mode where the gate tensors are acted on. + * \param[in] maxVirtualExtent The maximal extent allowed for the virtual mode shared between adjacent MPS tensors. + * \param[in] initialVirtualExtents A vector of size \p numSites-1 where the ith element denotes the extent of the shared mode for site i and site i+1 in the beginning of the simulation. + * \param[in] typeData The data type for all tensors and gates + * \param[in] typeCompute The compute type for all gate splitting process + */ + MPSHelper(int32_t numSites, + int64_t physExtent, + int64_t maxVirtualExtent, + const std::vector& initialVirtualExtents, + cudaDataType_t typeData, + cutensornetComputeType_t typeCompute); + + /** + * \brief Initialize the MPS metadata and cutensornet library. + */ + cutensornetStatus_t initialize(); + + /** + * \brief Compute the maximal number of elements for each site. + */ + std::vector getMaxTensorElements() const; + + /** + * \brief Update the SVD truncation setting. + * \param[in] absCutoff The cutoff value for absolute singular value truncation. + * \param[in] relCutoff The cutoff value for relative singular value truncation. + * \param[in] renorm The option for renormalization of the truncated singular values. + * \param[in] partition The option for partitioning of the singular values. + */ + cutensornetStatus_t setSVDConfig(double absCutoff, + double relCutoff, + cutensornetTensorSVDNormalization_t renorm, + cutensornetTensorSVDPartition_t partition); + + /** + * \brief Update the algorithm to use for the gating process. + * \param[in] gateAlgo The gate algorithm to use for MPS simulation. + */ + void setGateAlgorithm(cutensornetGateSplitAlgo_t gateAlgo) {gateAlgo_ = gateAlgo;} + + /** + * \brief Compute the maximal workspace needed for MPS gating algorithm. + * \param[out] workspaceSize The required workspace size on the device. + */ + cutensornetStatus_t computeMaxWorkspaceSizes(uint64_t* workspaceSize); + + /** + * \brief Compute the maximal workspace needed for MPS gating algorithm. + * \param[in] work Pointer to the allocated workspace. + * \param[in] workspaceSize The required workspace size on the device. + */ + cutensornetStatus_t setWorkspace(void* work, uint64_t workspaceSize); + + /** + * \brief In-place execution of the apply gate algorithm on \p siteA and \p siteB. + * \param[in] siteA The first site where the gate is applied to. + * \param[in] siteB The second site where the gate is applied to. Must be adjacent to \p siteA. + * \param[in,out] dataInA The data for the MPS tensor at \p siteA. The input will be overwritten with output mps tensor data. + * \param[in,out] dataInB The data for the MPS tensor at \p siteB. The input will be overwritten with output mps tensor data. + * \param[in] dataInG The input data for the gate tensor. + * \param[in] verbose Whether to print out the runtime information regarding truncation. + * \param[in] stream The CUDA stream on which the computation is performed. + */ + cutensornetStatus_t applyGate(uint32_t siteA, + uint32_t siteB, + void* dataInA, + void* dataInB, + const void* dataInG, + bool verbose, + cudaStream_t stream); + + /** + * \brief Free all the tensor descriptors in mpsHelper. + */ + ~MPSHelper() + { + if (inited_) + { + for (auto& descTensor: descTensors_) + { + cutensornetDestroyTensorDescriptor(descTensor); + } + cutensornetDestroy(handle_); + cutensornetDestroyWorkspaceDescriptor(workDesc_); + } + if (svdConfig_ != nullptr) + { + cutensornetDestroyTensorSVDConfig(svdConfig_); + } + if (svdInfo_ != nullptr) + { + cutensornetDestroyTensorSVDInfo(svdInfo_); + } + } + + private: + int32_t numSites_; ///< Number of sites in the MPS + int64_t physExtent_; ///< Extent for the physical index + int64_t maxVirtualExtent_{0}; ///< The maximal extent allowed for the virtual dimension + cudaDataType_t typeData_; + cutensornetComputeType_t typeCompute_; + + bool inited_{false}; + std::vector physModes_; ///< A vector of length \p numSites_ storing the physical mode of each site. + std::vector virtualModes_; ///< A vector of length \p numSites_+1; For site i, virtualModes_[i] and virtualModes_[i+1] represents the left and right virtual mode. + std::vector extentsPerSite_; ///< A vector of length \p numSites_+1; For site i, extentsPerSite_[i] and extentsPerSite_[i+1] represents the left and right virtual extent. + + cutensornetHandle_t handle_; + std::vector descTensors_; /// A vector of length \p numSites_ storing the cutensornetTensorDescriptor_t for each site + cutensornetWorkspaceDescriptor_t workDesc_{nullptr}; + cutensornetTensorSVDConfig_t svdConfig_{nullptr}; + cutensornetTensorSVDInfo_t svdInfo_{nullptr}; + cutensornetGateSplitAlgo_t gateAlgo_{CUTENSORNET_GATE_SPLIT_ALGO_DIRECT}; + int32_t nextMode_{0}; /// The next mode label to use for labelling site tensors and gates. +}; + +// Sphinx: #3 +MPSHelper::MPSHelper(int32_t numSites, + int64_t physExtent, + int64_t maxVirtualExtent, + const std::vector& initialVirtualExtents, + cudaDataType_t typeData, + cutensornetComputeType_t typeCompute) + : numSites_(numSites), + physExtent_(physExtent), + typeData_(typeData), + typeCompute_(typeCompute) +{ + // initialize vectors to store the modes and extents for physical and virtual bond + for (int32_t i=0; i MPSHelper::getMaxTensorElements() const +{ + // compute the maximal tensor sizes for all sites during MPS simulation + std::vector maxTensorElements(numSites_); + int64_t maxLeftExtent = 1; + for (int32_t i=0; i= numSites_) + { + std::cout<< "Site index can not exceed maximal number of sites" << std::endl; + return CUTENSORNET_STATUS_INVALID_VALUE; + } + + auto descTensorInA = descTensors_[siteA]; + auto descTensorInB = descTensors_[siteB]; + + cutensornetTensorDescriptor_t descTensorInG; + + /********************************* + * Create output tensor descriptors + **********************************/ + int32_t physModeInA = physModes_[siteA]; + int32_t physModeInB = physModes_[siteB]; + int32_t physModeOutA = nextMode_++; + int32_t physModeOutB = nextMode_++; + const int32_t modesG[]{physModeInA, physModeInB, physModeOutA, physModeOutB}; + const int64_t extentG[]{physExtent_, physExtent_, physExtent_, physExtent_}; + HANDLE_ERROR( cutensornetCreateTensorDescriptor(handle_, + /*numModes=*/4, + extentG, + /*strides=*/nullptr, // fortran layout + modesG, + typeData_, + &descTensorInG) ); + + int64_t leftExtentA = extentsPerSite_[siteA]; + int64_t extentABIn = extentsPerSite_[siteA+1]; + int64_t rightExtentB = extentsPerSite_[siteA+2]; + // Compute the expected shared extent of output tensor A and B. + int64_t combinedExtentLeft = std::min(leftExtentA, extentABIn*physExtent_) * physExtent_; + int64_t combinedExtentRight = std::min(rightExtentB, extentABIn*physExtent_) * physExtent_; + int64_t extentABOut = std::min({combinedExtentLeft, combinedExtentRight, maxVirtualExtent_}); + + cutensornetTensorDescriptor_t descTensorOutA; + cutensornetTensorDescriptor_t descTensorOutB; + const int32_t modesOutA[]{virtualModes_[siteA], physModeOutA, virtualModes_[siteA+1]}; + const int32_t modesOutB[]{virtualModes_[siteB], physModeOutB, virtualModes_[siteB+1]}; + const int64_t extentOutA[]{leftExtentA, physExtent_, extentABOut}; + const int64_t extentOutB[]{extentABOut, physExtent_, rightExtentB}; + + HANDLE_ERROR( cutensornetCreateTensorDescriptor(handle_, + /*numModes=*/3, + extentOutA, + /*strides=*/nullptr, // fortran layout + modesOutA, + typeData_, + &descTensorOutA) ); + + HANDLE_ERROR( cutensornetCreateTensorDescriptor(handle_, + /*numModes=*/3, + extentOutB, + /*strides=*/nullptr, // fortran layout + modesOutB, + typeData_, + &descTensorOutB) ); + + /********** + * Execution + ***********/ + HANDLE_ERROR( cutensornetGateSplit(handle_, + descTensorInA, dataInA, + descTensorInB, dataInB, + descTensorInG, dataInG, + descTensorOutA, dataInA, // overwrite in place + /*s=*/nullptr, // we partition s equally onto A and B, therefore s is not needed + descTensorOutB, dataInB, // overwrite in place + gateAlgo_, svdConfig_, typeCompute_, + svdInfo_, workDesc_, stream) ); + + /************************** + * Query runtime information + ***************************/ + if (verbose) + { + int64_t fullExtent; + int64_t reducedExtent; + double discardedWeight; + HANDLE_ERROR( cutensornetTensorSVDInfoGetAttribute( handle_, svdInfo_, CUTENSORNET_TENSOR_SVD_INFO_FULL_EXTENT, &fullExtent, sizeof(fullExtent)) ); + HANDLE_ERROR( cutensornetTensorSVDInfoGetAttribute( handle_, svdInfo_, CUTENSORNET_TENSOR_SVD_INFO_REDUCED_EXTENT, &reducedExtent, sizeof(reducedExtent)) ); + HANDLE_ERROR( cutensornetTensorSVDInfoGetAttribute( handle_, svdInfo_, CUTENSORNET_TENSOR_SVD_INFO_DISCARDED_WEIGHT, &discardedWeight, sizeof(discardedWeight)) ); + std::cout << "virtual bond truncated from " << fullExtent << " to " << reducedExtent << " with a discarded weight " << discardedWeight << std::endl; + } + + HANDLE_ERROR( cutensornetDestroyTensorDescriptor(descTensorInA) ); + HANDLE_ERROR( cutensornetDestroyTensorDescriptor(descTensorInB) ); + HANDLE_ERROR( cutensornetDestroyTensorDescriptor(descTensorInG) ); + + // update pointer to the output tensor descriptor and the output shared extent + physModes_[siteA] = physModeOutA; + physModes_[siteB] = physModeOutB; + descTensors_[siteA] = descTensorOutA; + descTensors_[siteB] = descTensorOutB; + + int32_t numModes = 3; + std::vector extentAOut(numModes); + HANDLE_ERROR( cutensornetGetTensorDetails(handle_, descTensorOutA, &numModes, nullptr, nullptr, extentAOut.data(), nullptr) ); + // update the shared extent of output A and B which can potentially get reduced if absCutoff and relCutoff is non-zero. + extentsPerSite_[siteA+1] = extentAOut[2]; // mode label order is always (left_virtual, physical, right_virtual) + return CUTENSORNET_STATUS_SUCCESS; +} + +// Sphinx: #10 +int main() +{ + const size_t cuTensornetVersion = cutensornetGetVersion(); + printf("cuTensorNet-vers:%ld\n",cuTensornetVersion); + + cudaDeviceProp prop; + int deviceId{-1}; + HANDLE_CUDA_ERROR( cudaGetDevice(&deviceId) ); + HANDLE_CUDA_ERROR( cudaGetDeviceProperties(&prop, deviceId) ); + + printf("===== device info ======\n"); + printf("GPU-name:%s\n", prop.name); + printf("GPU-clock:%d\n", prop.clockRate); + printf("GPU-memoryClock:%d\n", prop.memoryClockRate); + printf("GPU-nSM:%d\n", prop.multiProcessorCount); + printf("GPU-major:%d\n", prop.major); + printf("GPU-minor:%d\n", prop.minor); + printf("========================\n"); + + // Sphinx: #11 + /*********************************** + * Step 1: basic MPS setup + ************************************/ + + // setup the simulation setting for the MPS + typedef std::complex complexType; + cudaDataType_t typeData = CUDA_C_64F; + cutensornetComputeType_t typeCompute = CUTENSORNET_COMPUTE_64F; + int32_t numSites = 16; + int64_t physExtent = 2; + int64_t maxVirtualExtent = 12; + const std::vector initialVirtualExtents(numSites-1, 1); // starting MPS with shared extent of 1; + + // initialize an MPSHelper to dynamically update tensor metadats + MPSHelper mpsHelper(numSites, physExtent, maxVirtualExtent, initialVirtualExtents, typeData, typeCompute); + HANDLE_ERROR( mpsHelper.initialize() ); + + // Sphinx: #12 + /*********************************** + * Step 2: data allocation + ************************************/ + + // query largest tensor sizes for the MPS + const std::vector maxElementsPerSite = mpsHelper.getMaxTensorElements(); + std::vector tensors_h; + std::vector tensors_d; + for (int32_t i=0; i + *(complexType*)(data_h) = complexType(1,0); + void* data_d; + HANDLE_CUDA_ERROR( cudaMalloc(&data_d, maxSize) ); + // data transfer from host to device + HANDLE_CUDA_ERROR( cudaMemcpy(data_d, data_h, maxSize, cudaMemcpyHostToDevice) ); + tensors_h.push_back(data_h); + tensors_d.push_back(data_d); + } + + // initialize 4 random gate tensors on host and copy them to device + const int32_t numRandomGates = 4; + const int64_t numGateElements = physExtent * physExtent * physExtent * physExtent; // shape (2, 2, 2, 2) + size_t gateSize = sizeof(complexType) * numGateElements; + complexType* gates_h[numRandomGates]; + void* gates_d[numRandomGates]; + + for (int i=0; i +#include + +#include +#include +#include + +#include +#include + +#define HANDLE_ERROR(x) \ +{ const auto err = x; \ +if( err != CUTENSORNET_STATUS_SUCCESS ) \ +{ printf("Error: %s in line %d\n", cutensornetGetErrorString(err), __LINE__); return err; } \ +}; + +#define HANDLE_CUDA_ERROR(x) \ +{ const auto err = x; \ + if( err != cudaSuccess ) \ + { printf("Error: %s in line %d\n", cudaGetErrorString(err), __LINE__); return err; } \ +}; + +struct GPUTimer +{ + GPUTimer(cudaStream_t stream): stream_(stream) + { + cudaEventCreate(&start_); + cudaEventCreate(&stop_); + } + + ~GPUTimer() + { + cudaEventDestroy(start_); + cudaEventDestroy(stop_); + } + + void start() + { + cudaEventRecord(start_, stream_); + } + + float seconds() + { + cudaEventRecord(stop_, stream_); + cudaEventSynchronize(stop_); + float time; + cudaEventElapsedTime(&time, start_, stop_); + return time * 1e-3; + } + + private: + cudaEvent_t start_, stop_; + cudaStream_t stream_; +}; + +int64_t computeCombinedExtent(const std::unordered_map &extentMap, + const std::vector &modes) +{ + int64_t combinedExtent{1}; + for (auto mode: modes) + { + auto it = extentMap.find(mode); + if (it != extentMap.end()) + combinedExtent *= it->second; + } + return combinedExtent; +} + +int main() +{ + const size_t cuTensornetVersion = cutensornetGetVersion(); + printf("cuTensorNet-vers:%ld\n",cuTensornetVersion); + + cudaDeviceProp prop; + int deviceId{-1}; + HANDLE_CUDA_ERROR( cudaGetDevice(&deviceId) ); + HANDLE_CUDA_ERROR( cudaGetDeviceProperties(&prop, deviceId) ); + + printf("===== device info ======\n"); + printf("GPU-name:%s\n", prop.name); + printf("GPU-clock:%d\n", prop.clockRate); + printf("GPU-memoryClock:%d\n", prop.memoryClockRate); + printf("GPU-nSM:%d\n", prop.multiProcessorCount); + printf("GPU-major:%d\n", prop.major); + printf("GPU-minor:%d\n", prop.minor); + printf("========================\n"); + + // Sphinx: #2 + /********************************************** + * Tensor QR: T_{i,j,m,n} -> Q_{i,x,m} R_{n,x,j} + ***********************************************/ + + typedef float floatType; + cudaDataType_t typeData = CUDA_R_32F; + + // Create vector of modes + int32_t sharedMode = 'x'; + + std::vector modesT{'i','j','m','n'}; // input + std::vector modesQ{'i', sharedMode,'m'}; + std::vector modesR{'n', sharedMode,'j'}; // QR output + + // Extents + std::unordered_map extentMap; + extentMap['i'] = 16; + extentMap['j'] = 16; + extentMap['m'] = 16; + extentMap['n'] = 16; + + int64_t rowExtent = computeCombinedExtent(extentMap, modesQ); + int64_t colExtent = computeCombinedExtent(extentMap, modesR); + + // cuTensorNet tensor QR operates in reduced mode expecting k = min(m, n) + extentMap[sharedMode] = rowExtent <= colExtent? rowExtent: colExtent; + + // Create a vector of extents for each tensor + std::vector extentT; + for (auto mode : modesT) + extentT.push_back(extentMap[mode]); + std::vector extentQ; + for (auto mode : modesQ) + extentQ.push_back(extentMap[mode]); + std::vector extentR; + for (auto mode : modesR) + extentR.push_back(extentMap[mode]); + + // Sphinx: #3 + /*********************************** + * Allocating data on host and device + ************************************/ + + size_t elementsT = 1; + for (auto mode : modesT) + elementsT *= extentMap[mode]; + size_t elementsQ = 1; + for (auto mode : modesQ) + elementsQ *= extentMap[mode]; + size_t elementsR = 1; + for (auto mode : modesR) + elementsR *= extentMap[mode]; + + size_t sizeT = sizeof(floatType) * elementsT; + size_t sizeQ = sizeof(floatType) * elementsQ; + size_t sizeR = sizeof(floatType) * elementsR; + + printf("Total memory: %.2f GiB\n", (sizeT + sizeQ + sizeR)/1024./1024./1024); + + floatType *T = (floatType*) malloc(sizeT); + floatType *Q = (floatType*) malloc(sizeQ); + floatType *R = (floatType*) malloc(sizeR); + + if (T == NULL || Q==NULL || R==NULL ) + { + printf("Error: Host allocation of input T or output Q/R.\n"); + return -1; + } + + void* D_T; + void* D_Q; + void* D_R; + + HANDLE_CUDA_ERROR( cudaMalloc((void**) &D_T, sizeT) ); + HANDLE_CUDA_ERROR( cudaMalloc((void**) &D_Q, sizeQ) ); + HANDLE_CUDA_ERROR( cudaMalloc((void**) &D_R, sizeR) ); + + /**************** + * Initialize data + *****************/ + + for (uint64_t i = 0; i < elementsT; i++) + T[i] = ((floatType) rand())/RAND_MAX; + + HANDLE_CUDA_ERROR( cudaMemcpy(D_T, T, sizeT, cudaMemcpyHostToDevice) ); + printf("Allocate memory for data, and initialize data.\n"); + + // Sphinx: #4 + /****************** + * cuTensorNet + *******************/ + + cudaStream_t stream; + HANDLE_CUDA_ERROR( cudaStreamCreate(&stream) ); + + cutensornetHandle_t handle; + HANDLE_ERROR( cutensornetCreate(&handle) ); + + /*************************** + * Create tensor descriptors + ****************************/ + + cutensornetTensorDescriptor_t descTensorIn; + cutensornetTensorDescriptor_t descTensorQ; + cutensornetTensorDescriptor_t descTensorR; + + const int32_t numModesIn = modesT.size(); + const int32_t numModesQ = modesQ.size(); + const int32_t numModesR = modesR.size(); + + const int64_t* strides = NULL; // assuming fortran layout for all tensors + + HANDLE_ERROR( cutensornetCreateTensorDescriptor(handle, numModesIn, extentT.data(), strides, modesT.data(), typeData, &descTensorIn) ); + HANDLE_ERROR( cutensornetCreateTensorDescriptor(handle, numModesQ, extentQ.data(), strides, modesQ.data(), typeData, &descTensorQ) ); + HANDLE_ERROR( cutensornetCreateTensorDescriptor(handle, numModesR, extentR.data(), strides, modesR.data(), typeData, &descTensorR) ); + + printf("Initialize the cuTensorNet library and create all tensor descriptors.\n"); + + // Sphinx: #5 + /******************************************** + * Query and allocate required workspace sizes + *********************************************/ + + cutensornetWorkspaceDescriptor_t workDesc; + HANDLE_ERROR( cutensornetCreateWorkspaceDescriptor(handle, &workDesc) ); + HANDLE_ERROR( cutensornetWorkspaceComputeQRSizes(handle, descTensorIn, descTensorQ, descTensorR, workDesc) ); + uint64_t hostWorkspaceSize, deviceWorkspaceSize; + + // for tensor QR, it does not matter which cutensornetWorksizePref_t we pick + HANDLE_ERROR( cutensornetWorkspaceGetSize(handle, workDesc, CUTENSORNET_WORKSIZE_PREF_RECOMMENDED, CUTENSORNET_MEMSPACE_DEVICE, &deviceWorkspaceSize) ); + HANDLE_ERROR( cutensornetWorkspaceGetSize(handle, workDesc, CUTENSORNET_WORKSIZE_PREF_RECOMMENDED, CUTENSORNET_MEMSPACE_HOST, &hostWorkspaceSize) ); + + void *devWork = nullptr, *hostWork = nullptr; + if (deviceWorkspaceSize > 0) { + HANDLE_CUDA_ERROR( cudaMalloc(&devWork, deviceWorkspaceSize) ); + } + if (hostWorkspaceSize > 0) { + hostWork = malloc(hostWorkspaceSize); + } + HANDLE_ERROR( cutensornetWorkspaceSet(handle, workDesc, CUTENSORNET_MEMSPACE_DEVICE, devWork, deviceWorkspaceSize) ); + HANDLE_ERROR( cutensornetWorkspaceSet(handle, workDesc, CUTENSORNET_MEMSPACE_HOST, hostWork, hostWorkspaceSize) ); + + // Sphinx: #6 + /********** + * Execution + ***********/ + + GPUTimer timer{stream}; + double minTimeCUTENSOR = 1e100; + const int numRuns = 3; // to get stable perf results + for (int i=0; i < numRuns; ++i) + { + // restore output + cudaMemsetAsync(D_Q, 0, sizeQ, stream); + cudaMemsetAsync(D_R, 0, sizeR, stream); + cudaDeviceSynchronize(); + + timer.start(); + HANDLE_ERROR( cutensornetTensorQR(handle, + descTensorIn, D_T, + descTensorQ, D_Q, + descTensorR, D_R, + workDesc, + stream) ); + // Synchronize and measure timing + auto time = timer.seconds(); + minTimeCUTENSOR = (minTimeCUTENSOR < time) ? minTimeCUTENSOR : time; + } + + printf("Performing QR\n"); + + HANDLE_CUDA_ERROR( cudaMemcpyAsync(Q, D_Q, sizeQ, cudaMemcpyDeviceToHost) ); + HANDLE_CUDA_ERROR( cudaMemcpyAsync(R, D_R, sizeR, cudaMemcpyDeviceToHost) ); + + cudaDeviceSynchronize(); // device synchronization. + printf("%.2f ms\n", minTimeCUTENSOR * 1000.f); + + // Sphinx: #7 + /*************** + * Free resources + ****************/ + + HANDLE_ERROR( cutensornetDestroyTensorDescriptor(descTensorIn) ); + HANDLE_ERROR( cutensornetDestroyTensorDescriptor(descTensorQ) ); + HANDLE_ERROR( cutensornetDestroyTensorDescriptor(descTensorR) ); + HANDLE_ERROR( cutensornetDestroyWorkspaceDescriptor(workDesc) ); + HANDLE_ERROR( cutensornetDestroy(handle) ); + + if (T) free(T); + if (Q) free(Q); + if (R) free(R); + if (D_T) cudaFree(D_T); + if (D_Q) cudaFree(D_Q); + if (D_R) cudaFree(D_R); + if (devWork) cudaFree(devWork); + if (hostWork) free(hostWork); + + printf("Free resource and exit.\n"); + + return 0; +} diff --git a/samples/cutensornet/approxTN/tensor_svd_example.cu b/samples/cutensornet/approxTN/tensor_svd_example.cu new file mode 100644 index 0000000..e8fbf84 --- /dev/null +++ b/samples/cutensornet/approxTN/tensor_svd_example.cu @@ -0,0 +1,355 @@ +/* + * Copyright (c) 2021-2022, NVIDIA CORPORATION & AFFILIATES. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +// Sphinx: #1 +#include +#include + +#include +#include +#include + +#include +#include + +#define HANDLE_ERROR(x) \ +{ const auto err = x; \ +if( err != CUTENSORNET_STATUS_SUCCESS ) \ +{ printf("Error: %s in line %d\n", cutensornetGetErrorString(err), __LINE__); return err; } \ +}; + +#define HANDLE_CUDA_ERROR(x) \ +{ const auto err = x; \ + if( err != cudaSuccess ) \ + { printf("Error: %s in line %d\n", cudaGetErrorString(err), __LINE__); return err; } \ +}; + +struct GPUTimer +{ + GPUTimer(cudaStream_t stream): stream_(stream) + { + cudaEventCreate(&start_); + cudaEventCreate(&stop_); + } + + ~GPUTimer() + { + cudaEventDestroy(start_); + cudaEventDestroy(stop_); + } + + void start() + { + cudaEventRecord(start_, stream_); + } + + float seconds() + { + cudaEventRecord(stop_, stream_); + cudaEventSynchronize(stop_); + float time; + cudaEventElapsedTime(&time, start_, stop_); + return time * 1e-3; + } + + private: + cudaEvent_t start_, stop_; + cudaStream_t stream_; +}; + +int64_t computeCombinedExtent(const std::unordered_map &extentMap, + const std::vector &modes) +{ + int64_t combinedExtent{1}; + for (auto mode: modes) + { + auto it = extentMap.find(mode); + if (it != extentMap.end()) + combinedExtent *= it->second; + } + return combinedExtent; +} + +int main() +{ + const size_t cuTensornetVersion = cutensornetGetVersion(); + printf("cuTensorNet-vers:%ld\n",cuTensornetVersion); + + cudaDeviceProp prop; + int deviceId{-1}; + HANDLE_CUDA_ERROR( cudaGetDevice(&deviceId) ); + HANDLE_CUDA_ERROR( cudaGetDeviceProperties(&prop, deviceId) ); + + printf("===== device info ======\n"); + printf("GPU-name:%s\n", prop.name); + printf("GPU-clock:%d\n", prop.clockRate); + printf("GPU-memoryClock:%d\n", prop.memoryClockRate); + printf("GPU-nSM:%d\n", prop.multiProcessorCount); + printf("GPU-major:%d\n", prop.major); + printf("GPU-minor:%d\n", prop.minor); + printf("========================\n"); + + // Sphinx: #2 + /****************************************************** + * Tensor SVD: T_{i,j,m,n} -> U_{i,x,m} S_{x} V_{n,x,j} + *******************************************************/ + + typedef float floatType; + cudaDataType_t typeData = CUDA_R_32F; + + // Create vector of modes + int32_t sharedMode = 'x'; + + std::vector modesT{'i','j','m','n'}; // input + std::vector modesU{'i', sharedMode,'m'}; + std::vector modesV{'n', sharedMode,'j'}; // SVD output + + // Extents + std::unordered_map extentMap; + extentMap['i'] = 16; + extentMap['j'] = 16; + extentMap['m'] = 16; + extentMap['n'] = 16; + + int64_t rowExtent = computeCombinedExtent(extentMap, modesU); + int64_t colExtent = computeCombinedExtent(extentMap, modesV); + // cuTensorNet tensor SVD operates in reduced mode expecting k <= min(m, n) + int64_t fullSharedExtent = rowExtent <= colExtent? rowExtent: colExtent; + const int64_t maxExtent = fullSharedExtent / 2; //fix extent truncation with half of the singular values trimmed out + extentMap[sharedMode] = maxExtent; + + // Create a vector of extents for each tensor + std::vector extentT; + for (auto mode : modesT) + extentT.push_back(extentMap[mode]); + std::vector extentU; + for (auto mode : modesU) + extentU.push_back(extentMap[mode]); + std::vector extentV; + for (auto mode : modesV) + extentV.push_back(extentMap[mode]); + + // Sphinx: #3 + /*********************************** + * Allocating data on host and device + ************************************/ + + size_t elementsT = 1; + for (auto mode : modesT) + elementsT *= extentMap[mode]; + size_t elementsU = 1; + for (auto mode : modesU) + elementsU *= extentMap[mode]; + size_t elementsV = 1; + for (auto mode : modesV) + elementsV *= extentMap[mode]; + + size_t sizeT = sizeof(floatType) * elementsT; + size_t sizeU = sizeof(floatType) * elementsU; + size_t sizeS = sizeof(floatType) * extentMap[sharedMode]; + size_t sizeV = sizeof(floatType) * elementsV; + + printf("Total memory: %.2f GiB\n", (sizeT + sizeU + sizeS + sizeV)/1024./1024./1024); + + floatType *T = (floatType*) malloc(sizeT); + floatType *U = (floatType*) malloc(sizeU); + floatType *S = (floatType*) malloc(sizeS); + floatType *V = (floatType*) malloc(sizeV); + + if (T == NULL || U==NULL || S==NULL || V==NULL) + { + printf("Error: Host allocation of input T or output U/S/V.\n"); + return -1; + } + + void* D_T; + void* D_U; + void* D_S; + void* D_V; + + HANDLE_CUDA_ERROR( cudaMalloc((void**) &D_T, sizeT) ); + HANDLE_CUDA_ERROR( cudaMalloc((void**) &D_U, sizeU) ); + HANDLE_CUDA_ERROR( cudaMalloc((void**) &D_S, sizeS) ); + HANDLE_CUDA_ERROR( cudaMalloc((void**) &D_V, sizeV) ); + + /**************** + * Initialize data + *****************/ + + for (uint64_t i = 0; i < elementsT; i++) + T[i] = ((floatType) rand())/RAND_MAX; + + HANDLE_CUDA_ERROR( cudaMemcpy(D_T, T, sizeT, cudaMemcpyHostToDevice) ); + printf("Allocate memory for data, and initialize data.\n"); + + // Sphinx: #4 + /****************** + * cuTensorNet + *******************/ + + cudaStream_t stream; + HANDLE_CUDA_ERROR( cudaStreamCreate(&stream) ); + + cutensornetHandle_t handle; + HANDLE_ERROR( cutensornetCreate(&handle) ); + + /************************** + * Create tensor descriptors + ***************************/ + + cutensornetTensorDescriptor_t descTensorIn; + cutensornetTensorDescriptor_t descTensorU; + cutensornetTensorDescriptor_t descTensorV; + + const int32_t numModesIn = modesT.size(); + const int32_t numModesU = modesU.size(); + const int32_t numModesV = modesV.size(); + + const int64_t* strides = NULL; // assuming fortran layout for all tensors + + HANDLE_ERROR( cutensornetCreateTensorDescriptor(handle, numModesIn, extentT.data(), strides, modesT.data(), typeData, &descTensorIn) ); + HANDLE_ERROR( cutensornetCreateTensorDescriptor(handle, numModesU, extentU.data(), strides, modesU.data(), typeData, &descTensorU) ); + HANDLE_ERROR( cutensornetCreateTensorDescriptor(handle, numModesV, extentV.data(), strides, modesV.data(), typeData, &descTensorV) ); + + printf("Initialize the cuTensorNet library and create all tensor descriptors.\n"); + + // Sphinx: #5 + /******************************** + * Setup SVD truncation parameters + *********************************/ + + cutensornetTensorSVDConfig_t svdConfig; + HANDLE_ERROR( cutensornetCreateTensorSVDConfig(handle, &svdConfig) ); + double absCutoff = 1e-2; + HANDLE_ERROR( cutensornetTensorSVDConfigSetAttribute(handle, + svdConfig, + CUTENSORNET_TENSOR_SVD_CONFIG_ABS_CUTOFF, + &absCutoff, + sizeof(absCutoff)) ); + double relCutoff = 4e-2; + HANDLE_ERROR( cutensornetTensorSVDConfigSetAttribute(handle, + svdConfig, + CUTENSORNET_TENSOR_SVD_CONFIG_REL_CUTOFF, + &relCutoff, + sizeof(relCutoff)) ); + + /******************************************************** + * Create SVDInfo to record runtime SVD truncation details + *********************************************************/ + + cutensornetTensorSVDInfo_t svdInfo; + HANDLE_ERROR( cutensornetCreateTensorSVDInfo(handle, &svdInfo)) ; + + // Sphinx: #6 + /************************************************************** + * Query the required workspace sizes and allocate memory + **************************************************************/ + + cutensornetWorkspaceDescriptor_t workDesc; + HANDLE_ERROR( cutensornetCreateWorkspaceDescriptor(handle, &workDesc) ); + HANDLE_ERROR( cutensornetWorkspaceComputeSVDSizes(handle, descTensorIn, descTensorU, descTensorV, svdConfig, workDesc) ); + uint64_t hostWorkspaceSize, deviceWorkspaceSize; + // for tensor SVD, it does not matter which cutensornetWorksizePref_t we pick + HANDLE_ERROR( cutensornetWorkspaceGetSize(handle, workDesc, CUTENSORNET_WORKSIZE_PREF_RECOMMENDED, CUTENSORNET_MEMSPACE_DEVICE, &deviceWorkspaceSize) ); + HANDLE_ERROR( cutensornetWorkspaceGetSize(handle, workDesc, CUTENSORNET_WORKSIZE_PREF_RECOMMENDED, CUTENSORNET_MEMSPACE_HOST, &hostWorkspaceSize) ); + + void *devWork = nullptr, *hostWork = nullptr; + if (deviceWorkspaceSize > 0) { + HANDLE_CUDA_ERROR( cudaMalloc(&devWork, deviceWorkspaceSize) ); + } + if (hostWorkspaceSize > 0) { + hostWork = malloc(hostWorkspaceSize); + } + HANDLE_ERROR( cutensornetWorkspaceSet(handle, workDesc, CUTENSORNET_MEMSPACE_DEVICE, devWork, deviceWorkspaceSize) ); + HANDLE_ERROR( cutensornetWorkspaceSet(handle, workDesc, CUTENSORNET_MEMSPACE_HOST, hostWork, hostWorkspaceSize) ); + + // Sphinx: #7 + /********** + * Execution + ***********/ + + GPUTimer timer{stream}; + double minTimeCUTENSOR = 1e100; + const int numRuns = 3; // to get stable perf results + for (int i=0; i < numRuns; ++i) + { + // restore output + cudaMemsetAsync(D_U, 0, sizeU, stream); + cudaMemsetAsync(D_S, 0, sizeS, stream); + cudaMemsetAsync(D_V, 0, sizeV, stream); + cudaDeviceSynchronize(); + + // With value-based truncation, `cutensornetTensorSVD` can potentially update the shared extent in descTensorU/V. + // We here restore descTensorU/V to the original problem. + HANDLE_ERROR( cutensornetDestroyTensorDescriptor(descTensorU) ); + HANDLE_ERROR( cutensornetDestroyTensorDescriptor(descTensorV) ); + HANDLE_ERROR( cutensornetCreateTensorDescriptor(handle, numModesU, extentU.data(), strides, modesU.data(), typeData, &descTensorU) ); + HANDLE_ERROR( cutensornetCreateTensorDescriptor(handle, numModesV, extentV.data(), strides, modesV.data(), typeData, &descTensorV) ); + + timer.start(); + HANDLE_ERROR( cutensornetTensorSVD(handle, + descTensorIn, D_T, + descTensorU, D_U, + D_S, + descTensorV, D_V, + svdConfig, + svdInfo, + workDesc, + stream) ); + // Synchronize and measure timing + auto time = timer.seconds(); + minTimeCUTENSOR = (minTimeCUTENSOR < time) ? minTimeCUTENSOR : time; + } + + printf("Performing SVD\n"); + + HANDLE_CUDA_ERROR( cudaMemcpyAsync(U, D_U, sizeU, cudaMemcpyDeviceToHost) ); + HANDLE_CUDA_ERROR( cudaMemcpyAsync(S, D_S, sizeS, cudaMemcpyDeviceToHost) ); + HANDLE_CUDA_ERROR( cudaMemcpyAsync(V, D_V, sizeV, cudaMemcpyDeviceToHost) ); + + // Sphinx: #8 + /************************************* + * Query runtime truncation information + **************************************/ + + double discardedWeight{0}; + int64_t reducedExtent{0}; + cudaDeviceSynchronize(); // device synchronization. + HANDLE_ERROR( cutensornetTensorSVDInfoGetAttribute( handle, svdInfo, CUTENSORNET_TENSOR_SVD_INFO_DISCARDED_WEIGHT, &discardedWeight, sizeof(discardedWeight)) ); + HANDLE_ERROR( cutensornetTensorSVDInfoGetAttribute( handle, svdInfo, CUTENSORNET_TENSOR_SVD_INFO_REDUCED_EXTENT, &reducedExtent, sizeof(reducedExtent)) ); + + printf("elapsed time: %.2f ms\n", minTimeCUTENSOR * 1000.f); + printf("reduced extent found at runtime: %lu\n", reducedExtent); + printf("discarded weight: %.2f\n", discardedWeight); + + // Sphinx: #9 + /*************** + * Free resources + ****************/ + + HANDLE_ERROR( cutensornetDestroyTensorDescriptor(descTensorIn) ); + HANDLE_ERROR( cutensornetDestroyTensorDescriptor(descTensorU) ); + HANDLE_ERROR( cutensornetDestroyTensorDescriptor(descTensorV) ); + HANDLE_ERROR( cutensornetDestroyTensorSVDConfig(svdConfig) ); + HANDLE_ERROR( cutensornetDestroyTensorSVDInfo(svdInfo) ); + HANDLE_ERROR( cutensornetDestroyWorkspaceDescriptor(workDesc) ); + HANDLE_ERROR( cutensornetDestroy(handle) ); + + if (T) free(T); + if (U) free(U); + if (S) free(S); + if (V) free(V); + if (D_T) cudaFree(D_T); + if (D_U) cudaFree(D_U); + if (D_S) cudaFree(D_S); + if (D_V) cudaFree(D_V); + if (devWork) cudaFree(devWork); + if (hostWork) free(hostWork); + + printf("Free resource and exit.\n"); + + return 0; +} diff --git a/samples/cutensornet/tensornet_example.cu b/samples/cutensornet/tensornet_example.cu index b6efaab..c14e144 100644 --- a/samples/cutensornet/tensornet_example.cu +++ b/samples/cutensornet/tensornet_example.cu @@ -1,10 +1,11 @@ -/* +/* * Copyright (c) 2021-2022, NVIDIA CORPORATION & AFFILIATES. - * + * * SPDX-License-Identifier: BSD-3-Clause - */ + */ // Sphinx: #1 + #include #include @@ -14,20 +15,25 @@ #include #include -#include + #define HANDLE_ERROR(x) \ { const auto err = x; \ -if( err != CUTENSORNET_STATUS_SUCCESS ) \ -{ printf("Error: %s in line %d\n", cutensornetGetErrorString(err), __LINE__); return err; } \ + if( err != CUTENSORNET_STATUS_SUCCESS ) \ + { printf("Error: %s in line %d\n", cutensornetGetErrorString(err), __LINE__); \ + fflush(stdout); \ + } \ }; #define HANDLE_CUDA_ERROR(x) \ -{ const auto err = x; \ - if( err != cudaSuccess ) \ - { printf("Error: %s in line %d\n", cudaGetErrorString(err), __LINE__); return err; } \ +{ const auto err = x; \ + if( err != cudaSuccess ) \ + { printf("CUDA Error: %s in line %d\n", cudaGetErrorString(err), __LINE__); \ + fflush(stdout); \ + } \ }; + struct GPUTimer { GPUTimer(cudaStream_t stream): stream_(stream) @@ -62,53 +68,72 @@ struct GPUTimer }; -int main() +int main(int argc, char **argv) { - const size_t cuTensornetVersion = cutensornetGetVersion(); - printf("cuTensorNet-vers:%ld\n",cuTensornetVersion); + static_assert(sizeof(size_t) == sizeof(int64_t), "Please build this sample on a 64-bit architecture!"); + bool verbose = true; + + // Check cuTensorNet version + const size_t cuTensornetVersion = cutensornetGetVersion(); + if(verbose) + printf("cuTensorNet version: %ld\n", cuTensornetVersion); + + // Set GPU device + int numDevices {0}; + HANDLE_CUDA_ERROR( cudaGetDeviceCount(&numDevices) ); + const int deviceId = 0; + HANDLE_CUDA_ERROR( cudaSetDevice(deviceId) ); cudaDeviceProp prop; - int deviceId{-1}; - HANDLE_CUDA_ERROR( cudaGetDevice(&deviceId) ); HANDLE_CUDA_ERROR( cudaGetDeviceProperties(&prop, deviceId) ); - printf("===== device info ======\n"); - printf("GPU-name:%s\n", prop.name); - printf("GPU-clock:%d\n", prop.clockRate); - printf("GPU-memoryClock:%d\n", prop.memoryClockRate); - printf("GPU-nSM:%d\n", prop.multiProcessorCount); - printf("GPU-major:%d\n", prop.major); - printf("GPU-minor:%d\n", prop.minor); - printf("========================\n"); + if(verbose) { + printf("===== device info ======\n"); + printf("GPU-name:%s\n", prop.name); + printf("GPU-clock:%d\n", prop.clockRate); + printf("GPU-memoryClock:%d\n", prop.memoryClockRate); + printf("GPU-nSM:%d\n", prop.multiProcessorCount); + printf("GPU-major:%d\n", prop.major); + printf("GPU-minor:%d\n", prop.minor); + printf("========================\n"); + } typedef float floatType; cudaDataType_t typeData = CUDA_R_32F; cutensornetComputeType_t typeCompute = CUTENSORNET_COMPUTE_32F; - printf("Include headers and define data types\n"); + if(verbose) + printf("Included headers and defined data types\n"); // Sphinx: #2 /********************** - * Computing: D_{m,x,n,y} = A_{m,h,k,n} B_{u,k,h} C_{x,u,y} + * Computing: R_{k,l} = A_{a,b,c,d,e,f} B_{b,g,h,e,i,j} C_{m,a,g,f,i,k} D_{l,c,h,d,j,m} **********************/ - constexpr int32_t numInputs = 3; + constexpr int32_t numInputs = 4; - // Create vector of modes - std::vector modesA{'m','h','k','n'}; - std::vector modesB{'u','k','h'}; - std::vector modesC{'x','u','y'}; - std::vector modesD{'m','x','n','y'}; + // Create vectors of tensor modes + std::vector modesA{'a','b','c','d','e','f'}; + std::vector modesB{'b','g','h','e','i','j'}; + std::vector modesC{'m','a','g','f','i','k'}; + std::vector modesD{'l','c','h','d','j','m'}; + std::vector modesR{'k','l'}; - // Extents + // Set mode extents std::unordered_map extent; - extent['m'] = 96; - extent['n'] = 96; - extent['u'] = 96; - extent['h'] = 64; - extent['k'] = 64; - extent['x'] = 64; - extent['y'] = 64; + extent['a'] = 16; + extent['b'] = 16; + extent['c'] = 16; + extent['d'] = 16; + extent['e'] = 16; + extent['f'] = 16; + extent['g'] = 16; + extent['h'] = 16; + extent['i'] = 16; + extent['j'] = 16; + extent['k'] = 16; + extent['l'] = 16; + extent['m'] = 16; // Create a vector of extents for each tensor std::vector extentA; @@ -123,8 +148,12 @@ int main() std::vector extentD; for (auto mode : modesD) extentD.push_back(extent[mode]); + std::vector extentR; + for (auto mode : modesR) + extentR.push_back(extent[mode]); - printf("Define network, modes, and extents\n"); + if(verbose) + printf("Defined tensor network, modes, and extents\n"); // Sphinx: #3 /********************** @@ -143,28 +172,36 @@ int main() size_t elementsD = 1; for (auto mode : modesD) elementsD *= extent[mode]; + size_t elementsR = 1; + for (auto mode : modesR) + elementsR *= extent[mode]; size_t sizeA = sizeof(floatType) * elementsA; size_t sizeB = sizeof(floatType) * elementsB; size_t sizeC = sizeof(floatType) * elementsC; size_t sizeD = sizeof(floatType) * elementsD; - printf("Total memory: %.2f GiB\n", (sizeA + sizeB + sizeC + sizeD)/1024./1024./1024); + size_t sizeR = sizeof(floatType) * elementsR; + if(verbose) + printf("Total GPU memory used for tensor storage: %.2f GiB\n", + (sizeA + sizeB + sizeC + sizeD + sizeR) / 1024. /1024. / 1024); void* rawDataIn_d[numInputs]; - void* D_d; + void* R_d; HANDLE_CUDA_ERROR( cudaMalloc((void**) &rawDataIn_d[0], sizeA) ); HANDLE_CUDA_ERROR( cudaMalloc((void**) &rawDataIn_d[1], sizeB) ); HANDLE_CUDA_ERROR( cudaMalloc((void**) &rawDataIn_d[2], sizeC) ); - HANDLE_CUDA_ERROR( cudaMalloc((void**) &D_d, sizeD)); + HANDLE_CUDA_ERROR( cudaMalloc((void**) &rawDataIn_d[3], sizeD) ); + HANDLE_CUDA_ERROR( cudaMalloc((void**) &R_d, sizeR)); floatType *A = (floatType*) malloc(sizeof(floatType) * elementsA); floatType *B = (floatType*) malloc(sizeof(floatType) * elementsB); floatType *C = (floatType*) malloc(sizeof(floatType) * elementsC); floatType *D = (floatType*) malloc(sizeof(floatType) * elementsD); + floatType *R = (floatType*) malloc(sizeof(floatType) * elementsR); - if (A == NULL || B == NULL || C == NULL || D == NULL) + if (A == NULL || B == NULL || C == NULL || D == NULL || R == NULL) { - printf("Error: Host allocation of A or C.\n"); + printf("Error: Host memory allocation failed!\n"); return -1; } @@ -172,19 +209,23 @@ int main() * Initialize data *******************/ + memset(R, 0, sizeof(floatType) * elementsR); for (uint64_t i = 0; i < elementsA; i++) - A[i] = ((floatType) rand())/RAND_MAX; + A[i] = ((floatType) rand()) / RAND_MAX; for (uint64_t i = 0; i < elementsB; i++) - B[i] = ((floatType) rand())/RAND_MAX; + B[i] = ((floatType) rand()) / RAND_MAX; for (uint64_t i = 0; i < elementsC; i++) - C[i] = ((floatType) rand())/RAND_MAX; - memset(D, 0, sizeof(floatType) * elementsD); + C[i] = ((floatType) rand()) / RAND_MAX; + for (uint64_t i = 0; i < elementsD; i++) + D[i] = ((floatType) rand()) / RAND_MAX; HANDLE_CUDA_ERROR( cudaMemcpy(rawDataIn_d[0], A, sizeA, cudaMemcpyHostToDevice) ); HANDLE_CUDA_ERROR( cudaMemcpy(rawDataIn_d[1], B, sizeB, cudaMemcpyHostToDevice) ); HANDLE_CUDA_ERROR( cudaMemcpy(rawDataIn_d[2], C, sizeC, cudaMemcpyHostToDevice) ); + HANDLE_CUDA_ERROR( cudaMemcpy(rawDataIn_d[3], D, sizeD, cudaMemcpyHostToDevice) ); - printf("Allocate memory for data, and initialize data.\n"); + if(verbose) + printf("Allocated GPU memory for data, and initialize data\n"); // Sphinx: #4 /************************* @@ -201,44 +242,27 @@ int main() const int32_t nmodeB = modesB.size(); const int32_t nmodeC = modesC.size(); const int32_t nmodeD = modesD.size(); + const int32_t nmodeR = modesR.size(); /******************************* * Create Network Descriptor *******************************/ - const int32_t* modesIn[] = {modesA.data(), modesB.data(), modesC.data()}; - int32_t const numModesIn[] = {nmodeA, nmodeB, nmodeC}; - const int64_t* extentsIn[] = {extentA.data(), extentB.data(), extentC.data()}; - const int64_t* stridesIn[] = {NULL, NULL, NULL}; // strides are optional; if no stride is provided, then cuTensorNet assumes a generalized column-major data layout - - // Notice that pointers are allocated via cudaMalloc are aligned to 256 byte - // boundaries by default; however here we're checking the pointer alignment explicitly - // to demonstrate how one would check the alginment for arbitrary pointers. - - auto getMaximalPointerAlignment = [](const void* ptr) { - const uint64_t ptrAddr = reinterpret_cast(ptr); - uint32_t alignment = 1; - while(ptrAddr % alignment == 0 && - alignment < 256) // at the latest we terminate once the alignment reached 256 bytes (we could be going, but any alignment larger or equal to 256 is equally fine) - { - alignment *= 2; - } - return alignment; - }; - const uint32_t alignmentsIn[] = {getMaximalPointerAlignment(rawDataIn_d[0]), - getMaximalPointerAlignment(rawDataIn_d[1]), - getMaximalPointerAlignment(rawDataIn_d[2])}; - const uint32_t alignmentOut = getMaximalPointerAlignment(D_d); - - // setup tensor network + const int32_t* modesIn[] = {modesA.data(), modesB.data(), modesC.data(), modesD.data()}; + int32_t const numModesIn[] = {nmodeA, nmodeB, nmodeC, nmodeD}; + const int64_t* extentsIn[] = {extentA.data(), extentB.data(), extentC.data(), extentD.data()}; + const int64_t* stridesIn[] = {NULL, NULL, NULL, NULL}; // strides are optional; if no stride is provided, cuTensorNet assumes a generalized column-major data layout + + // Set up tensor network cutensornetNetworkDescriptor_t descNet; HANDLE_ERROR( cutensornetCreateNetworkDescriptor(handle, - numInputs, numModesIn, extentsIn, stridesIn, modesIn, alignmentsIn, - nmodeD, extentD.data(), /*stridesOut = */NULL, modesD.data(), alignmentOut, - typeData, typeCompute, - &descNet) ); + numInputs, numModesIn, extentsIn, stridesIn, modesIn, NULL, + nmodeR, extentR.data(), /*stridesOut = */NULL, modesR.data(), + typeData, typeCompute, + &descNet) ); - printf("Initialize the cuTensorNet library and create a network descriptor.\n"); + if(verbose) + printf("Initialized the cuTensorNet library and created a tensor network descriptor\n"); // Sphinx: #5 /******************************* @@ -247,7 +271,9 @@ int main() size_t freeMem, totalMem; HANDLE_CUDA_ERROR( cudaMemGetInfo(&freeMem, &totalMem) ); - uint64_t workspaceLimit = totalMem * 0.9; + uint64_t workspaceLimit = (uint64_t)((double)freeMem * 0.9); + if(verbose) + printf("Workspace limit = %lu\n", workspaceLimit); /******************************* * Find "optimal" contraction order and slicing @@ -256,16 +282,15 @@ int main() cutensornetContractionOptimizerConfig_t optimizerConfig; HANDLE_ERROR( cutensornetCreateContractionOptimizerConfig(handle, &optimizerConfig) ); - // Set the value of the partitioner imbalance factor, if desired - int32_t imbalance_factor = 30; - HANDLE_ERROR( cutensornetContractionOptimizerConfigSetAttribute( - handle, - optimizerConfig, - CUTENSORNET_CONTRACTION_OPTIMIZER_CONFIG_GRAPH_IMBALANCE_FACTOR, - &imbalance_factor, - sizeof(imbalance_factor)) ); - + // Set the desired number of hyper-samples (defaults to 0) + int32_t num_hypersamples = 8; + HANDLE_ERROR( cutensornetContractionOptimizerConfigSetAttribute(handle, + optimizerConfig, + CUTENSORNET_CONTRACTION_OPTIMIZER_CONFIG_HYPER_NUM_SAMPLES, + &num_hypersamples, + sizeof(num_hypersamples)) ); + // Create contraction optimizer info and find an optimized contraction path cutensornetContractionOptimizerInfo_t optimizerInfo; HANDLE_ERROR( cutensornetCreateContractionOptimizerInfo(handle, descNet, &optimizerInfo) ); @@ -275,17 +300,18 @@ int main() workspaceLimit, optimizerInfo) ); + // Query the number of slices the tensor network execution will be split into int64_t numSlices = 0; HANDLE_ERROR( cutensornetContractionOptimizerInfoGetAttribute( - handle, - optimizerInfo, - CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_NUM_SLICES, - &numSlices, - sizeof(numSlices)) ); - + handle, + optimizerInfo, + CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_NUM_SLICES, + &numSlices, + sizeof(numSlices)) ); assert(numSlices > 0); - printf("Find an optimized contraction path with cuTensorNet optimizer.\n"); + if(verbose) + printf("Found an optimized contraction path using cuTensorNet optimizer\n"); // Sphinx: #6 /******************************* @@ -296,49 +322,48 @@ int main() HANDLE_ERROR( cutensornetCreateWorkspaceDescriptor(handle, &workDesc) ); uint64_t requiredWorkspaceSize = 0; - HANDLE_ERROR( cutensornetWorkspaceComputeSizes(handle, - descNet, - optimizerInfo, - workDesc) ); + HANDLE_ERROR( cutensornetWorkspaceComputeContractionSizes(handle, + descNet, + optimizerInfo, + workDesc) ); HANDLE_ERROR( cutensornetWorkspaceGetSize(handle, - workDesc, - CUTENSORNET_WORKSIZE_PREF_MIN, - CUTENSORNET_MEMSPACE_DEVICE, - &requiredWorkspaceSize) ); + workDesc, + CUTENSORNET_WORKSIZE_PREF_MIN, + CUTENSORNET_MEMSPACE_DEVICE, + &requiredWorkspaceSize) ); - void *work = nullptr; + void* work = nullptr; HANDLE_CUDA_ERROR( cudaMalloc(&work, requiredWorkspaceSize) ); HANDLE_ERROR( cutensornetWorkspaceSet(handle, - workDesc, - CUTENSORNET_MEMSPACE_DEVICE, - work, - requiredWorkspaceSize) ); + workDesc, + CUTENSORNET_MEMSPACE_DEVICE, + work, + requiredWorkspaceSize) ); - printf("Allocate workspace.\n"); + if(verbose) + printf("Allocated and set up the GPU workspace\n"); // Sphinx: #7 /******************************* - * Initialize all pair-wise contraction plans (for cuTENSOR). + * Initialize the pairwise contraction plan (for cuTENSOR). *******************************/ cutensornetContractionPlan_t plan; - HANDLE_ERROR( cutensornetCreateContractionPlan(handle, - descNet, - optimizerInfo, - workDesc, - &plan) ); - + descNet, + optimizerInfo, + workDesc, + &plan) ); /******************************* * Optional: Auto-tune cuTENSOR's cutensorContractionPlan to pick the fastest kernel - * for each pairwise contraction. + * for each pairwise tensor contraction. *******************************/ cutensornetContractionAutotunePreference_t autotunePref; HANDLE_ERROR( cutensornetCreateContractionAutotunePreference(handle, - &autotunePref) ); + &autotunePref) ); const int numAutotuningIterations = 5; // may be 0 HANDLE_ERROR( cutensornetContractionAutotunePreferenceSetAttribute( @@ -348,94 +373,112 @@ int main() &numAutotuningIterations, sizeof(numAutotuningIterations)) ); - // modify the plan again to find the best pair-wise contractions + // Modify the plan again to find the best pair-wise contractions HANDLE_ERROR( cutensornetContractionAutotune(handle, - plan, - rawDataIn_d, - D_d, - workDesc, - autotunePref, - stream) ); + plan, + rawDataIn_d, + R_d, + workDesc, + autotunePref, + stream) ); HANDLE_ERROR( cutensornetDestroyContractionAutotunePreference(autotunePref) ); - printf("Create a contraction plan for cuTensorNet and optionally auto-tune it.\n"); + if(verbose) + printf("Created a contraction plan for cuTensorNet and optionally auto-tuned it\n"); // Sphinx: #8 /********************** - * Run + * Execute the tensor network contraction **********************/ - cutensornetSliceGroup_t sliceGroup{}; - // Create a cutensornetSliceGroup_t object from a range of slice IDs. + // Create a cutensornetSliceGroup_t object from a range of slice IDs + cutensornetSliceGroup_t sliceGroup{}; HANDLE_ERROR( cutensornetCreateSliceGroupFromIDRange(handle, 0, numSlices, 1, &sliceGroup) ); - GPUTimer timer{stream}; + GPUTimer timer {stream}; double minTimeCUTENSOR = 1e100; - const int numRuns = 3; // to get stable perf results - for (int i=0; i < numRuns; ++i) + const int numRuns = 3; // number of repeats to get stable performance results + for (int i = 0; i < numRuns; ++i) { - cudaMemcpy(D_d, D, sizeD, cudaMemcpyHostToDevice); // restore output - cudaDeviceSynchronize(); + HANDLE_CUDA_ERROR( cudaMemcpy(R_d, R, sizeR, cudaMemcpyHostToDevice) ); // restore the output tensor on GPU + HANDLE_CUDA_ERROR( cudaDeviceSynchronize() ); /* - * Contract over all slices. - * - * A user may choose to parallelize over the slices across multiple devices. + * Contract all slices of the tensor network */ timer.start(); - int32_t accumulateOutput = 0; + int32_t accumulateOutput = 0; // output tensor data will be overwritten HANDLE_ERROR( cutensornetContractSlices(handle, - plan, - rawDataIn_d, - D_d, - accumulateOutput, - workDesc, - sliceGroup, // Alternatively, NULL can also be used to contract over all the slices instead of specifying a sliceGroup object. - stream) ); - - // Synchronize and measure timing + plan, + rawDataIn_d, + R_d, + accumulateOutput, + workDesc, + sliceGroup, // slternatively, NULL can also be used to contract over all slices instead of specifying a sliceGroup object + stream) ); + + // Synchronize and measure best timing auto time = timer.seconds(); - minTimeCUTENSOR = (minTimeCUTENSOR < time) ? minTimeCUTENSOR : time; + minTimeCUTENSOR = (time > minTimeCUTENSOR) ? minTimeCUTENSOR : time; } - printf("Contract the network, each slice uses the same contraction plan.\n"); + if(verbose) + printf("Contracted the tensor network, each slice used the same contraction plan\n"); + // Print the 1-norm of the output tensor (verification) + HANDLE_CUDA_ERROR( cudaStreamSynchronize(stream) ); + HANDLE_CUDA_ERROR( cudaMemcpy(R, R_d, sizeR, cudaMemcpyDeviceToHost) ); // restore the output tensor on Host + double norm1 = 0.0; + for (int64_t i = 0; i < elementsR; ++i) { + norm1 += std::abs(R[i]); + } + if(verbose) + printf("Computed the 1-norm of the output tensor: %e\n", norm1); /*************************/ - double flops{0.}; + // Query the total Flop count for the tensor network contraction + double flops {0.0}; HANDLE_ERROR( cutensornetContractionOptimizerInfoGetAttribute( - handle, - optimizerInfo, - CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_FLOP_COUNT, - &flops, - sizeof(flops)) ); - - printf("numSlices: %ld\n", numSlices); - printf("%.2f ms / slice\n", minTimeCUTENSOR * 1000.f / numSlices); - printf("%.2f GFLOPS/s\n", flops/1e9/minTimeCUTENSOR ); + handle, + optimizerInfo, + CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_FLOP_COUNT, + &flops, + sizeof(flops)) ); + + if(verbose) { + printf("Number of tensor network slices = %ld\n", numSlices); + printf("Tensor network contraction time (ms) = %.3f\n", minTimeCUTENSOR * 1000.f); + } + // Free cuTensorNet resources HANDLE_ERROR( cutensornetDestroySliceGroup(sliceGroup) ); - HANDLE_ERROR( cutensornetDestroy(handle) ); - HANDLE_ERROR( cutensornetDestroyNetworkDescriptor(descNet) ); HANDLE_ERROR( cutensornetDestroyContractionPlan(plan) ); - HANDLE_ERROR( cutensornetDestroyContractionOptimizerConfig(optimizerConfig) ); - HANDLE_ERROR( cutensornetDestroyContractionOptimizerInfo(optimizerInfo) ); HANDLE_ERROR( cutensornetDestroyWorkspaceDescriptor(workDesc) ); + HANDLE_ERROR( cutensornetDestroyContractionOptimizerInfo(optimizerInfo) ); + HANDLE_ERROR( cutensornetDestroyContractionOptimizerConfig(optimizerConfig) ); + HANDLE_ERROR( cutensornetDestroyNetworkDescriptor(descNet) ); + HANDLE_ERROR( cutensornetDestroy(handle) ); - if (A) free(A); - if (B) free(B); - if (C) free(C); + // Free Host memory resources + if (R) free(R); if (D) free(D); + if (C) free(C); + if (B) free(B); + if (A) free(A); + + // Free GPU memory resources + if (work) cudaFree(work); + if (R_d) cudaFree(R_d); if (rawDataIn_d[0]) cudaFree(rawDataIn_d[0]); if (rawDataIn_d[1]) cudaFree(rawDataIn_d[1]); if (rawDataIn_d[2]) cudaFree(rawDataIn_d[2]); - if (D_d) cudaFree(D_d); - if (work) cudaFree(work); + if (rawDataIn_d[3]) cudaFree(rawDataIn_d[3]); - printf("Free resource and exit.\n"); + if(verbose) + printf("Freed resources and exited\n"); return 0; } diff --git a/samples/cutensornet/tensornet_example_mpi.cu b/samples/cutensornet/tensornet_example_mpi.cu index 2446e60..1629901 100644 --- a/samples/cutensornet/tensornet_example_mpi.cu +++ b/samples/cutensornet/tensornet_example_mpi.cu @@ -6,10 +6,6 @@ // Sphinx: #1 -// Sphinx: MPI #1 [begin] -#include -// Sphinx: MPI #1 [end] - #include #include @@ -20,10 +16,16 @@ #include #include +// Sphinx: MPI #1 [begin] + +#include + +// Sphinx: MPI #1 [end] + #define HANDLE_ERROR(x) \ { const auto err = x; \ if( err != CUTENSORNET_STATUS_SUCCESS ) \ - { printf("[Process %d] Error: %s in line %d\n", rank, cutensornetGetErrorString(err), __LINE__); \ + { printf("Error: %s in line %d\n", cutensornetGetErrorString(err), __LINE__); \ fflush(stdout); \ MPI_Abort(MPI_COMM_WORLD, err); \ } \ @@ -32,7 +34,7 @@ #define HANDLE_CUDA_ERROR(x) \ { const auto err = x; \ if( err != cudaSuccess ) \ - { printf("[Process %d] CUDA Error: %s in line %d\n", rank, cudaGetErrorString(err), __LINE__); \ + { printf("CUDA Error: %s in line %d\n", cudaGetErrorString(err), __LINE__); \ fflush(stdout); \ MPI_Abort(MPI_COMM_WORLD, err) ; \ } \ @@ -45,7 +47,7 @@ if( err != MPI_SUCCESS ) \ { char error[MPI_MAX_ERROR_STRING]; int len; \ MPI_Error_string(err, error, &len); \ - printf("[Process %d] MPI Error: %s in line %d\n", rank, error, __LINE__); \ + printf("MPI Error: %s in line %d\n", error, __LINE__); \ fflush(stdout); \ MPI_Abort(MPI_COMM_WORLD, err); \ } \ @@ -53,7 +55,6 @@ // Sphinx: MPI #2 [end] - struct GPUTimer { GPUTimer(cudaStream_t stream): stream_(stream) @@ -88,57 +89,49 @@ struct GPUTimer }; -int main(int argc, char *argv[]) +int main(int argc, char **argv) { - static_assert(sizeof(size_t) == sizeof(int64_t), "Please build this sample on a 64-bit architecture."); + static_assert(sizeof(size_t) == sizeof(int64_t), "Please build this sample on a 64-bit architecture!"); // Sphinx: MPI #3 [begin] - // Initialize MPI. - int errorCode = MPI_Init(&argc, &argv); - if (errorCode != MPI_SUCCESS) - { - printf("Error initializing MPI.\n"); - MPI_Abort(MPI_COMM_WORLD, errorCode); - } - - const int root{0}; - int rank{}; + // Initialize MPI + HANDLE_MPI_ERROR( MPI_Init(&argc, &argv) ); + int rank {-1}; HANDLE_MPI_ERROR( MPI_Comm_rank(MPI_COMM_WORLD, &rank) ); - - int numProcs{}; + int numProcs {0}; HANDLE_MPI_ERROR( MPI_Comm_size(MPI_COMM_WORLD, &numProcs) ); // Sphinx: MPI #3 [end] - if (rank == root) + bool verbose = (rank == 0) ? true : false; + if (verbose) { - printf("*** Printing is done only from the root process to prevent jumbled messages ***\n"); - printf("The number of processes is %d.\n", numProcs); + printf("*** Printing is done only from the root MPI process to prevent jumbled messages ***\n"); + printf("The number of MPI processes is %d\n", numProcs); } + if(verbose) + printf("Initialized MPI service\n"); - // Get cuTensornet version and device properties. + // Check cuTensorNet version const size_t cuTensornetVersion = cutensornetGetVersion(); - if (rank == root) - printf("cuTensorNet-vers:%ld\n", cuTensornetVersion); - - int numDevices; - HANDLE_CUDA_ERROR( cudaGetDeviceCount(&numDevices) ); - - cudaDeviceProp prop; + if(verbose) + printf("cuTensorNet version: %ld\n", cuTensornetVersion); // Sphinx: MPI #4 [begin] - // Set deviceId based on ranks and nodes. - int deviceId = rank % numDevices; // We assume that the processes are mapped to nodes in contiguous chunks. + // Set GPU device based on ranks and nodes + 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) ); + cudaDeviceProp prop; HANDLE_CUDA_ERROR( cudaGetDeviceProperties(&prop, deviceId) ); // Sphinx: MPI #4 [end] - if (rank == root) - { - printf("===== root process device info ======\n"); + if(verbose) { + printf("===== device info ======\n"); printf("GPU-name:%s\n", prop.name); printf("GPU-clock:%d\n", prop.clockRate); printf("GPU-memoryClock:%d\n", prop.memoryClockRate); @@ -152,33 +145,39 @@ int main(int argc, char *argv[]) MPI_Datatype floatTypeMPI = MPI_FLOAT; cudaDataType_t typeData = CUDA_R_32F; cutensornetComputeType_t typeCompute = CUTENSORNET_COMPUTE_32F; - auto Absolute = fabsf; - if (rank == root) - printf("Include headers and define data types\n"); + if(verbose) + printf("Included headers and defined data types\n"); // Sphinx: #2 /********************** - * Computing: D_{m,x,n,y} = A_{m,h,k,n} B_{u,k,h} C_{x,u,y} + * Computing: R_{k,l} = A_{a,b,c,d,e,f} B_{b,g,h,e,i,j} C_{m,a,g,f,i,k} D_{l,c,h,d,j,m} **********************/ - constexpr int32_t numInputs = 3; + constexpr int32_t numInputs = 4; - // Create vector of modes - std::vector modesA{'m','h','k','n'}; - std::vector modesB{'u','k','h'}; - std::vector modesC{'x','u','y'}; - std::vector modesD{'m','x','n','y'}; + // Create vectors of tensor modes + std::vector modesA{'a','b','c','d','e','f'}; + std::vector modesB{'b','g','h','e','i','j'}; + std::vector modesC{'m','a','g','f','i','k'}; + std::vector modesD{'l','c','h','d','j','m'}; + std::vector modesR{'k','l'}; - // Extents + // Set mode extents std::unordered_map extent; - extent['m'] = 96; - extent['n'] = 96; - extent['u'] = 96; - extent['h'] = 64; - extent['k'] = 64; - extent['x'] = 64; - extent['y'] = 64; + extent['a'] = 16; + extent['b'] = 16; + extent['c'] = 16; + extent['d'] = 16; + extent['e'] = 16; + extent['f'] = 16; + extent['g'] = 16; + extent['h'] = 16; + extent['i'] = 16; + extent['j'] = 16; + extent['k'] = 16; + extent['l'] = 16; + extent['m'] = 16; // Create a vector of extents for each tensor std::vector extentA; @@ -193,9 +192,12 @@ int main(int argc, char *argv[]) std::vector extentD; for (auto mode : modesD) extentD.push_back(extent[mode]); + std::vector extentR; + for (auto mode : modesR) + extentR.push_back(extent[mode]); - if (rank == root) - printf("Define network, modes, and extents\n"); + if(verbose) + printf("Defined tensor network, modes, and extents\n"); // Sphinx: #3 /********************** @@ -214,31 +216,37 @@ int main(int argc, char *argv[]) size_t elementsD = 1; for (auto mode : modesD) elementsD *= extent[mode]; + size_t elementsR = 1; + for (auto mode : modesR) + elementsR *= extent[mode]; size_t sizeA = sizeof(floatType) * elementsA; size_t sizeB = sizeof(floatType) * elementsB; size_t sizeC = sizeof(floatType) * elementsC; size_t sizeD = sizeof(floatType) * elementsD; - if (rank == root) - printf("Total memory: %.2f GiB\n", (sizeA + sizeB + sizeC + sizeD)/1024./1024./1024); + size_t sizeR = sizeof(floatType) * elementsR; + if(verbose) + printf("Total GPU memory used for tensor storage: %.2f GiB\n", + (sizeA + sizeB + sizeC + sizeD + sizeR) / 1024. /1024. / 1024); void* rawDataIn_d[numInputs]; - void* D_d; + void* R_d; HANDLE_CUDA_ERROR( cudaMalloc((void**) &rawDataIn_d[0], sizeA) ); HANDLE_CUDA_ERROR( cudaMalloc((void**) &rawDataIn_d[1], sizeB) ); HANDLE_CUDA_ERROR( cudaMalloc((void**) &rawDataIn_d[2], sizeC) ); - HANDLE_CUDA_ERROR( cudaMalloc((void**) &D_d, sizeD)); + HANDLE_CUDA_ERROR( cudaMalloc((void**) &rawDataIn_d[3], sizeD) ); + HANDLE_CUDA_ERROR( cudaMalloc((void**) &R_d, sizeR)); floatType *A = (floatType*) malloc(sizeof(floatType) * elementsA); floatType *B = (floatType*) malloc(sizeof(floatType) * elementsB); floatType *C = (floatType*) malloc(sizeof(floatType) * elementsC); floatType *D = (floatType*) malloc(sizeof(floatType) * elementsD); + floatType *R = (floatType*) malloc(sizeof(floatType) * elementsR); - if (A == NULL || B == NULL || C == NULL || D == NULL) + if (A == NULL || B == NULL || C == NULL || D == NULL || R == NULL) { - printf("Process %d: Error: Host allocation of A, B, C, or D.\n", rank); - MPI_Abort(MPI_COMM_WORLD, -1); - + printf("Error: Host memory allocation failed!\n"); + return -1; } // Sphinx: MPI #5 [begin] @@ -247,31 +255,35 @@ int main(int argc, char *argv[]) * Initialize data *******************/ - // Rank root creates the tensor data. - if (rank == root) + memset(R, 0, sizeof(floatType) * elementsR); + if(rank == 0) { for (uint64_t i = 0; i < elementsA; i++) - A[i] = ((floatType) rand())/RAND_MAX; + A[i] = ((floatType) rand()) / RAND_MAX; for (uint64_t i = 0; i < elementsB; i++) - B[i] = ((floatType) rand())/RAND_MAX; + B[i] = ((floatType) rand()) / RAND_MAX; for (uint64_t i = 0; i < elementsC; i++) - C[i] = ((floatType) rand())/RAND_MAX; + C[i] = ((floatType) rand()) / RAND_MAX; + for (uint64_t i = 0; i < elementsD; i++) + D[i] = ((floatType) rand()) / RAND_MAX; } - // Broadcast data to all ranks. - HANDLE_MPI_ERROR( MPI_Bcast(A, elementsA, floatTypeMPI, root, MPI_COMM_WORLD) ); - HANDLE_MPI_ERROR( MPI_Bcast(B, elementsB, floatTypeMPI, root, MPI_COMM_WORLD) ); - HANDLE_MPI_ERROR( MPI_Bcast(C, elementsC, floatTypeMPI, root, MPI_COMM_WORLD) ); + // Broadcast input data to all ranks + HANDLE_MPI_ERROR( MPI_Bcast(A, elementsA, floatTypeMPI, 0, MPI_COMM_WORLD) ); + HANDLE_MPI_ERROR( MPI_Bcast(B, elementsB, floatTypeMPI, 0, MPI_COMM_WORLD) ); + HANDLE_MPI_ERROR( MPI_Bcast(C, elementsC, floatTypeMPI, 0, MPI_COMM_WORLD) ); + HANDLE_MPI_ERROR( MPI_Bcast(D, elementsD, floatTypeMPI, 0, MPI_COMM_WORLD) ); - // Copy data onto the device on all ranks. + // Copy data to GPU HANDLE_CUDA_ERROR( cudaMemcpy(rawDataIn_d[0], A, sizeA, cudaMemcpyHostToDevice) ); HANDLE_CUDA_ERROR( cudaMemcpy(rawDataIn_d[1], B, sizeB, cudaMemcpyHostToDevice) ); HANDLE_CUDA_ERROR( cudaMemcpy(rawDataIn_d[2], C, sizeC, cudaMemcpyHostToDevice) ); + HANDLE_CUDA_ERROR( cudaMemcpy(rawDataIn_d[3], D, sizeD, cudaMemcpyHostToDevice) ); - // Sphinx: MPI #5 [end] + if(verbose) + printf("Allocated GPU memory for data, and initialize data\n"); - if (rank == root) - printf("Allocate memory for data, calculate workspace limit, and initialize data.\n"); + // Sphinx: MPI #5 [end] // Sphinx: #4 /************************* @@ -280,6 +292,7 @@ int main(int argc, char *argv[]) cudaStream_t stream; cudaStreamCreate(&stream); + cutensornetHandle_t handle; HANDLE_ERROR( cutensornetCreate(&handle) ); @@ -287,45 +300,27 @@ int main(int argc, char *argv[]) const int32_t nmodeB = modesB.size(); const int32_t nmodeC = modesC.size(); const int32_t nmodeD = modesD.size(); + const int32_t nmodeR = modesR.size(); /******************************* * Create Network Descriptor *******************************/ - const int32_t* modesIn[] = {modesA.data(), modesB.data(), modesC.data()}; - int32_t const numModesIn[] = {nmodeA, nmodeB, nmodeC}; - const int64_t* extentsIn[] = {extentA.data(), extentB.data(), extentC.data()}; - const int64_t* stridesIn[] = {NULL, NULL, NULL}; // strides are optional; if no stride is provided, then cuTensorNet assumes a generalized column-major data layout - - // Notice that pointers are allocated via cudaMalloc are aligned to 256 byte - // boundaries by default; however here we're checking the pointer alignment explicitly - // to demonstrate how one would check the alginment for arbitrary pointers. - - auto getMaximalPointerAlignment = [](const void* ptr) { - const uint64_t ptrAddr = reinterpret_cast(ptr); - uint32_t alignment = 1; - while(ptrAddr % alignment == 0 && - alignment < 256) // at the latest we terminate once the alignment reached 256 bytes (we could be going, but any alignment larger or equal to 256 is equally fine) - { - alignment *= 2; - } - return alignment; - }; - const uint32_t alignmentsIn[] = {getMaximalPointerAlignment(rawDataIn_d[0]), - getMaximalPointerAlignment(rawDataIn_d[1]), - getMaximalPointerAlignment(rawDataIn_d[2])}; - const uint32_t alignmentOut = getMaximalPointerAlignment(D_d); - - // setup tensor network + const int32_t* modesIn[] = {modesA.data(), modesB.data(), modesC.data(), modesD.data()}; + int32_t const numModesIn[] = {nmodeA, nmodeB, nmodeC, nmodeD}; + const int64_t* extentsIn[] = {extentA.data(), extentB.data(), extentC.data(), extentD.data()}; + const int64_t* stridesIn[] = {NULL, NULL, NULL, NULL}; // strides are optional; if no stride is provided, cuTensorNet assumes a generalized column-major data layout + + // Set up tensor network cutensornetNetworkDescriptor_t descNet; HANDLE_ERROR( cutensornetCreateNetworkDescriptor(handle, - numInputs, numModesIn, extentsIn, stridesIn, modesIn, alignmentsIn, - nmodeD, extentD.data(), /*stridesOut = */NULL, modesD.data(), alignmentOut, - typeData, typeCompute, - &descNet) ); + numInputs, numModesIn, extentsIn, stridesIn, modesIn, NULL, + nmodeR, extentR.data(), /*stridesOut = */NULL, modesR.data(), + typeData, typeCompute, + &descNet) ); - if (rank == root) - printf("Initialize the cuTensorNet library and create a network descriptor.\n"); + if(verbose) + printf("Initialized the cuTensorNet library and created a tensor network descriptor\n"); // Sphinx: #5 /******************************* @@ -333,113 +328,117 @@ int main(int argc, char *argv[]) *******************************/ size_t freeMem, totalMem; - HANDLE_CUDA_ERROR( cudaMemGetInfo(&freeMem, &totalMem ) ); - HANDLE_MPI_ERROR( MPI_Allreduce(MPI_IN_PLACE, &totalMem, 1, MPI_INT64_T, MPI_MIN, MPI_COMM_WORLD) ); - uint64_t workspaceLimit = totalMem * 0.9; + HANDLE_CUDA_ERROR( cudaMemGetInfo(&freeMem, &totalMem) ); + uint64_t workspaceLimit = (uint64_t)((double)freeMem * 0.9); + // Make sure all MPI processes will assume the minimal workspace size among all + HANDLE_MPI_ERROR( MPI_Allreduce(MPI_IN_PLACE, &workspaceLimit, 1, MPI_INT64_T, MPI_MIN, MPI_COMM_WORLD) ); + if(verbose) + printf("Workspace limit = %lu\n", workspaceLimit); /******************************* - * Find "optimal" contraction order and slicing + * Find "optimal" contraction order and slicing (in parallel) *******************************/ cutensornetContractionOptimizerConfig_t optimizerConfig; HANDLE_ERROR( cutensornetCreateContractionOptimizerConfig(handle, &optimizerConfig) ); + // Set the desired number of hyper-samples (defaults to 0) + int32_t num_hypersamples = 8; + HANDLE_ERROR( cutensornetContractionOptimizerConfigSetAttribute(handle, + optimizerConfig, + CUTENSORNET_CONTRACTION_OPTIMIZER_CONFIG_HYPER_NUM_SAMPLES, + &num_hypersamples, + sizeof(num_hypersamples)) ); + + // Create contraction optimizer info cutensornetContractionOptimizerInfo_t optimizerInfo; - HANDLE_ERROR(cutensornetCreateContractionOptimizerInfo(handle, descNet, &optimizerInfo) ); + HANDLE_ERROR( cutensornetCreateContractionOptimizerInfo(handle, descNet, &optimizerInfo) ); // Sphinx: MPI #6 [begin] // Compute the path on all ranks so that we can choose the path with the lowest cost. Note that since this is a tiny - // example with 3 operands, all processes will compute the same globally optimal path. This is not the case for large - // tensor networks. For large networks, hyperoptimization is also beneficial and can be enabled by setting the - // optimizer config attribute CUTENSORNET_CONTRACTION_OPTIMIZER_CONFIG_HYPER_NUM_SAMPLES. - - // Force slicing. - int32_t min_slices = numProcs; - HANDLE_ERROR( cutensornetContractionOptimizerConfigSetAttribute( - handle, - optimizerConfig, - CUTENSORNET_CONTRACTION_OPTIMIZER_CONFIG_SLICER_MIN_SLICES, - &min_slices, - sizeof(min_slices)) ); - + // example with 4 operands, all processes will compute the same globally optimal path. This is not the case for large + // tensor networks. For large networks, hyper-optimization does become beneficial. + + // Enforce tensor network slicing (for parallelization) + const int32_t min_slices = numProcs; + HANDLE_ERROR( cutensornetContractionOptimizerConfigSetAttribute(handle, + optimizerConfig, + CUTENSORNET_CONTRACTION_OPTIMIZER_CONFIG_SLICER_MIN_SLICES, + &min_slices, + sizeof(min_slices)) ); + + // Find an optimized tensor network contraction path on each MPI process HANDLE_ERROR( cutensornetContractionOptimize(handle, - descNet, - optimizerConfig, - workspaceLimit, - optimizerInfo) ); + descNet, + optimizerConfig, + workspaceLimit, + optimizerInfo) ); + // Query the obtained Flop count double flops{-1.}; - HANDLE_ERROR( cutensornetContractionOptimizerInfoGetAttribute( - handle, - optimizerInfo, - CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_FLOP_COUNT, - &flops, - sizeof(flops)) ); + HANDLE_ERROR( cutensornetContractionOptimizerInfoGetAttribute(handle, + optimizerInfo, + CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_FLOP_COUNT, + &flops, + sizeof(flops)) ); - // Choose the path with the lowest cost. + // Choose the contraction path with the lowest Flop cost struct { - double value; - int rank; + double value; + int rank; } in{flops, rank}, out; - HANDLE_MPI_ERROR( MPI_Allreduce(&in, &out, 1, MPI_DOUBLE_INT, MPI_MINLOC, MPI_COMM_WORLD) ); - - int sender = out.rank; + const int sender = out.rank; flops = out.value; - if (rank == root) - { - printf("Process %d has the path with the lowest FLOP count %lf.\n", sender, flops); - } - size_t bufSize; + if (verbose) + printf("Process %d has the path with the lowest FLOP count %lf\n", sender, flops); - // Get buffer size for optimizerInfo and broadcast it. + // Get the buffer size for optimizerInfo and broadcast it + size_t bufSize {0}; if (rank == sender) { HANDLE_ERROR( cutensornetContractionOptimizerInfoGetPackedSize(handle, optimizerInfo, &bufSize) ); } - HANDLE_MPI_ERROR( MPI_Bcast(&bufSize, 1, MPI_INT64_T, sender, MPI_COMM_WORLD) ); - // Allocate buffer. + // Allocate a buffer std::vector buffer(bufSize); - // Pack optimizerInfo on sender and broadcast it. + // Pack optimizerInfo on sender and broadcast it if (rank == sender) { HANDLE_ERROR( cutensornetContractionOptimizerInfoPackData(handle, optimizerInfo, buffer.data(), bufSize) ); } - HANDLE_MPI_ERROR( MPI_Bcast(buffer.data(), bufSize, MPI_CHAR, sender, MPI_COMM_WORLD) ); - // Unpack optimizerInfo from buffer. + // Unpack optimizerInfo from the buffer if (rank != sender) { HANDLE_ERROR( cutensornetUpdateContractionOptimizerInfoFromPackedData(handle, buffer.data(), bufSize, optimizerInfo) ); } + // Query the number of slices the tensor network execution will be split into int64_t numSlices = 0; HANDLE_ERROR( cutensornetContractionOptimizerInfoGetAttribute( - handle, - optimizerInfo, - CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_NUM_SLICES, - &numSlices, - sizeof(numSlices)) ); - + handle, + optimizerInfo, + CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_NUM_SLICES, + &numSlices, + sizeof(numSlices)) ); assert(numSlices > 0); - // Calculate each process's share of the slices. - + // Calculate each process's share of the slices int64_t procChunk = numSlices / numProcs; int extra = numSlices % numProcs; int procSliceBegin = rank * procChunk + std::min(rank, extra); - int procSliceEnd = rank == numProcs - 1 ? numSlices : (rank + 1) * procChunk + std::min(rank + 1, extra); + int procSliceEnd = (rank == numProcs - 1) ? numSlices : (rank + 1) * procChunk + std::min(rank + 1, extra); // Sphinx: MPI #6 [end] - if (rank == root) - printf("Find an optimized contraction path with cuTensorNet optimizer.\n"); + if(verbose) + printf("Found an optimized contraction path using cuTensorNet optimizer\n"); // Sphinx: #6 /******************************* @@ -450,49 +449,48 @@ int main(int argc, char *argv[]) HANDLE_ERROR( cutensornetCreateWorkspaceDescriptor(handle, &workDesc) ); uint64_t requiredWorkspaceSize = 0; - HANDLE_ERROR( cutensornetWorkspaceComputeSizes(handle, - descNet, - optimizerInfo, - workDesc) ); + HANDLE_ERROR( cutensornetWorkspaceComputeContractionSizes(handle, + descNet, + optimizerInfo, + workDesc) ); HANDLE_ERROR( cutensornetWorkspaceGetSize(handle, - workDesc, - CUTENSORNET_WORKSIZE_PREF_MIN, - CUTENSORNET_MEMSPACE_DEVICE, - &requiredWorkspaceSize) ); + workDesc, + CUTENSORNET_WORKSIZE_PREF_MIN, + CUTENSORNET_MEMSPACE_DEVICE, + &requiredWorkspaceSize) ); - void *work = nullptr; + void* work = nullptr; HANDLE_CUDA_ERROR( cudaMalloc(&work, requiredWorkspaceSize) ); HANDLE_ERROR( cutensornetWorkspaceSet(handle, - workDesc, - CUTENSORNET_MEMSPACE_DEVICE, - work, - requiredWorkspaceSize) ); + workDesc, + CUTENSORNET_MEMSPACE_DEVICE, + work, + requiredWorkspaceSize) ); - if (rank == root) - printf("Allocate workspace.\n"); + if(verbose) + printf("Allocated and set up the GPU workspace\n"); // Sphinx: #7 /******************************* - * Initialize all pair-wise contraction plans (for cuTENSOR) + * Initialize the pairwise contraction plan (for cuTENSOR). *******************************/ cutensornetContractionPlan_t plan; - HANDLE_ERROR( cutensornetCreateContractionPlan(handle, - descNet, - optimizerInfo, - workDesc, - &plan) ); + descNet, + optimizerInfo, + workDesc, + &plan) ); /******************************* * Optional: Auto-tune cuTENSOR's cutensorContractionPlan to pick the fastest kernel + * for each pairwise tensor contraction. *******************************/ - cutensornetContractionAutotunePreference_t autotunePref; HANDLE_ERROR( cutensornetCreateContractionAutotunePreference(handle, - &autotunePref) ); + &autotunePref) ); const int numAutotuningIterations = 5; // may be 0 HANDLE_ERROR( cutensornetContractionAutotunePreferenceSetAttribute( @@ -502,37 +500,37 @@ int main(int argc, char *argv[]) &numAutotuningIterations, sizeof(numAutotuningIterations)) ); - // modify the plan again to find the best pair-wise contractions + // Modify the plan again to find the best pair-wise contractions HANDLE_ERROR( cutensornetContractionAutotune(handle, - plan, - rawDataIn_d, - D_d, - workDesc, - autotunePref, - stream) ); + plan, + rawDataIn_d, + R_d, + workDesc, + autotunePref, + stream) ); HANDLE_ERROR( cutensornetDestroyContractionAutotunePreference(autotunePref) ); - if (rank == root) - printf("Create a contraction plan for cuTensorNet and optionally auto-tune it.\n"); + if(verbose) + printf("Created a contraction plan for cuTensorNet and optionally auto-tuned it\n"); // Sphinx: #8 /********************** - * Run + * Execute the tensor network contraction (in parallel) **********************/ // Sphinx: MPI #7 [begin] + // Create a cutensornetSliceGroup_t object from a range of slice IDs cutensornetSliceGroup_t sliceGroup{}; - // Create a cutensornetSliceGroup_t object from a range of slice IDs. HANDLE_ERROR( cutensornetCreateSliceGroupFromIDRange(handle, procSliceBegin, procSliceEnd, 1, &sliceGroup) ); // Sphinx: MPI #7 [end] GPUTimer timer{stream}; double minTimeCUTENSOR = 1e100; - const int numRuns = 3; // to get stable perf results - for (int i=0; i < numRuns; ++i) + const int numRuns = 3; // to get stable performance results + for (int i = 0; i < numRuns; ++i) { cudaDeviceSynchronize(); @@ -541,7 +539,7 @@ int main(int argc, char *argv[]) */ timer.start(); - // Don't accumulate into output since we use a one-process-per-gpu model. + // Don't accumulate into output since we use a one-process-per-gpu model int32_t accumulateOutput = 0; // Sphinx: MPI #8 [begin] @@ -549,7 +547,7 @@ int main(int argc, char *argv[]) HANDLE_ERROR( cutensornetContractSlices(handle, plan, rawDataIn_d, - D_d, + R_d, accumulateOutput, workDesc, sliceGroup, @@ -557,109 +555,80 @@ int main(int argc, char *argv[]) // Sphinx: MPI #8 [end] - // Synchronize and measure timing - auto time = timer.seconds(); - minTimeCUTENSOR = (minTimeCUTENSOR < time) ? minTimeCUTENSOR : time; - } + // Sphinx: MPI #9 [begin] - if (rank == root) - printf("Contract the network, all slices within the same rank use the same contraction plan.\n"); + // Perform Allreduce operation on the output tensor + HANDLE_CUDA_ERROR( cudaStreamSynchronize(stream) ); + HANDLE_CUDA_ERROR( cudaMemcpy(R, R_d, sizeR, cudaMemcpyDeviceToHost) ); // restore the output tensor on Host + HANDLE_MPI_ERROR( MPI_Allreduce(MPI_IN_PLACE, R, elementsR, floatTypeMPI, MPI_SUM, MPI_COMM_WORLD) ); - /*************************/ + // Sphinx: MPI #9 [end] - if (rank == root) - { - printf("numSlices: %ld\n", numSlices); - int64_t numSlicesProc = procSliceEnd - procSliceBegin; - printf("numSlices on root process: %ld\n", numSlicesProc); - if (numSlicesProc > 0) - printf("%.2f ms / slice\n", minTimeCUTENSOR * 1000.f / numSlicesProc); + // Measure timing + auto time = timer.seconds(); + minTimeCUTENSOR = (minTimeCUTENSOR < time) ? minTimeCUTENSOR : time; } - HANDLE_ERROR( cutensornetDestroySliceGroup(sliceGroup) ); + if (verbose) + printf("Contracted the tensor network, all slices within the same rank used the same contraction plan.\n"); - HANDLE_CUDA_ERROR( cudaMemcpy(D, D_d, sizeD, cudaMemcpyDeviceToHost) ); - - // Sphinx: MPI #9 [begin] - - // Reduce on root process. - if (rank == root) - { - HANDLE_MPI_ERROR( MPI_Reduce(MPI_IN_PLACE, D, elementsD, floatTypeMPI, MPI_SUM, root, MPI_COMM_WORLD) ); - } - else - { - HANDLE_MPI_ERROR( MPI_Reduce(D, D, elementsD, floatTypeMPI, MPI_SUM, root, MPI_COMM_WORLD) ); + // Print the 1-norm of the output tensor (verification) + double norm1 = 0.0; + for (int64_t i = 0; i < elementsR; ++i) { + norm1 += std::abs(R[i]); } + if(verbose) + printf("Computed the 1-norm of the output tensor: %e\n", norm1); - // Sphinx: MPI #9 [end] - - // Compute the reference result. - if (rank == root) - { - floatType *Reference = (floatType*) malloc(sizeof(floatType) * elementsD); - if (Reference == NULL) - { - printf("Error: Host allocation of Reference.\n"); - MPI_Abort(MPI_COMM_WORLD, -1); - } - - void *Reference_d; - HANDLE_CUDA_ERROR( cudaMalloc((void**) &Reference_d, sizeD) ); + /*************************/ - int32_t accumulateOutput = 0; - HANDLE_ERROR( cutensornetContractSlices(handle, - plan, - rawDataIn_d, - Reference_d, - accumulateOutput, - workDesc, - NULL, // Contract over all the slices. - stream) ); - cudaDeviceSynchronize(); - HANDLE_CUDA_ERROR( cudaMemcpy(Reference, Reference_d, sizeD, cudaMemcpyDeviceToHost) ); - - // Calculate the error. - floatType max{}, maxError{}; - for (int i=0; i < elementsD; ++i) - { - floatType error = Absolute(D[i] - Reference[i]); - if (error > maxError) - maxError = error; - if (Absolute(Reference[i]) > max) - max = Absolute(Reference[i]); - } - printf("The inf norm of the reference result is %f, the maximum absolute error is %f, and the maximum relative error is %e.\n", max, maxError, maxError/max); - - free(Reference); - cudaFree(Reference_d); + // Query the total Flop count for the tensor network contraction + flops = 0.0; + HANDLE_ERROR( cutensornetContractionOptimizerInfoGetAttribute( + handle, + optimizerInfo, + CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_FLOP_COUNT, + &flops, + sizeof(flops)) ); + + if(verbose) { + printf("Number of tensor network slices = %ld\n", numSlices); + printf("Tensor network contraction time (ms) = %.3f\n", minTimeCUTENSOR * 1000.f); } - HANDLE_ERROR( cutensornetDestroy(handle) ); - HANDLE_ERROR( cutensornetDestroyNetworkDescriptor(descNet) ); + // Free cuTensorNet resources + HANDLE_ERROR( cutensornetDestroySliceGroup(sliceGroup) ); HANDLE_ERROR( cutensornetDestroyContractionPlan(plan) ); - HANDLE_ERROR( cutensornetDestroyContractionOptimizerConfig(optimizerConfig) ); - HANDLE_ERROR( cutensornetDestroyContractionOptimizerInfo(optimizerInfo) ); HANDLE_ERROR( cutensornetDestroyWorkspaceDescriptor(workDesc) ); + HANDLE_ERROR( cutensornetDestroyContractionOptimizerInfo(optimizerInfo) ); + HANDLE_ERROR( cutensornetDestroyContractionOptimizerConfig(optimizerConfig) ); + HANDLE_ERROR( cutensornetDestroyNetworkDescriptor(descNet) ); + HANDLE_ERROR( cutensornetDestroy(handle) ); - if (A) free(A); - if (B) free(B); - if (C) free(C); + // Free Host memory resources + if (R) free(R); if (D) free(D); + if (C) free(C); + if (B) free(B); + if (A) free(A); + + // Free GPU memory resources + if (work) cudaFree(work); + if (R_d) cudaFree(R_d); if (rawDataIn_d[0]) cudaFree(rawDataIn_d[0]); if (rawDataIn_d[1]) cudaFree(rawDataIn_d[1]); if (rawDataIn_d[2]) cudaFree(rawDataIn_d[2]); - if (D_d) cudaFree(D_d); - if (work) cudaFree(work); - - if (rank == root) - printf("Free resources and exit.\n"); + if (rawDataIn_d[3]) cudaFree(rawDataIn_d[3]); // Sphinx: MPI #10 [begin] + // Shut down MPI service HANDLE_MPI_ERROR( MPI_Finalize() ); // Sphinx: MPI #10 [end] + if(verbose) + printf("Freed resources and exited\n"); + return 0; } diff --git a/samples/cutensornet/tensornet_example_mpi_auto.cu b/samples/cutensornet/tensornet_example_mpi_auto.cu new file mode 100644 index 0000000..b5da86a --- /dev/null +++ b/samples/cutensornet/tensornet_example_mpi_auto.cu @@ -0,0 +1,564 @@ +/* + * Copyright (c) 2021-2022, NVIDIA CORPORATION & AFFILIATES. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +// Sphinx: #1 + +#include +#include + +#include +#include +#include + +#include +#include + +// Sphinx: MPI #1 [begin] + +#include + +// Sphinx: MPI #1 [end] + +#define HANDLE_ERROR(x) \ +{ const auto err = x; \ + if( err != CUTENSORNET_STATUS_SUCCESS ) \ + { printf("Error: %s in line %d\n", cutensornetGetErrorString(err), __LINE__); \ + fflush(stdout); \ + MPI_Abort(MPI_COMM_WORLD, err); \ + } \ +}; + +#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); \ + MPI_Abort(MPI_COMM_WORLD, err) ; \ + } \ +}; + +// Sphinx: MPI #2 [begin] + +#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); \ + } \ +}; + +// Sphinx: MPI #2 [end] + +struct GPUTimer +{ + GPUTimer(cudaStream_t stream): stream_(stream) + { + cudaEventCreate(&start_); + cudaEventCreate(&stop_); + } + + ~GPUTimer() + { + cudaEventDestroy(start_); + cudaEventDestroy(stop_); + } + + void start() + { + cudaEventRecord(start_, stream_); + } + + float seconds() + { + cudaEventRecord(stop_, stream_); + cudaEventSynchronize(stop_); + float time; + cudaEventElapsedTime(&time, start_, stop_); + return time * 1e-3; + } + + private: + cudaEvent_t start_, stop_; + cudaStream_t stream_; +}; + + +int main(int argc, char **argv) +{ + static_assert(sizeof(size_t) == sizeof(int64_t), "Please build this sample on a 64-bit architecture!"); + + // Sphinx: MPI #3 [begin] + + // Initialize MPI + 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) ); + + // Sphinx: MPI #3 [end] + + bool verbose = (rank == 0) ? true : false; + if (verbose) + { + printf("*** Printing is done only from the root MPI process to prevent jumbled messages ***\n"); + printf("The number of MPI processes is %d\n", numProcs); + } + if(verbose) + printf("Initialized MPI service\n"); + + // Check cuTensorNet version + const size_t cuTensornetVersion = cutensornetGetVersion(); + if(verbose) + printf("cuTensorNet version: %ld\n", cuTensornetVersion); + + // Sphinx: MPI #4 [begin] + + // Set GPU device based on ranks and nodes + 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) ); + cudaDeviceProp prop; + HANDLE_CUDA_ERROR( cudaGetDeviceProperties(&prop, deviceId) ); + + // Sphinx: MPI #4 [end] + + if(verbose) { + printf("===== device info ======\n"); + printf("GPU-name:%s\n", prop.name); + printf("GPU-clock:%d\n", prop.clockRate); + printf("GPU-memoryClock:%d\n", prop.memoryClockRate); + printf("GPU-nSM:%d\n", prop.multiProcessorCount); + printf("GPU-major:%d\n", prop.major); + printf("GPU-minor:%d\n", prop.minor); + printf("========================\n"); + } + + typedef float floatType; + MPI_Datatype floatTypeMPI = MPI_FLOAT; + cudaDataType_t typeData = CUDA_R_32F; + cutensornetComputeType_t typeCompute = CUTENSORNET_COMPUTE_32F; + + if(verbose) + printf("Included headers and defined data types\n"); + + // Sphinx: #2 + /********************** + * Computing: R_{k,l} = A_{a,b,c,d,e,f} B_{b,g,h,e,i,j} C_{m,a,g,f,i,k} D_{l,c,h,d,j,m} + **********************/ + + constexpr int32_t numInputs = 4; + + // Create vectors of tensor modes + std::vector modesA{'a','b','c','d','e','f'}; + std::vector modesB{'b','g','h','e','i','j'}; + std::vector modesC{'m','a','g','f','i','k'}; + std::vector modesD{'l','c','h','d','j','m'}; + std::vector modesR{'k','l'}; + + // Set mode extents + std::unordered_map extent; + extent['a'] = 16; + extent['b'] = 16; + extent['c'] = 16; + extent['d'] = 16; + extent['e'] = 16; + extent['f'] = 16; + extent['g'] = 16; + extent['h'] = 16; + extent['i'] = 16; + extent['j'] = 16; + extent['k'] = 16; + extent['l'] = 16; + extent['m'] = 16; + + // Create a vector of extents for each tensor + std::vector extentA; + for (auto mode : modesA) + extentA.push_back(extent[mode]); + std::vector extentB; + for (auto mode : modesB) + extentB.push_back(extent[mode]); + std::vector extentC; + for (auto mode : modesC) + extentC.push_back(extent[mode]); + std::vector extentD; + for (auto mode : modesD) + extentD.push_back(extent[mode]); + std::vector extentR; + for (auto mode : modesR) + extentR.push_back(extent[mode]); + + if(verbose) + printf("Defined tensor network, modes, and extents\n"); + + // Sphinx: #3 + /********************** + * Allocating data + **********************/ + + size_t elementsA = 1; + for (auto mode : modesA) + elementsA *= extent[mode]; + size_t elementsB = 1; + for (auto mode : modesB) + elementsB *= extent[mode]; + size_t elementsC = 1; + for (auto mode : modesC) + elementsC *= extent[mode]; + size_t elementsD = 1; + for (auto mode : modesD) + elementsD *= extent[mode]; + size_t elementsR = 1; + for (auto mode : modesR) + elementsR *= extent[mode]; + + size_t sizeA = sizeof(floatType) * elementsA; + size_t sizeB = sizeof(floatType) * elementsB; + size_t sizeC = sizeof(floatType) * elementsC; + size_t sizeD = sizeof(floatType) * elementsD; + size_t sizeR = sizeof(floatType) * elementsR; + if(verbose) + printf("Total GPU memory used for tensor storage: %.2f GiB\n", + (sizeA + sizeB + sizeC + sizeD + sizeR) / 1024. /1024. / 1024); + + void* rawDataIn_d[numInputs]; + void* R_d; + HANDLE_CUDA_ERROR( cudaMalloc((void**) &rawDataIn_d[0], sizeA) ); + HANDLE_CUDA_ERROR( cudaMalloc((void**) &rawDataIn_d[1], sizeB) ); + HANDLE_CUDA_ERROR( cudaMalloc((void**) &rawDataIn_d[2], sizeC) ); + HANDLE_CUDA_ERROR( cudaMalloc((void**) &rawDataIn_d[3], sizeD) ); + HANDLE_CUDA_ERROR( cudaMalloc((void**) &R_d, sizeR)); + + floatType *A = (floatType*) malloc(sizeof(floatType) * elementsA); + floatType *B = (floatType*) malloc(sizeof(floatType) * elementsB); + floatType *C = (floatType*) malloc(sizeof(floatType) * elementsC); + floatType *D = (floatType*) malloc(sizeof(floatType) * elementsD); + floatType *R = (floatType*) malloc(sizeof(floatType) * elementsR); + + if (A == NULL || B == NULL || C == NULL || D == NULL || R == NULL) + { + printf("Error: Host memory allocation failed!\n"); + return -1; + } + + // Sphinx: MPI #5 [begin] + + /******************* + * Initialize data + *******************/ + + memset(R, 0, sizeof(floatType) * elementsR); + if(rank == 0) + { + for (uint64_t i = 0; i < elementsA; i++) + A[i] = ((floatType) rand()) / RAND_MAX; + for (uint64_t i = 0; i < elementsB; i++) + B[i] = ((floatType) rand()) / RAND_MAX; + for (uint64_t i = 0; i < elementsC; i++) + C[i] = ((floatType) rand()) / RAND_MAX; + for (uint64_t i = 0; i < elementsD; i++) + D[i] = ((floatType) rand()) / RAND_MAX; + } + + // Broadcast input data to all ranks + HANDLE_MPI_ERROR( MPI_Bcast(A, elementsA, floatTypeMPI, 0, MPI_COMM_WORLD) ); + HANDLE_MPI_ERROR( MPI_Bcast(B, elementsB, floatTypeMPI, 0, MPI_COMM_WORLD) ); + HANDLE_MPI_ERROR( MPI_Bcast(C, elementsC, floatTypeMPI, 0, MPI_COMM_WORLD) ); + HANDLE_MPI_ERROR( MPI_Bcast(D, elementsD, floatTypeMPI, 0, MPI_COMM_WORLD) ); + + // Copy data to GPU + HANDLE_CUDA_ERROR( cudaMemcpy(rawDataIn_d[0], A, sizeA, cudaMemcpyHostToDevice) ); + HANDLE_CUDA_ERROR( cudaMemcpy(rawDataIn_d[1], B, sizeB, cudaMemcpyHostToDevice) ); + HANDLE_CUDA_ERROR( cudaMemcpy(rawDataIn_d[2], C, sizeC, cudaMemcpyHostToDevice) ); + HANDLE_CUDA_ERROR( cudaMemcpy(rawDataIn_d[3], D, sizeD, cudaMemcpyHostToDevice) ); + + if(verbose) + printf("Allocated GPU memory for data, and initialize data\n"); + + // Sphinx: MPI #5 [end] + + // Sphinx: #4 + /************************* + * cuTensorNet + *************************/ + + cudaStream_t stream; + cudaStreamCreate(&stream); + + cutensornetHandle_t handle; + HANDLE_ERROR( cutensornetCreate(&handle) ); + + const int32_t nmodeA = modesA.size(); + const int32_t nmodeB = modesB.size(); + const int32_t nmodeC = modesC.size(); + const int32_t nmodeD = modesD.size(); + const int32_t nmodeR = modesR.size(); + + /******************************* + * Create Network Descriptor + *******************************/ + + const int32_t* modesIn[] = {modesA.data(), modesB.data(), modesC.data(), modesD.data()}; + int32_t const numModesIn[] = {nmodeA, nmodeB, nmodeC, nmodeD}; + const int64_t* extentsIn[] = {extentA.data(), extentB.data(), extentC.data(), extentD.data()}; + const int64_t* stridesIn[] = {NULL, NULL, NULL, NULL}; // strides are optional; if no stride is provided, cuTensorNet assumes a generalized column-major data layout + + // Set up tensor network + cutensornetNetworkDescriptor_t descNet; + HANDLE_ERROR( cutensornetCreateNetworkDescriptor(handle, + numInputs, numModesIn, extentsIn, stridesIn, modesIn, NULL, + nmodeR, extentR.data(), /*stridesOut = */NULL, modesR.data(), + typeData, typeCompute, + &descNet) ); + + if(verbose) + printf("Initialized the cuTensorNet library and created a tensor network descriptor\n"); + + // Sphinx: #5 + /******************************* + * Choose workspace limit based on available resources. + *******************************/ + + size_t freeMem, totalMem; + HANDLE_CUDA_ERROR( cudaMemGetInfo(&freeMem, &totalMem) ); + uint64_t workspaceLimit = (uint64_t)((double)freeMem * 0.9); + if(verbose) + printf("Workspace limit = %lu\n", workspaceLimit); + + // Sphinx: MPI #6 [begin] + + /******************************* + * Activate distributed (parallel) execution prior to + * calling contraction path finder and contraction executor + *******************************/ + // HANDLE_ERROR( cutensornetDistributedResetConfiguration(handle, NULL, 0) ); // resets back to serial execution + MPI_Comm cutnComm; + HANDLE_MPI_ERROR( MPI_Comm_dup(MPI_COMM_WORLD, &cutnComm) ); // duplicate MPI communicator + HANDLE_ERROR( cutensornetDistributedResetConfiguration(handle, &cutnComm, sizeof(cutnComm)) ); + if(verbose) + printf("Reset distributed MPI configuration\n"); + + // Sphinx: MPI #6 [end] + + /******************************* + * Find "optimal" contraction order and slicing (in parallel) + *******************************/ + + cutensornetContractionOptimizerConfig_t optimizerConfig; + HANDLE_ERROR( cutensornetCreateContractionOptimizerConfig(handle, &optimizerConfig) ); + + // Set the desired number of hyper-samples (defaults to 0) + int32_t num_hypersamples = 8; + HANDLE_ERROR( cutensornetContractionOptimizerConfigSetAttribute(handle, + optimizerConfig, + CUTENSORNET_CONTRACTION_OPTIMIZER_CONFIG_HYPER_NUM_SAMPLES, + &num_hypersamples, + sizeof(num_hypersamples)) ); + + // Create contraction optimizer info and find an optimized contraction path + cutensornetContractionOptimizerInfo_t optimizerInfo; + HANDLE_ERROR( cutensornetCreateContractionOptimizerInfo(handle, descNet, &optimizerInfo) ); + + HANDLE_ERROR( cutensornetContractionOptimize(handle, + descNet, + optimizerConfig, + workspaceLimit, + optimizerInfo) ); + + // Query the number of slices the tensor network execution will be split into + int64_t numSlices = 0; + HANDLE_ERROR( cutensornetContractionOptimizerInfoGetAttribute( + handle, + optimizerInfo, + CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_NUM_SLICES, + &numSlices, + sizeof(numSlices)) ); + assert(numSlices > 0); + + if(verbose) + printf("Found an optimized contraction path using cuTensorNet optimizer\n"); + + // Sphinx: #6 + /******************************* + * Create workspace descriptor, allocate workspace, and set it. + *******************************/ + + cutensornetWorkspaceDescriptor_t workDesc; + HANDLE_ERROR( cutensornetCreateWorkspaceDescriptor(handle, &workDesc) ); + + uint64_t requiredWorkspaceSize = 0; + HANDLE_ERROR( cutensornetWorkspaceComputeContractionSizes(handle, + descNet, + optimizerInfo, + workDesc) ); + + HANDLE_ERROR( cutensornetWorkspaceGetSize(handle, + workDesc, + CUTENSORNET_WORKSIZE_PREF_MIN, + CUTENSORNET_MEMSPACE_DEVICE, + &requiredWorkspaceSize) ); + + void* work = nullptr; + HANDLE_CUDA_ERROR( cudaMalloc(&work, requiredWorkspaceSize) ); + + HANDLE_ERROR( cutensornetWorkspaceSet(handle, + workDesc, + CUTENSORNET_MEMSPACE_DEVICE, + work, + requiredWorkspaceSize) ); + + if(verbose) + printf("Allocated and set up the GPU workspace\n"); + + // Sphinx: #7 + /******************************* + * Initialize the pairwise contraction plan (for cuTENSOR). + *******************************/ + + cutensornetContractionPlan_t plan; + HANDLE_ERROR( cutensornetCreateContractionPlan(handle, + descNet, + optimizerInfo, + workDesc, + &plan) ); + + /******************************* + * Optional: Auto-tune cuTENSOR's cutensorContractionPlan to pick the fastest kernel + * for each pairwise tensor contraction. + *******************************/ + cutensornetContractionAutotunePreference_t autotunePref; + HANDLE_ERROR( cutensornetCreateContractionAutotunePreference(handle, + &autotunePref) ); + + const int numAutotuningIterations = 5; // may be 0 + HANDLE_ERROR( cutensornetContractionAutotunePreferenceSetAttribute( + handle, + autotunePref, + CUTENSORNET_CONTRACTION_AUTOTUNE_MAX_ITERATIONS, + &numAutotuningIterations, + sizeof(numAutotuningIterations)) ); + + // Modify the plan again to find the best pair-wise contractions + HANDLE_ERROR( cutensornetContractionAutotune(handle, + plan, + rawDataIn_d, + R_d, + workDesc, + autotunePref, + stream) ); + + HANDLE_ERROR( cutensornetDestroyContractionAutotunePreference(autotunePref) ); + + if(verbose) + printf("Created a contraction plan for cuTensorNet and optionally auto-tuned it\n"); + + // Sphinx: #8 + /********************** + * Execute the tensor network contraction (in parallel) + **********************/ + + // Create a cutensornetSliceGroup_t object from a range of slice IDs + cutensornetSliceGroup_t sliceGroup{}; + HANDLE_ERROR( cutensornetCreateSliceGroupFromIDRange(handle, 0, numSlices, 1, &sliceGroup) ); + + GPUTimer timer {stream}; + double minTimeCUTENSOR = 1e100; + const int numRuns = 3; // number of repeats to get stable performance results + for (int i = 0; i < numRuns; ++i) + { + HANDLE_CUDA_ERROR( cudaMemcpy(R_d, R, sizeR, cudaMemcpyHostToDevice) ); // restore the output tensor on GPU + HANDLE_CUDA_ERROR( cudaDeviceSynchronize() ); + + /* + * Contract all slices of the tensor network (in parallel) + */ + timer.start(); + + int32_t accumulateOutput = 0; // output tensor data will be overwritten + HANDLE_ERROR( cutensornetContractSlices(handle, + plan, + rawDataIn_d, + R_d, + accumulateOutput, + workDesc, + sliceGroup, // slternatively, NULL can also be used to contract over all slices instead of specifying a sliceGroup object + stream) ); + + // Synchronize and measure best timing + auto time = timer.seconds(); + minTimeCUTENSOR = (time > minTimeCUTENSOR) ? minTimeCUTENSOR : time; + } + + if(verbose) + printf("Contracted the tensor network, each slice used the same contraction plan\n"); + + // Print the 1-norm of the output tensor (verification) + HANDLE_CUDA_ERROR( cudaStreamSynchronize(stream) ); + HANDLE_CUDA_ERROR( cudaMemcpy(R, R_d, sizeR, cudaMemcpyDeviceToHost) ); // restore the output tensor on Host + double norm1 = 0.0; + for (int64_t i = 0; i < elementsR; ++i) { + norm1 += std::abs(R[i]); + } + if(verbose) + printf("Computed the 1-norm of the output tensor: %e\n", norm1); + + /*************************/ + + // Query the total Flop count for the tensor network contraction + double flops {0.0}; + HANDLE_ERROR( cutensornetContractionOptimizerInfoGetAttribute( + handle, + optimizerInfo, + CUTENSORNET_CONTRACTION_OPTIMIZER_INFO_FLOP_COUNT, + &flops, + sizeof(flops)) ); + + if(verbose) { + printf("Number of tensor network slices = %ld\n", numSlices); + printf("Tensor network contraction time (ms) = %.3f\n", minTimeCUTENSOR * 1000.f); + } + + // Free cuTensorNet resources + HANDLE_ERROR( cutensornetDestroySliceGroup(sliceGroup) ); + HANDLE_ERROR( cutensornetDestroyContractionPlan(plan) ); + HANDLE_ERROR( cutensornetDestroyWorkspaceDescriptor(workDesc) ); + HANDLE_ERROR( cutensornetDestroyContractionOptimizerInfo(optimizerInfo) ); + HANDLE_ERROR( cutensornetDestroyContractionOptimizerConfig(optimizerConfig) ); + HANDLE_ERROR( cutensornetDestroyNetworkDescriptor(descNet) ); + HANDLE_ERROR( cutensornetDestroy(handle) ); + + // Free Host memory resources + if (R) free(R); + if (D) free(D); + if (C) free(C); + if (B) free(B); + if (A) free(A); + + // Free GPU memory resources + if (work) cudaFree(work); + if (R_d) cudaFree(R_d); + if (rawDataIn_d[0]) cudaFree(rawDataIn_d[0]); + if (rawDataIn_d[1]) cudaFree(rawDataIn_d[1]); + if (rawDataIn_d[2]) cudaFree(rawDataIn_d[2]); + if (rawDataIn_d[3]) cudaFree(rawDataIn_d[3]); + + // Sphinx: MPI #7 [begin] + + // Shut down MPI service + HANDLE_MPI_ERROR( MPI_Finalize() ); + + // Sphinx: MPI #7 [end] + + if(verbose) + printf("Freed resources and exited\n"); + + return 0; +}