nvmath-python 1.0.0__cp314-cp314t-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nvmath/__init__.pxd +0 -0
- nvmath/__init__.py +45 -0
- nvmath/_internal/__init__.py +0 -0
- nvmath/_internal/attribute_ifc_factory.py +330 -0
- nvmath/_internal/layout.py +70 -0
- nvmath/_internal/templates.py +130 -0
- nvmath/_internal/threadsafe.py +106 -0
- nvmath/_internal/utils.py +43 -0
- nvmath/_internal/workspace.py +490 -0
- nvmath/_utils.py +147 -0
- nvmath/bindings/__init__.py +60 -0
- nvmath/bindings/_internal/__init__.pxd +0 -0
- nvmath/bindings/_internal/__init__.py +0 -0
- nvmath/bindings/_internal/common_types.pxd +31 -0
- nvmath/bindings/_internal/cublas.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cublas.pxd +530 -0
- nvmath/bindings/_internal/cublasLt.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cublasLt.pxd +59 -0
- nvmath/bindings/_internal/cublasMp.pxd +52 -0
- nvmath/bindings/_internal/cudss.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cudss.pxd +54 -0
- nvmath/bindings/_internal/cufft.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cufft.pxd +70 -0
- nvmath/bindings/_internal/cufftMp.pxd +77 -0
- nvmath/bindings/_internal/curand.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/curand.pxd +42 -0
- nvmath/bindings/_internal/cusolver.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cusolver.pxd +15 -0
- nvmath/bindings/_internal/cusolverDn.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cusolverDn.pxd +406 -0
- nvmath/bindings/_internal/cusolverMp.pxd +71 -0
- nvmath/bindings/_internal/cusolverSp.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cusolverSp.pxd +75 -0
- nvmath/bindings/_internal/cusparse.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cusparse.pxd +471 -0
- nvmath/bindings/_internal/cusparseLt.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cusparseLt.pxd +48 -0
- nvmath/bindings/_internal/cutensor.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cutensor.pxd +58 -0
- nvmath/bindings/_internal/mathdx.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/mathdx.pxd +116 -0
- nvmath/bindings/_internal/nvshmem.pxd +29 -0
- nvmath/bindings/_internal/utils.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/utils.pxd +174 -0
- nvmath/bindings/_internal/utils.pyi +10 -0
- nvmath/bindings/cublas.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cublas.pxd +558 -0
- nvmath/bindings/cublas.pyi +812 -0
- nvmath/bindings/cublasLt.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cublasLt.pxd +109 -0
- nvmath/bindings/cublasLt.pyi +1461 -0
- nvmath/bindings/cublasMp.pxd +85 -0
- nvmath/bindings/cublasMp.pyi +267 -0
- nvmath/bindings/cudss.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cudss.pxd +98 -0
- nvmath/bindings/cudss.pyi +443 -0
- nvmath/bindings/cufft.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cufft.pxd +118 -0
- nvmath/bindings/cufft.pyi +301 -0
- nvmath/bindings/cufftMp.pxd +124 -0
- nvmath/bindings/cufftMp.pyi +326 -0
- nvmath/bindings/curand.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/curand.pxd +71 -0
- nvmath/bindings/curand.pyi +189 -0
- nvmath/bindings/cusolver.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cusolver.pxd +62 -0
- nvmath/bindings/cusolver.pyi +320 -0
- nvmath/bindings/cusolverDn.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cusolverDn.pxd +430 -0
- nvmath/bindings/cusolverDn.pyi +422 -0
- nvmath/bindings/cusolverMp.pxd +98 -0
- nvmath/bindings/cusolverMp.pyi +114 -0
- nvmath/bindings/cusolverSp.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cusolverSp.pxd +95 -0
- nvmath/bindings/cusolverSp.pyi +70 -0
- nvmath/bindings/cusparse.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cusparse.pxd +546 -0
- nvmath/bindings/cusparse.pyi +1017 -0
- nvmath/bindings/cusparseLt.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cusparseLt.pxd +99 -0
- nvmath/bindings/cusparseLt.pyi +252 -0
- nvmath/bindings/cutensor.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cutensor.pxd +98 -0
- nvmath/bindings/cutensor.pyi +324 -0
- nvmath/bindings/cycublas.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycublas.pxd +664 -0
- nvmath/bindings/cycublasLt.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycublasLt.pxd +1045 -0
- nvmath/bindings/cycublasMp.pxd +171 -0
- nvmath/bindings/cycudss.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycudss.pxd +277 -0
- nvmath/bindings/cycufft.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycufft.pxd +333 -0
- nvmath/bindings/cycufftMp.pxd +342 -0
- nvmath/bindings/cycurand.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycurand.pxd +141 -0
- nvmath/bindings/cycusolver.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycusolver.pxd +137 -0
- nvmath/bindings/cycusolverDn.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycusolverDn.pxd +443 -0
- nvmath/bindings/cycusolverMp.pxd +107 -0
- nvmath/bindings/cycusolverSp.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycusolverSp.pxd +93 -0
- nvmath/bindings/cycusparse.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycusparse.pxd +679 -0
- nvmath/bindings/cycusparseLt.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycusparseLt.pxd +135 -0
- nvmath/bindings/cycutensor.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycutensor.pxd +189 -0
- nvmath/bindings/cymathdx.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cymathdx.pxd +552 -0
- nvmath/bindings/cynvshmem.pxd +118 -0
- nvmath/bindings/mathdx.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/mathdx.pxd +182 -0
- nvmath/bindings/mathdx.pyi +1562 -0
- nvmath/bindings/nvpl/__init__.pxd +0 -0
- nvmath/bindings/nvpl/__init__.py +13 -0
- nvmath/bindings/nvpl/_internal/__init__.pxd +0 -0
- nvmath/bindings/nvpl/_internal/__init__.py +0 -0
- nvmath/bindings/nvpl/_internal/blas.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/nvpl/_internal/blas.pxd +237 -0
- nvmath/bindings/nvpl/_internal/fft.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/nvpl/_internal/fft.pxd +36 -0
- nvmath/bindings/nvpl/blas.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/nvpl/blas.pxd +131 -0
- nvmath/bindings/nvpl/blas.pyi +168 -0
- nvmath/bindings/nvpl/cyblas.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/nvpl/cyblas.pxd +280 -0
- nvmath/bindings/nvpl/cyfft.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/nvpl/cyfft.pxd +93 -0
- nvmath/bindings/nvpl/fft.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/nvpl/fft.pxd +100 -0
- nvmath/bindings/nvpl/fft.pyi +168 -0
- nvmath/bindings/nvshmem.pxd +54 -0
- nvmath/bindings/nvshmem.pyi +191 -0
- nvmath/device/__init__.py +38 -0
- nvmath/device/_deprecated.py +33 -0
- nvmath/device/common.py +315 -0
- nvmath/device/common_backend.py +131 -0
- nvmath/device/common_cuda.py +201 -0
- nvmath/device/common_numba.py +300 -0
- nvmath/device/common_numba_cuda_mlir.py +202 -0
- nvmath/device/common_opaque_tensor.py +201 -0
- nvmath/device/cublasdx.py +1606 -0
- nvmath/device/cublasdx_backend.py +860 -0
- nvmath/device/cublasdx_numba.py +1534 -0
- nvmath/device/cublasdx_numba_cuda_mlir.py +208 -0
- nvmath/device/cufftdx.py +373 -0
- nvmath/device/cufftdx_backend.py +220 -0
- nvmath/device/cufftdx_numba.py +140 -0
- nvmath/device/cufftdx_numba_cuda_mlir.py +79 -0
- nvmath/device/curand_kernel.py +9147 -0
- nvmath/device/cusolverdx.py +2708 -0
- nvmath/device/cusolverdx_backend.py +440 -0
- nvmath/device/cusolverdx_numba.py +567 -0
- nvmath/device/cusolverdx_numba_cuda_mlir.py +604 -0
- nvmath/device/cusolverdx_overload_backend.py +1029 -0
- nvmath/device/llvm_array.py +29 -0
- nvmath/device/random.py +441 -0
- nvmath/device/random_helpers.py +23 -0
- nvmath/device/random_states.py +187 -0
- nvmath/device/types.py +138 -0
- nvmath/device/vector_types_numba.py +259 -0
- nvmath/distributed/__init__.py +200 -0
- nvmath/distributed/_internal/__init__.py +0 -0
- nvmath/distributed/_internal/nccl.py +86 -0
- nvmath/distributed/_internal/nvshmem.py +307 -0
- nvmath/distributed/_internal/symmetric_memory.py +35 -0
- nvmath/distributed/_internal/tensor_ifc.py +70 -0
- nvmath/distributed/_internal/tensor_ifc_cupy.py +68 -0
- nvmath/distributed/_internal/tensor_ifc_host_device.py +172 -0
- nvmath/distributed/_internal/tensor_ifc_numpy.py +46 -0
- nvmath/distributed/_internal/tensor_ifc_torch.py +162 -0
- nvmath/distributed/_internal/tensor_wrapper.py +81 -0
- nvmath/distributed/_utils.py +167 -0
- nvmath/distributed/distribution/__init__.py +30 -0
- nvmath/distributed/distribution/_configuration.py +39 -0
- nvmath/distributed/distribution/distributions.py +1024 -0
- nvmath/distributed/distribution/redistribute.py +1284 -0
- nvmath/distributed/fft/__init__.py +7 -0
- nvmath/distributed/fft/_configuration.py +82 -0
- nvmath/distributed/fft/fft.py +2742 -0
- nvmath/distributed/linalg/__init__.py +22 -0
- nvmath/distributed/linalg/_internal/__init__.py +3 -0
- nvmath/distributed/linalg/_internal/epilog_protocol.py +586 -0
- nvmath/distributed/linalg/_internal/matmul_desc_ifc.py +28 -0
- nvmath/distributed/linalg/advanced/__init__.py +8 -0
- nvmath/distributed/linalg/advanced/_configuration.py +171 -0
- nvmath/distributed/linalg/advanced/matmulmod.py +3573 -0
- nvmath/distributed/linalg/generic/__init__.py +8 -0
- nvmath/distributed/linalg/generic/_caching.py +66 -0
- nvmath/distributed/linalg/generic/_configuration.py +61 -0
- nvmath/distributed/linalg/generic/_factorization.py +172 -0
- nvmath/distributed/linalg/generic/_initialization.py +966 -0
- nvmath/distributed/linalg/generic/_problem_spec.py +511 -0
- nvmath/distributed/linalg/generic/solvermod.py +1368 -0
- nvmath/distributed/process_group.py +408 -0
- nvmath/fft/__init__.py +7 -0
- nvmath/fft/_configuration.py +189 -0
- nvmath/fft/_exec_utils.py +82 -0
- nvmath/fft/_helpers.py +237 -0
- nvmath/fft/fft.py +3122 -0
- nvmath/internal/__init__.pxd +3 -0
- nvmath/internal/__init__.py +10 -0
- nvmath/internal/_bindings.cp314t-win_amd64.pyd +0 -0
- nvmath/internal/_bindings.pxd +18 -0
- nvmath/internal/_device_utils.py +45 -0
- nvmath/internal/_layout/__init__.pxd +3 -0
- nvmath/internal/_layout/__init__.py +7 -0
- nvmath/internal/_layout/_layout.cp314t-win_amd64.pyd +0 -0
- nvmath/internal/_layout/_layout.pxd +1303 -0
- nvmath/internal/_layout/_layout.pyi +1145 -0
- nvmath/internal/enum_utils.py +142 -0
- nvmath/internal/formatters.py +87 -0
- nvmath/internal/mem_limit.py +51 -0
- nvmath/internal/memory.cp314t-win_amd64.pyd +0 -0
- nvmath/internal/memory.pxd +13 -0
- nvmath/internal/memory.pyi +50 -0
- nvmath/internal/ndbuffer/__init__.pxd +3 -0
- nvmath/internal/ndbuffer/__init__.py +9 -0
- nvmath/internal/ndbuffer/_copy_kernel.cp314t-win_amd64.pyd +0 -0
- nvmath/internal/ndbuffer/_copy_kernel.pxd +10 -0
- nvmath/internal/ndbuffer/_jit.cp314t-win_amd64.pyd +0 -0
- nvmath/internal/ndbuffer/_jit.pxd +7 -0
- nvmath/internal/ndbuffer/_ndbuffer.cp314t-win_amd64.pyd +0 -0
- nvmath/internal/ndbuffer/_ndbuffer.pxd +40 -0
- nvmath/internal/ndbuffer/_ndbuffer.pyi +463 -0
- nvmath/internal/ndbuffer/copy_kernel/args.h +34 -0
- nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/array_view.h +52 -0
- nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/elementwise.h +68 -0
- nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/grid_indexer.h +69 -0
- nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/transposed.h +242 -0
- nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/type_utils.h +39 -0
- nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/utils.h +132 -0
- nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/vec.h +159 -0
- nvmath/internal/ndbuffer/copy_kernel/elementwise.h +53 -0
- nvmath/internal/ndbuffer/copy_kernel/transposed.h +58 -0
- nvmath/internal/package_ifc.py +168 -0
- nvmath/internal/package_ifc_cuda.py +57 -0
- nvmath/internal/package_ifc_cupy.py +67 -0
- nvmath/internal/package_ifc_torch.py +69 -0
- nvmath/internal/package_wrapper.py +14 -0
- nvmath/internal/tensor_ifc.py +179 -0
- nvmath/internal/tensor_ifc_cupy.py +234 -0
- nvmath/internal/tensor_ifc_ndbuffer.py +147 -0
- nvmath/internal/tensor_ifc_numpy.py +184 -0
- nvmath/internal/tensor_ifc_torch.py +178 -0
- nvmath/internal/tensor_wrapper.py +160 -0
- nvmath/internal/typemaps.py +113 -0
- nvmath/internal/utils.py +805 -0
- nvmath/linalg/__init__.py +56 -0
- nvmath/linalg/_internal/__init__.py +3 -0
- nvmath/linalg/_internal/algo_cap_ifc.py +82 -0
- nvmath/linalg/_internal/algo_config_ifc.py +43 -0
- nvmath/linalg/_internal/batch.py +234 -0
- nvmath/linalg/_internal/enum_to_tuples.py +64 -0
- nvmath/linalg/_internal/epilog_protocol.py +766 -0
- nvmath/linalg/_internal/layout.py +624 -0
- nvmath/linalg/_internal/matmul_desc_ifc.py +28 -0
- nvmath/linalg/_internal/matmul_pref_ifc.py +27 -0
- nvmath/linalg/_internal/matrix_layout_ifc.py +26 -0
- nvmath/linalg/_internal/solver_utils.py +432 -0
- nvmath/linalg/_internal/typemaps.py +144 -0
- nvmath/linalg/_internal/utils.py +157 -0
- nvmath/linalg/advanced/__init__.py +8 -0
- nvmath/linalg/advanced/_algorithmmod.py +170 -0
- nvmath/linalg/advanced/_configuration.py +351 -0
- nvmath/linalg/advanced/helpers/__init__.py +5 -0
- nvmath/linalg/advanced/helpers/matmul.py +1316 -0
- nvmath/linalg/advanced/matmulmod.py +3734 -0
- nvmath/linalg/generic/__init__.py +53 -0
- nvmath/linalg/generic/_configuration/__init__.py +39 -0
- nvmath/linalg/generic/_configuration/layout.py +263 -0
- nvmath/linalg/generic/_configuration/match.py +734 -0
- nvmath/linalg/generic/_configuration/qualifiers.py +493 -0
- nvmath/linalg/generic/_configuration/solver_configuration.py +59 -0
- nvmath/linalg/generic/_configuration/wrap.py +217 -0
- nvmath/linalg/generic/_dtype.py +15 -0
- nvmath/linalg/generic/matmulmod.py +2094 -0
- nvmath/linalg/generic/solvermod.py +1301 -0
- nvmath/memory.py +279 -0
- nvmath/sparse/__init__.py +38 -0
- nvmath/sparse/_internal/__init__.py +21 -0
- nvmath/sparse/_internal/common_utils.py +147 -0
- nvmath/sparse/_internal/cudss_config_ifc.py +702 -0
- nvmath/sparse/_internal/cudss_data_ifc.py +399 -0
- nvmath/sparse/_internal/cudss_utils.py +506 -0
- nvmath/sparse/_internal/cusparse_utils.py +382 -0
- nvmath/sparse/_internal/sparse_bsc_ifc.py +303 -0
- nvmath/sparse/_internal/sparse_bsr_ifc.py +305 -0
- nvmath/sparse/_internal/sparse_coo_ifc.py +256 -0
- nvmath/sparse/_internal/sparse_csc_ifc.py +268 -0
- nvmath/sparse/_internal/sparse_csr_ifc.py +288 -0
- nvmath/sparse/_internal/sparse_dia_ifc.py +242 -0
- nvmath/sparse/_internal/sparse_format_helpers.py +601 -0
- nvmath/sparse/_internal/sparse_tensor_ifc.py +133 -0
- nvmath/sparse/_internal/sparse_ust_ifc.py +141 -0
- nvmath/sparse/_internal/utils.py +56 -0
- nvmath/sparse/advanced/__init__.py +7 -0
- nvmath/sparse/advanced/_configuration.py +227 -0
- nvmath/sparse/advanced/direct_solver.py +2069 -0
- nvmath/sparse/generic/__init__.py +7 -0
- nvmath/sparse/generic/_configuration.py +129 -0
- nvmath/sparse/generic/_helpers.py +137 -0
- nvmath/sparse/generic/_thunks.py +21 -0
- nvmath/sparse/generic/matmulmod.py +2353 -0
- nvmath/sparse/ust/__init__.py +7 -0
- nvmath/sparse/ust/_converters.py +422 -0
- nvmath/sparse/ust/_cpp.py +28 -0
- nvmath/sparse/ust/_drawer.py +565 -0
- nvmath/sparse/ust/_emitter.py +1033 -0
- nvmath/sparse/ust/_jit.py +188 -0
- nvmath/sparse/ust/_kernel.py +282 -0
- nvmath/sparse/ust/_utils.py +149 -0
- nvmath/sparse/ust/interfaces/__init__.py +0 -0
- nvmath/sparse/ust/interfaces/torch_interface.py +476 -0
- nvmath/sparse/ust/tensor.py +1016 -0
- nvmath/sparse/ust/tensor_format.py +957 -0
- nvmath/tensor/__init__.py +6 -0
- nvmath/tensor/_configuration.py +120 -0
- nvmath/tensor/_internal/__init__.py +3 -0
- nvmath/tensor/_internal/cutensor_config_ifc.py +279 -0
- nvmath/tensor/_internal/cutensor_utils.py +230 -0
- nvmath/tensor/_internal/data.py +43 -0
- nvmath/tensor/_internal/einsum_parser.py +444 -0
- nvmath/tensor/_internal/typemaps.py +96 -0
- nvmath/tensor/contract.py +1900 -0
- nvmath_python-1.0.0.dist-info/METADATA +134 -0
- nvmath_python-1.0.0.dist-info/RECORD +332 -0
- nvmath_python-1.0.0.dist-info/WHEEL +5 -0
- nvmath_python-1.0.0.dist-info/licenses/LICENSE +177 -0
- nvmath_python-1.0.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,1900 @@
|
|
|
1
|
+
# Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from typing import Any, Literal, cast
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from .. import memory
|
|
12
|
+
from .._internal.layout import is_contiguous_and_dense
|
|
13
|
+
from .._internal.workspace import Workspace
|
|
14
|
+
from .._utils import CudaDataType
|
|
15
|
+
from ..bindings import cutensor
|
|
16
|
+
from ..internal import tensor_wrapper, utils
|
|
17
|
+
from ..internal.typemaps import DATA_TYPE_TO_NAME, NAME_TO_DATA_TYPE
|
|
18
|
+
from ._configuration import ContractionOptions, ExecutionCUDA
|
|
19
|
+
from ._internal import cutensor_utils, einsum_parser
|
|
20
|
+
from ._internal.cutensor_config_ifc import ContractionPlanPreference
|
|
21
|
+
from ._internal.typemaps import get_compute_type_name, get_default_compute_type_from_dtype_name, get_supported_compute_types
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"BinaryContraction",
|
|
25
|
+
"TernaryContraction",
|
|
26
|
+
"ContractionPlanPreference",
|
|
27
|
+
"ComputeDesc",
|
|
28
|
+
"binary_contraction",
|
|
29
|
+
"ternary_contraction",
|
|
30
|
+
"Operator",
|
|
31
|
+
"tensor_qualifiers_dtype",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
Operator = cutensor.Operator
|
|
36
|
+
|
|
37
|
+
ComputeDesc = cutensor.ComputeDesc
|
|
38
|
+
|
|
39
|
+
tensor_qualifiers_dtype = np.int32
|
|
40
|
+
|
|
41
|
+
# As of cuTensor 2.5.0, only the following operators are supported in the contraction APIs
|
|
42
|
+
OPERATORS_SUPPORTED = {Operator.OP_IDENTITY, Operator.OP_CONJ}
|
|
43
|
+
|
|
44
|
+
SHARED_CONTRACTION_DOCUMENTATION = utils.COMMON_SHARED_DOC_MAP.copy()
|
|
45
|
+
SHARED_CONTRACTION_DOCUMENTATION.update(
|
|
46
|
+
{
|
|
47
|
+
"expr": """\
|
|
48
|
+
The einsum expression to perform the contraction.
|
|
49
|
+
""".replace("\n", " "),
|
|
50
|
+
#
|
|
51
|
+
"a": """\
|
|
52
|
+
A tensor representing the first operand to the tensor contraction. The currently supported types
|
|
53
|
+
are :class:`numpy.ndarray`, :class:`cupy.ndarray`, and :class:`torch.Tensor`.""".replace("\n", " "),
|
|
54
|
+
#
|
|
55
|
+
"b": """\
|
|
56
|
+
A tensor representing the second operand to the tensor contraction. The currently supported types
|
|
57
|
+
are :class:`numpy.ndarray`, :class:`cupy.ndarray`, and :class:`torch.Tensor`.""".replace("\n", " "),
|
|
58
|
+
#
|
|
59
|
+
"c": """\
|
|
60
|
+
A tensor representing the third operand to the tensor contraction. The currently supported types
|
|
61
|
+
are :class:`numpy.ndarray`, :class:`cupy.ndarray`, and :class:`torch.Tensor`.""".replace("\n", " "),
|
|
62
|
+
#
|
|
63
|
+
"addend": """\
|
|
64
|
+
(Optional) A tensor representing the operand to add to the tensor contraction result (fused operation in cuTensor).
|
|
65
|
+
The currently supported types are :class:`numpy.ndarray`, :class:`cupy.ndarray`,
|
|
66
|
+
and :class:`torch.Tensor`.""".replace("\n", " "),
|
|
67
|
+
#
|
|
68
|
+
"alpha": """\
|
|
69
|
+
The scale factor for the tensor contraction term as a real or complex number. The default is
|
|
70
|
+
:math:`1.0`.""".replace("\n", " "),
|
|
71
|
+
#
|
|
72
|
+
"beta": """\
|
|
73
|
+
The scale factor for the tensor addition term as a real or complex number. A value for `beta` must be provided if
|
|
74
|
+
the operand to be added is specified.""".replace("\n", " "),
|
|
75
|
+
#
|
|
76
|
+
"qualifiers": """\
|
|
77
|
+
If desired, specify the operators as a :class:`numpy.ndarray` of dtype :class:`~nvmath.tensor.tensor_qualifiers_dtype`
|
|
78
|
+
with the same length as the number of operands in the contraction expression plus one (for the operand to be added).
|
|
79
|
+
All elements must be valid :class:`~nvmath.tensor.Operator` objects. See
|
|
80
|
+
:ref:`matrix-tensor-qualifiers` for the motivation behind qualifiers.""".replace("\n", " "),
|
|
81
|
+
#
|
|
82
|
+
"options": """\
|
|
83
|
+
Specify options for the tensor contraction as a :class:`~nvmath.tensor.ContractionOptions` object. Alternatively,
|
|
84
|
+
a `dict` containing the parameters for the ``ContractionOptions`` constructor can also be provided. If not specified, the
|
|
85
|
+
value will be set to the default-constructed ``ContractionOptions`` object.""".replace("\n", " "),
|
|
86
|
+
#
|
|
87
|
+
"execution": """\
|
|
88
|
+
Specify execution space options for the tensor contraction as a :class:`ExecutionCUDA` object or a string 'cuda'.
|
|
89
|
+
Alternatively, a `dict` containing 'name' key set to 'cuda' and the additional parameters for the ``ExecutionCUDA``
|
|
90
|
+
constructor can also be provided. If not provided, the execution space will be selected to match operand's storage if
|
|
91
|
+
the operands are on the GPU. If the operands are on the CPU and execution space is not provided, the execution space
|
|
92
|
+
will be a default-constructed :class:`ExecutionCUDA` object with device_id = 0.""".replace("\n", " "),
|
|
93
|
+
#
|
|
94
|
+
"out": """\
|
|
95
|
+
(Optional) The output tensor to store the result of the contraction. Must be a :class:`numpy.ndarray`, \
|
|
96
|
+
:class:`cupy.ndarray`, or :class:`torch.Tensor` object and must be on the same device as the input operands. \
|
|
97
|
+
If not specified, the result will be returned on the same device as the input operands.
|
|
98
|
+
|
|
99
|
+
.. experimental:: parameter
|
|
100
|
+
|
|
101
|
+
""".strip(),
|
|
102
|
+
#
|
|
103
|
+
"result": """\
|
|
104
|
+
The result of the specified contraction, which remains on the same device and belong to the
|
|
105
|
+
same package as the input operands. """.replace("\n", " "),
|
|
106
|
+
#
|
|
107
|
+
"reset_operands_semantics": """\
|
|
108
|
+
Semantics:
|
|
109
|
+
- Only the operands explicitly passed are updated. At least one operand
|
|
110
|
+
is required (all of them after :meth:`release_operands`), otherwise
|
|
111
|
+
a :class:`ValueError` is raised.
|
|
112
|
+
|
|
113
|
+
- This method validates each new operand against its corresponding one
|
|
114
|
+
set during the object's initialization.
|
|
115
|
+
An operand is compatible if all of the following requirements are met:
|
|
116
|
+
|
|
117
|
+
- The shapes, strides, and datatypes match those of the old one.
|
|
118
|
+
- The package (e.g., cupy, torch, numpy) matches that of the old one.
|
|
119
|
+
- The memory space (CPU or GPU) matches that of the old one.
|
|
120
|
+
- The device matches that of the old one, if the operand is on GPU.
|
|
121
|
+
- The pointer alignment must be the same or a multiple of the old one.
|
|
122
|
+
|
|
123
|
+
- If the execution space matches the memory space of the operand:
|
|
124
|
+
operand's reference is updated with no data copying.
|
|
125
|
+
|
|
126
|
+
- If the execution space does not match the memory space of the operand:
|
|
127
|
+
data is copied between different memory spaces.
|
|
128
|
+
""",
|
|
129
|
+
#
|
|
130
|
+
"reset_operands_unchecked": utils._reset_operand_unchecked_docstring(
|
|
131
|
+
True, version_added="0.9.0", validation_examples="package match, data type match, pointer alignment match"
|
|
132
|
+
),
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class InvalidContractionState(Exception):
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _validate_contraction_preconditions(expr, a, b, c, d, qualifiers, options):
|
|
142
|
+
"""
|
|
143
|
+
Validate preconditions for _ElementaryContraction initialization.
|
|
144
|
+
|
|
145
|
+
This function checks all preconditions that can be validated before
|
|
146
|
+
wrapping operands or allocating resources. It raises exceptions for invalid inputs.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
expr: Einstein expression string
|
|
150
|
+
a: First input operand
|
|
151
|
+
b: Second input operand
|
|
152
|
+
c: Optional third operand (for binary) or offset (for ternary)
|
|
153
|
+
d: Optional offset operand (for ternary only)
|
|
154
|
+
qualifiers: Optional qualifiers array
|
|
155
|
+
options: Optional contraction options
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
tuple: (num_inputs, inputs, output) from parsed expression
|
|
159
|
+
"""
|
|
160
|
+
# Parse expression to determine number of inputs (validates expression format)
|
|
161
|
+
inputs, output = einsum_parser.parse_einsum_str(expr)
|
|
162
|
+
num_inputs = len(inputs)
|
|
163
|
+
|
|
164
|
+
# Validate number of inputs, c/d operand consistency with expression type
|
|
165
|
+
if num_inputs == 2:
|
|
166
|
+
if d is not None:
|
|
167
|
+
raise ValueError(f"Binary contraction '{expr}' (2 inputs) cannot have a 'd' operand. ")
|
|
168
|
+
elif num_inputs == 3:
|
|
169
|
+
if c is None:
|
|
170
|
+
raise ValueError(f"Ternary contraction '{expr}' (3 inputs) requires 'c' operand (third multiplicand). ")
|
|
171
|
+
else:
|
|
172
|
+
raise NotImplementedError(
|
|
173
|
+
f"Expression '{expr}' has {num_inputs} inputs. Only binary and ternary contractions are supported."
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Validate qualifiers structure (if provided)
|
|
177
|
+
if qualifiers is not None:
|
|
178
|
+
try:
|
|
179
|
+
qualifiers_array = np.asarray(qualifiers, dtype=np.int32)
|
|
180
|
+
except Exception as e:
|
|
181
|
+
raise TypeError(f"Qualifiers must be array-like and convertible to int32: {e}") from e
|
|
182
|
+
|
|
183
|
+
# Check array length
|
|
184
|
+
expected_len = num_inputs + 1 # one per input + one for offset
|
|
185
|
+
if qualifiers_array.size != expected_len:
|
|
186
|
+
contraction_type = "binary" if num_inputs == 2 else "ternary"
|
|
187
|
+
operand_names = "a, b, offset" if num_inputs == 2 else "a, b, c, offset"
|
|
188
|
+
raise ValueError(
|
|
189
|
+
f"The qualifiers must be a numpy array of length {expected_len} "
|
|
190
|
+
f"(one per operand: {operand_names}), got {qualifiers_array.size}. "
|
|
191
|
+
f"Expression '{expr}' is a {contraction_type} contraction."
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Validate offset qualifier must be identity
|
|
195
|
+
if qualifiers_array[num_inputs] != cutensor.Operator.OP_IDENTITY:
|
|
196
|
+
raise ValueError(f"The operand for the offset must be the identity operator, found {qualifiers_array[num_inputs]}")
|
|
197
|
+
|
|
198
|
+
# Validate all input qualifiers are supported operators
|
|
199
|
+
operand_names = ["a", "b", "c"][:num_inputs]
|
|
200
|
+
for op_name, qualifier in zip(operand_names, qualifiers_array[:-1], strict=False):
|
|
201
|
+
if qualifier not in OPERATORS_SUPPORTED:
|
|
202
|
+
raise ValueError(
|
|
203
|
+
f"Each operator must be a valid cuTensor operator, "
|
|
204
|
+
f"currently only support {OPERATORS_SUPPORTED}, "
|
|
205
|
+
f"got {qualifier} for operand '{op_name}'."
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Validate qualifiers against operand dtypes (e.g., OP_CONJ requires complex)
|
|
209
|
+
operands = [a, b, c][:num_inputs]
|
|
210
|
+
for op_name, operand, qualifier in zip(operand_names, operands, qualifiers_array[:-1], strict=False):
|
|
211
|
+
if qualifier == cutensor.Operator.OP_CONJ:
|
|
212
|
+
# Check if operand has dtype attribute
|
|
213
|
+
if not hasattr(operand, "dtype"):
|
|
214
|
+
raise TypeError(
|
|
215
|
+
f"Operand '{op_name}' must be array-like with a dtype attribute "
|
|
216
|
+
f"(numpy.ndarray, cupy.ndarray, or torch.Tensor)"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Check for complex dtype
|
|
220
|
+
dtype_str = str(operand.dtype)
|
|
221
|
+
if "complex" not in dtype_str:
|
|
222
|
+
raise ValueError(
|
|
223
|
+
f"Cannot apply OP_CONJ (conjugate) operator to operand '{op_name}' "
|
|
224
|
+
f"with dtype '{operand.dtype}'. Conjugate operator requires complex dtype."
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Validate options structure (if provided)
|
|
228
|
+
if options is not None:
|
|
229
|
+
# Extract compute_type from options (could be dict or ContractionOptions object)
|
|
230
|
+
if isinstance(options, dict):
|
|
231
|
+
compute_type = options.get("compute_type")
|
|
232
|
+
else:
|
|
233
|
+
compute_type = getattr(options, "compute_type", None)
|
|
234
|
+
|
|
235
|
+
# Validate compute_type is None or int
|
|
236
|
+
if compute_type is not None and not isinstance(compute_type, int):
|
|
237
|
+
raise ValueError(f"Invalid compute type: {compute_type}. compute_type must be None or an integer.")
|
|
238
|
+
|
|
239
|
+
return num_inputs, inputs, output
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@utils.docstring_decorator(SHARED_CONTRACTION_DOCUMENTATION, skip_missing=False)
|
|
243
|
+
class _ElementaryContraction:
|
|
244
|
+
"""
|
|
245
|
+
Pairwise contraction:
|
|
246
|
+
out = a @ b + c
|
|
247
|
+
Ternary contraction:
|
|
248
|
+
out = a @ b @ c + d
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
def __init__(
|
|
252
|
+
self,
|
|
253
|
+
expr,
|
|
254
|
+
a,
|
|
255
|
+
b,
|
|
256
|
+
*,
|
|
257
|
+
c=None,
|
|
258
|
+
d=None,
|
|
259
|
+
out=None,
|
|
260
|
+
qualifiers=None,
|
|
261
|
+
options=None,
|
|
262
|
+
execution: ExecutionCUDA | str | dict[str, Any] | None = None,
|
|
263
|
+
stream: utils.AnyStream | int | None = None,
|
|
264
|
+
):
|
|
265
|
+
"""Binary & Ternary Contraction"""
|
|
266
|
+
|
|
267
|
+
# Initialize valid_state to True here because this flag is currenttly only
|
|
268
|
+
# used to check if the object has been free'd or not.
|
|
269
|
+
# As far as freeing the object is concerned, upon initialization,
|
|
270
|
+
# the object is already valid.
|
|
271
|
+
self.valid_state = True
|
|
272
|
+
|
|
273
|
+
# Track whether the user has called release_operands().
|
|
274
|
+
self._operands_released = False
|
|
275
|
+
|
|
276
|
+
# ========================================================================
|
|
277
|
+
# Validate preconditions right away, if it fails, no state changes will be made
|
|
278
|
+
# ========================================================================
|
|
279
|
+
self.num_inputs, inputs, output = _validate_contraction_preconditions(expr, a, b, c, d, qualifiers, options)
|
|
280
|
+
self.expr = expr
|
|
281
|
+
|
|
282
|
+
# ========================================================================
|
|
283
|
+
# Process options (needed for logging and configuration)
|
|
284
|
+
# ========================================================================
|
|
285
|
+
self.options: ContractionOptions = utils.check_or_create_options( # type: ignore[assignment]
|
|
286
|
+
ContractionOptions, options, "elementary contraction options"
|
|
287
|
+
)
|
|
288
|
+
self.logger = self.options.logger if self.options.logger is not None else logging.getLogger()
|
|
289
|
+
log_info = self.logger.isEnabledFor(logging.INFO)
|
|
290
|
+
log_debug = self.logger.isEnabledFor(logging.DEBUG)
|
|
291
|
+
|
|
292
|
+
# ========================================================================
|
|
293
|
+
# Wrap and validate operands (a, b, and c, d if needed)
|
|
294
|
+
# ========================================================================
|
|
295
|
+
self.a, self.b = tensor_wrapper.wrap_operands([a, b])
|
|
296
|
+
self.input_operand_class = self.a.__class__
|
|
297
|
+
self.input_package = utils.get_operands_package([self.a, self.b])
|
|
298
|
+
|
|
299
|
+
# Determine internal package (numpy -> cuda conversion)
|
|
300
|
+
if self.input_package == "numpy":
|
|
301
|
+
self.internal_package = "cuda"
|
|
302
|
+
else:
|
|
303
|
+
self.internal_package = self.input_package
|
|
304
|
+
tensor_wrapper.maybe_register_package(self.internal_package)
|
|
305
|
+
|
|
306
|
+
# Wrap optional operands c, d and validate package consistency
|
|
307
|
+
wrapped_operands = [self.a, self.b]
|
|
308
|
+
self.c_provided = c is not None
|
|
309
|
+
self.d_provided = d is not None
|
|
310
|
+
|
|
311
|
+
self.c: tensor_wrapper.TensorHolder[Any] | None
|
|
312
|
+
self.d: tensor_wrapper.TensorHolder[Any] | None
|
|
313
|
+
for op_name, op in zip(["c", "d"], [c, d], strict=False):
|
|
314
|
+
if op is not None:
|
|
315
|
+
op = tensor_wrapper.wrap_operand(op)
|
|
316
|
+
if op.name != self.input_package:
|
|
317
|
+
raise ValueError(
|
|
318
|
+
f"operand has package '{op.name}' but expected '{self.input_package}'. "
|
|
319
|
+
f"All operands must be from the same tensor package."
|
|
320
|
+
)
|
|
321
|
+
wrapped_operands.append(op)
|
|
322
|
+
setattr(self, op_name, op)
|
|
323
|
+
|
|
324
|
+
# ========================================================================
|
|
325
|
+
# Setup qualifiers
|
|
326
|
+
# ========================================================================
|
|
327
|
+
# If here, preconditions were validated, we can safely create the qualifiers
|
|
328
|
+
# or use the ones provided.
|
|
329
|
+
if qualifiers is None:
|
|
330
|
+
self.qualifiers = np.full(self.num_inputs + 1, cutensor.Operator.OP_IDENTITY, dtype=np.int32) # size of enum value
|
|
331
|
+
else:
|
|
332
|
+
# validation of qualifiers done during preconditions' check
|
|
333
|
+
self.qualifiers = np.asarray(qualifiers, dtype=np.int32)
|
|
334
|
+
|
|
335
|
+
# ========================================================================
|
|
336
|
+
# Setup execution environment and device management
|
|
337
|
+
# ========================================================================
|
|
338
|
+
self.input_device_id = utils.get_operands_device_id(wrapped_operands)
|
|
339
|
+
self.blocking = self.options.blocking is True or self.input_device_id == "cpu"
|
|
340
|
+
if self.blocking:
|
|
341
|
+
self.call_prologue = "This call is blocking and will return only after the operation is complete."
|
|
342
|
+
else:
|
|
343
|
+
self.call_prologue = (
|
|
344
|
+
"This call is non-blocking and will return immediately after the operation is launched on the device."
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
self.execution = utils.check_or_create_one_of_options(
|
|
348
|
+
(ExecutionCUDA,),
|
|
349
|
+
execution,
|
|
350
|
+
"execution options",
|
|
351
|
+
default_name="cuda",
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
if log_info:
|
|
355
|
+
self.logger.info(
|
|
356
|
+
f"The input tensor's memory space is {self.input_device_id}, and the execution space "
|
|
357
|
+
f"is device {self.execution.device_id}."
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# Determine execution device and create stream
|
|
361
|
+
if self.input_device_id == "cpu":
|
|
362
|
+
self.execution_device_id = self.execution.device_id
|
|
363
|
+
stream_holder = utils.get_or_create_stream(self.execution_device_id, stream, self.internal_package)
|
|
364
|
+
# Transfer CPU operands to execution device
|
|
365
|
+
self.a = self.a.to(self.execution_device_id, stream_holder)
|
|
366
|
+
self.b = self.b.to(self.execution_device_id, stream_holder)
|
|
367
|
+
if self.c is not None:
|
|
368
|
+
self.c = self.c.to(self.execution_device_id, stream_holder)
|
|
369
|
+
if self.d is not None:
|
|
370
|
+
self.d = self.d.to(self.execution_device_id, stream_holder)
|
|
371
|
+
if log_debug:
|
|
372
|
+
self.logger.debug(f"The input tensors have been copied to the execution device: {self.execution_device_id}.")
|
|
373
|
+
else:
|
|
374
|
+
self.execution_device_id = self.input_device_id
|
|
375
|
+
stream_holder = utils.get_or_create_stream(self.execution_device_id, stream, self.internal_package)
|
|
376
|
+
|
|
377
|
+
# ========================================================================
|
|
378
|
+
# Determine data types and compute configuration
|
|
379
|
+
# ========================================================================
|
|
380
|
+
# TODO: cutensor supports R_64F (A) C_64F (B) C_64F (C) combination (and inverse)
|
|
381
|
+
# https://docs.nvidia.com/cuda/cutensor/latest/api/cutensor.html#cutensorcreatecontraction
|
|
382
|
+
self.data_type = utils.get_operands_dtype(wrapped_operands)
|
|
383
|
+
self.cuda_data_type = NAME_TO_DATA_TYPE[self.data_type]
|
|
384
|
+
|
|
385
|
+
# Parse compute descriptor
|
|
386
|
+
if self.options.compute_type is None:
|
|
387
|
+
self.compute_type = get_default_compute_type_from_dtype_name(self.data_type)
|
|
388
|
+
else:
|
|
389
|
+
# Type validation done in preconditions, here check compatibility with data type
|
|
390
|
+
if self.options.compute_type not in get_supported_compute_types(self.data_type):
|
|
391
|
+
raise ValueError(f"Invalid compute type: {self.options.compute_type} for data type: {self.data_type}")
|
|
392
|
+
self.compute_type = self.options.compute_type
|
|
393
|
+
if self.compute_type == ComputeDesc.COMPUTE_8XINT8():
|
|
394
|
+
# TODO: remove the check once cutensor requirement is bumped to 2.6.0
|
|
395
|
+
version = cutensor.get_version()
|
|
396
|
+
if version == 20500:
|
|
397
|
+
raise RuntimeError(
|
|
398
|
+
"The 8XINT8 compute type is not supported in cuTensor 2.5.0 due to a known bug. "
|
|
399
|
+
"Please upgrade cuTensor to a later version."
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
if log_info:
|
|
403
|
+
self.logger.info(f"The compute type is: {get_compute_type_name(self.compute_type)}.")
|
|
404
|
+
qualifiers_message = (
|
|
405
|
+
f"The contraction qualifiers are: "
|
|
406
|
+
f"A = {cutensor.Operator(self.qualifiers[0]).name}, "
|
|
407
|
+
f"B = {cutensor.Operator(self.qualifiers[1]).name}, "
|
|
408
|
+
f"C = {cutensor.Operator(self.qualifiers[2]).name}"
|
|
409
|
+
)
|
|
410
|
+
if self.num_inputs == 3:
|
|
411
|
+
qualifiers_message += f", D = {cutensor.Operator(self.qualifiers[3]).name}"
|
|
412
|
+
self.logger.info(qualifiers_message)
|
|
413
|
+
# ========================================================================
|
|
414
|
+
# Parse einsum modes and setup output tensor
|
|
415
|
+
# ========================================================================
|
|
416
|
+
self.input_modes, self.output_modes, _, size_dict = einsum_parser.parse_elementary_einsum(
|
|
417
|
+
inputs, output, self.a, self.b, self.c
|
|
418
|
+
)[:4]
|
|
419
|
+
|
|
420
|
+
self.output_shape = [size_dict[mode] for mode in self.output_modes]
|
|
421
|
+
|
|
422
|
+
# self.out is the output tensor that will be used for the execution
|
|
423
|
+
# self.out_return is the output tensor that will be returned by the execute method
|
|
424
|
+
self.output_provided = out is not None
|
|
425
|
+
self.result_strides: Sequence[int] | None = None
|
|
426
|
+
self.result_layout: Literal["optimized", "natural", "C", "F"] | None = None
|
|
427
|
+
|
|
428
|
+
if self.output_provided:
|
|
429
|
+
out = tensor_wrapper.wrap_operand(out)
|
|
430
|
+
if out.name != self.input_package:
|
|
431
|
+
raise ValueError(f"The output operand out must be a {self.input_package} tensor")
|
|
432
|
+
if out.device_id != self.input_device_id:
|
|
433
|
+
raise ValueError("The output operand out must be on the same device as the input operands.")
|
|
434
|
+
self.out_return = out
|
|
435
|
+
if out.device_id == self.execution_device_id:
|
|
436
|
+
self.out = out
|
|
437
|
+
else:
|
|
438
|
+
self.out = out.to(self.execution_device_id, stream_holder)
|
|
439
|
+
if log_debug:
|
|
440
|
+
self.logger.debug(f"The output tensor is copied to the execution device: {self.execution_device_id}.")
|
|
441
|
+
else:
|
|
442
|
+
self.out = self.out_return = None
|
|
443
|
+
|
|
444
|
+
# Determine the result layout to use
|
|
445
|
+
if self.options.result_layout == "auto":
|
|
446
|
+
if self.num_inputs == 2 and c is None:
|
|
447
|
+
self.result_layout = "optimized"
|
|
448
|
+
elif self.num_inputs == 3 and d is None:
|
|
449
|
+
# Optimized layout is binary-only, so ternary defaults to row-major.
|
|
450
|
+
self.result_layout = "C"
|
|
451
|
+
else:
|
|
452
|
+
# Follow the addend layout to satisfy cuTENSOR stride requirements.
|
|
453
|
+
self.result_layout = "natural"
|
|
454
|
+
else:
|
|
455
|
+
self.result_layout = cast(Literal["C", "F", "optimized"], self.options.result_layout)
|
|
456
|
+
if self.result_layout == "optimized":
|
|
457
|
+
if self.num_inputs == 3:
|
|
458
|
+
raise ValueError("Optimized result layout is not supported for ternary contraction.")
|
|
459
|
+
elif c is not None:
|
|
460
|
+
raise ValueError("Optimized result layout is not supported when an addend is specified.")
|
|
461
|
+
|
|
462
|
+
# Compute the result strides based on the result layout
|
|
463
|
+
if self.result_layout == "optimized":
|
|
464
|
+
assert self.num_inputs == 2, "Internal Error: optimized layout unexpectedly used for non-binary contraction."
|
|
465
|
+
self.result_strides = cutensor_utils.get_optimized_strides(
|
|
466
|
+
self.input_modes, self.output_modes, self.output_shape
|
|
467
|
+
)
|
|
468
|
+
self.logger.debug(f"The optimized result strides are: {self.result_strides}")
|
|
469
|
+
elif self.result_layout in {"C", "F"}:
|
|
470
|
+
self.result_strides = cutensor_utils.compute_strides(self.output_shape, order=self.result_layout)
|
|
471
|
+
elif self.result_layout == "natural":
|
|
472
|
+
if self.num_inputs == 2:
|
|
473
|
+
assert self.c is not None
|
|
474
|
+
self.result_strides = self.c.strides
|
|
475
|
+
else:
|
|
476
|
+
assert self.d is not None
|
|
477
|
+
self.result_strides = self.d.strides
|
|
478
|
+
if not is_contiguous_and_dense(self.output_shape, self.result_strides):
|
|
479
|
+
raise ValueError(
|
|
480
|
+
"When output is not specified and an addend is provided, the addend must be contiguous in memory."
|
|
481
|
+
)
|
|
482
|
+
else:
|
|
483
|
+
raise AssertionError("Internal Error: Invalid result layout.")
|
|
484
|
+
|
|
485
|
+
# ========================================================================
|
|
486
|
+
# Setup memory management
|
|
487
|
+
# ========================================================================
|
|
488
|
+
self.allocator = (
|
|
489
|
+
self.options.allocator
|
|
490
|
+
if self.options.allocator is not None
|
|
491
|
+
else memory._MEMORY_MANAGER[self.internal_package](self.execution_device_id, self.logger)
|
|
492
|
+
)
|
|
493
|
+
self.memory_limit = utils.get_memory_limit_from_device_id(self.options.memory_limit, self.execution_device_id)
|
|
494
|
+
self.tensor_descs = {}
|
|
495
|
+
|
|
496
|
+
# ========================================================================
|
|
497
|
+
# Create cuTensor handle and descriptors
|
|
498
|
+
# ========================================================================
|
|
499
|
+
with utils.device_ctx(self.execution_device_id):
|
|
500
|
+
if self.options.handle is not None:
|
|
501
|
+
self.own_handle = False
|
|
502
|
+
self.handle = self.options.handle
|
|
503
|
+
self.logger.info(f"The library handle has been set to the specified value: {self.handle}.")
|
|
504
|
+
else:
|
|
505
|
+
self.own_handle = True
|
|
506
|
+
self.handle = cutensor.create()
|
|
507
|
+
self.logger.info(f"The library handle has been created: {self.handle}.")
|
|
508
|
+
|
|
509
|
+
operands_names: tuple[str, ...]
|
|
510
|
+
if self.num_inputs == 2:
|
|
511
|
+
addend_name = "c"
|
|
512
|
+
operands_names = ("a", "b", "out") if c is None else ("a", "b", "c", "out")
|
|
513
|
+
else:
|
|
514
|
+
addend_name = "d"
|
|
515
|
+
operands_names = ("a", "b", "c", "out") if d is None else ("a", "b", "c", "d", "out")
|
|
516
|
+
|
|
517
|
+
# Create tensor descriptors for all relevant operands
|
|
518
|
+
for op_name in operands_names:
|
|
519
|
+
op = getattr(self, op_name)
|
|
520
|
+
if op is None:
|
|
521
|
+
assert op_name == "out", "Internal Error: out should be None if not provided."
|
|
522
|
+
# NOTE: Even when result_strides are not C order, the output pointer
|
|
523
|
+
# alignment (corresponding to the first element of the output tensor)
|
|
524
|
+
# from cupy/torch/ndbuffer's default allocator are still always
|
|
525
|
+
# aligned to at least 256 bytes.
|
|
526
|
+
self.tensor_descs[op_name] = cutensor_utils.TensorDescriptor.from_metadata(
|
|
527
|
+
self.handle, self.output_shape, self.data_type, strides=self.result_strides
|
|
528
|
+
)
|
|
529
|
+
else:
|
|
530
|
+
self.tensor_descs[op_name] = cutensor_utils.TensorDescriptor.from_tensor_holder(self.handle, op)
|
|
531
|
+
if log_debug:
|
|
532
|
+
self.logger.debug(
|
|
533
|
+
f"The tensor descriptor for operand {op_name} with shape {self.tensor_descs[op_name].shape}, "
|
|
534
|
+
f"strides {self.tensor_descs[op_name].strides}, dtype {self.tensor_descs[op_name].dtype}, "
|
|
535
|
+
f"and pointer alignment {self.tensor_descs[op_name].alignment} has been created."
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
if addend_name not in operands_names:
|
|
539
|
+
# If addend is not specified, we can reuse the output descriptor for the addend
|
|
540
|
+
self.tensor_descs[addend_name] = self.tensor_descs["out"]
|
|
541
|
+
|
|
542
|
+
# ========================================================================
|
|
543
|
+
# Create contraction descriptor
|
|
544
|
+
# ========================================================================
|
|
545
|
+
if self.num_inputs == 2:
|
|
546
|
+
self.contraction_desc = cutensor.create_contraction(
|
|
547
|
+
self.handle,
|
|
548
|
+
self.tensor_descs["a"].ptr,
|
|
549
|
+
self.input_modes[0],
|
|
550
|
+
self.qualifiers[0],
|
|
551
|
+
self.tensor_descs["b"].ptr,
|
|
552
|
+
self.input_modes[1],
|
|
553
|
+
self.qualifiers[1],
|
|
554
|
+
self.tensor_descs["c"].ptr,
|
|
555
|
+
self.output_modes, # NOTE: currently assuming c has the same output modes as the out
|
|
556
|
+
self.qualifiers[2], # only identity operator is supported for c
|
|
557
|
+
self.tensor_descs["out"].ptr,
|
|
558
|
+
self.output_modes,
|
|
559
|
+
self.compute_type,
|
|
560
|
+
)
|
|
561
|
+
else:
|
|
562
|
+
self.contraction_desc = cutensor.create_contraction_trinary(
|
|
563
|
+
self.handle,
|
|
564
|
+
self.tensor_descs["a"].ptr,
|
|
565
|
+
self.input_modes[0],
|
|
566
|
+
self.qualifiers[0],
|
|
567
|
+
self.tensor_descs["b"].ptr,
|
|
568
|
+
self.input_modes[1],
|
|
569
|
+
self.qualifiers[1],
|
|
570
|
+
self.tensor_descs["c"].ptr,
|
|
571
|
+
self.input_modes[2],
|
|
572
|
+
self.qualifiers[2],
|
|
573
|
+
self.tensor_descs["d"].ptr,
|
|
574
|
+
self.output_modes,
|
|
575
|
+
self.qualifiers[3], # only identity operator is supported for d
|
|
576
|
+
self.tensor_descs["out"].ptr,
|
|
577
|
+
self.output_modes,
|
|
578
|
+
self.compute_type,
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
# ========================================================================
|
|
582
|
+
# Query scalar type and initialize planning/workspace state
|
|
583
|
+
# ========================================================================
|
|
584
|
+
scalar_dtype = cutensor.get_operation_descriptor_attribute_dtype(cutensor.OperationDescriptorAttribute.SCALAR_TYPE)
|
|
585
|
+
scalar_dtype_buffer = np.empty(1, dtype=scalar_dtype)
|
|
586
|
+
cutensor.operation_descriptor_get_attribute(
|
|
587
|
+
self.handle,
|
|
588
|
+
self.contraction_desc,
|
|
589
|
+
cutensor.OperationDescriptorAttribute.SCALAR_TYPE,
|
|
590
|
+
scalar_dtype_buffer.ctypes.data,
|
|
591
|
+
scalar_dtype_buffer.itemsize,
|
|
592
|
+
)
|
|
593
|
+
self.scalar_type = CudaDataType(scalar_dtype_buffer.item())
|
|
594
|
+
|
|
595
|
+
self.alpha = np.empty(1, dtype=DATA_TYPE_TO_NAME[self.scalar_type])
|
|
596
|
+
self.beta = np.empty(1, dtype=DATA_TYPE_TO_NAME[self.scalar_type])
|
|
597
|
+
|
|
598
|
+
# Initialize planning-related members
|
|
599
|
+
self.contraction_planned = False
|
|
600
|
+
self.plan_preference_ptr = cutensor.create_plan_preference(self.handle, cutensor.Algo.DEFAULT, cutensor.JitMode.NONE)
|
|
601
|
+
self._plan_preference = ContractionPlanPreference(self)
|
|
602
|
+
|
|
603
|
+
# Initialize remaining members
|
|
604
|
+
# Workspace lifecycle is managed by `Workspace` (allocate-on-demand,
|
|
605
|
+
# reuse, exception cleanup, stream-ordered free).
|
|
606
|
+
self.workspace = Workspace(
|
|
607
|
+
self.allocator,
|
|
608
|
+
self.logger,
|
|
609
|
+
label="contraction workspace",
|
|
610
|
+
device_id=self.execution_device_id,
|
|
611
|
+
)
|
|
612
|
+
self.last_compute_event = None
|
|
613
|
+
self.plan_ptr = None
|
|
614
|
+
|
|
615
|
+
if log_info:
|
|
616
|
+
self.logger.info(f"The {self.__class__.__name__} object has been created.")
|
|
617
|
+
|
|
618
|
+
def _check_valid_contraction(self, *args, **kwargs):
|
|
619
|
+
"""
|
|
620
|
+
Check if the ElementaryContraction object is alive and well.
|
|
621
|
+
"""
|
|
622
|
+
if not self.valid_state:
|
|
623
|
+
raise InvalidContractionState("The ElementaryContraction object cannot be used after resources are free'd")
|
|
624
|
+
|
|
625
|
+
def _check_valid_operands(self, *args, **kwargs):
|
|
626
|
+
"""
|
|
627
|
+
Check if the operands are available for the operation.
|
|
628
|
+
"""
|
|
629
|
+
what = kwargs["what"]
|
|
630
|
+
if self._operands_released:
|
|
631
|
+
raise RuntimeError(
|
|
632
|
+
f"{what} cannot be performed after the operands have been released. "
|
|
633
|
+
f"Use reset_operands() to provide new operands before performing the {what.lower()}."
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
def _free_plan_resources(self, exception: Exception | None = None) -> bool:
|
|
637
|
+
"""
|
|
638
|
+
Free resources allocated in planning.
|
|
639
|
+
"""
|
|
640
|
+
|
|
641
|
+
if self.plan_ptr is not None:
|
|
642
|
+
cutensor.destroy_plan(self.plan_ptr)
|
|
643
|
+
self.plan_ptr = None
|
|
644
|
+
|
|
645
|
+
if self.plan_preference_ptr is not None:
|
|
646
|
+
cutensor.destroy_plan_preference(self.plan_preference_ptr)
|
|
647
|
+
self.plan_preference_ptr = None
|
|
648
|
+
|
|
649
|
+
self.contraction_planned = False
|
|
650
|
+
return True
|
|
651
|
+
|
|
652
|
+
def _check_planned(self, *args, **kwargs):
|
|
653
|
+
what = kwargs["what"]
|
|
654
|
+
if not self.contraction_planned:
|
|
655
|
+
raise RuntimeError(f"{what} cannot be performed before plan() has been called.")
|
|
656
|
+
|
|
657
|
+
@property
|
|
658
|
+
def plan_preference(self):
|
|
659
|
+
"""
|
|
660
|
+
An accessor to configure or query the contraction planning phase
|
|
661
|
+
attributes.
|
|
662
|
+
|
|
663
|
+
Returns:
|
|
664
|
+
A :class:`ContractionPlanPreference` object, whose attributes can be set (or
|
|
665
|
+
queried) to configure the planning phase.
|
|
666
|
+
|
|
667
|
+
.. seealso::
|
|
668
|
+
:class:`ContractionPlanPreference`, :meth:`plan`.
|
|
669
|
+
"""
|
|
670
|
+
return self._plan_preference
|
|
671
|
+
|
|
672
|
+
@utils.precondition(_check_valid_contraction)
|
|
673
|
+
def reset_operands(self, *, a=None, b=None, c=None, d=None, out=None, stream: utils.AnyStream | int | None = None):
|
|
674
|
+
if self.num_inputs == 2 and d is not None:
|
|
675
|
+
raise RuntimeError("Internal Error: For pairwise contractions, d can not be set.")
|
|
676
|
+
|
|
677
|
+
# if the operands have been released, all operands must be provided
|
|
678
|
+
if self._operands_released:
|
|
679
|
+
all_provided = (
|
|
680
|
+
a is not None
|
|
681
|
+
and b is not None
|
|
682
|
+
and (c is not None or not self.c_provided)
|
|
683
|
+
and (d is not None or not self.d_provided)
|
|
684
|
+
and (out is not None or not self.output_provided)
|
|
685
|
+
)
|
|
686
|
+
if not all_provided:
|
|
687
|
+
raise ValueError("After release_operands(), all operands must be provided.")
|
|
688
|
+
elif all(arg is None for arg in (a, b, c, d, out)):
|
|
689
|
+
# All arguments are None: there is nothing to update, so reject the call.
|
|
690
|
+
raise ValueError("reset_operands() requires at least one operand to be provided.")
|
|
691
|
+
|
|
692
|
+
stream_holder = None # lazy initialization
|
|
693
|
+
for op_name, op in zip(["a", "b", "c", "d", "out"], [a, b, c, d, out], strict=False):
|
|
694
|
+
# if op is None, we keep the original value of the operand,
|
|
695
|
+
# so skip the rest of the logic for this operand
|
|
696
|
+
if op is None:
|
|
697
|
+
continue
|
|
698
|
+
|
|
699
|
+
if (op_name == "c" and not self.c_provided) or (op_name == "d" and not self.d_provided):
|
|
700
|
+
raise ValueError(
|
|
701
|
+
f"operand {op_name} was not specified during the initialization "
|
|
702
|
+
f"of the ElementaryContraction object and therefore can not be reset "
|
|
703
|
+
f"to a concrete tensor."
|
|
704
|
+
)
|
|
705
|
+
op = tensor_wrapper.wrap_operand(op)
|
|
706
|
+
if op.name != self.input_package:
|
|
707
|
+
raise ValueError(f"The operand {op_name} must be a {self.input_package} tensor")
|
|
708
|
+
if op.device_id != self.input_device_id:
|
|
709
|
+
raise ValueError(
|
|
710
|
+
f"The operand {op_name} must be on the same device "
|
|
711
|
+
f"as the operands provided during the initialization of the "
|
|
712
|
+
f"ElementaryContraction object."
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
tensor_desc = self.tensor_descs[op_name]
|
|
716
|
+
|
|
717
|
+
error_pattern = (
|
|
718
|
+
f"The operand {op_name} must have the same {{attr}} "
|
|
719
|
+
f"as the one specified during the initialization of the "
|
|
720
|
+
f"ElementaryContraction object."
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
if tensor_desc.shape != op.shape:
|
|
724
|
+
raise ValueError(error_pattern.format(attr="shape"))
|
|
725
|
+
|
|
726
|
+
if tensor_desc.dtype != op.dtype:
|
|
727
|
+
raise ValueError(error_pattern.format(attr="dtype"))
|
|
728
|
+
|
|
729
|
+
if tensor_desc.strides != op.strides:
|
|
730
|
+
raise ValueError(error_pattern.format(attr="strides"))
|
|
731
|
+
|
|
732
|
+
if op_name == "out":
|
|
733
|
+
self.out_return = op
|
|
734
|
+
|
|
735
|
+
if op.device_id != self.execution_device_id:
|
|
736
|
+
if stream_holder is None:
|
|
737
|
+
stream_holder = utils.get_or_create_stream(self.execution_device_id, stream, self.internal_package)
|
|
738
|
+
if op_name == "out" and self.out is not None:
|
|
739
|
+
# if out_name is "out" and we own a valid self.out,
|
|
740
|
+
# we can directly reuse it here
|
|
741
|
+
op = self.out
|
|
742
|
+
else:
|
|
743
|
+
op = op.to(self.execution_device_id, stream_holder)
|
|
744
|
+
elif cutensor_utils.compute_pointer_alignment(op.data_ptr) % self.tensor_descs[op_name].alignment:
|
|
745
|
+
# If the operand is in the same memory space as the
|
|
746
|
+
# execution space (copy not needed), we need to check if the pointer
|
|
747
|
+
# alignment is the same or a multiple of the original pointer alignments.
|
|
748
|
+
# If the operand is in host memory, this check can be skipped as we
|
|
749
|
+
# internally copy the operand to the execution space and
|
|
750
|
+
# device ptr alignment should be guaranteed to be the same.
|
|
751
|
+
raise ValueError(
|
|
752
|
+
f"The pointer alignment of the operand {op_name} must be the same or a multiple of the corresponding "
|
|
753
|
+
f"pointer alignment specified during the initialization of the {self.__class__.__name__} object."
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
setattr(self, op_name, op)
|
|
757
|
+
self.logger.info(f"operand {op_name} has been reset to the new operand provided.")
|
|
758
|
+
self._operands_released = False
|
|
759
|
+
return
|
|
760
|
+
|
|
761
|
+
@utils.precondition(_check_valid_contraction)
|
|
762
|
+
def _release_operands(self):
|
|
763
|
+
if self._operands_released:
|
|
764
|
+
self.logger.info("Operands have already been released; nothing to do.")
|
|
765
|
+
return
|
|
766
|
+
|
|
767
|
+
# We release the internal tensor references held by the wrappers
|
|
768
|
+
# for the user-provided operands and/or their GPU mirrors.
|
|
769
|
+
# The TensorHolder wrappers themselves are kept alive so that
|
|
770
|
+
# reset_operands_unchecked can reuse them without re-wrapping.
|
|
771
|
+
self.a.tensor = None
|
|
772
|
+
self.b.tensor = None
|
|
773
|
+
if self.c_provided:
|
|
774
|
+
self.c.tensor = None
|
|
775
|
+
if self.d_provided:
|
|
776
|
+
self.d.tensor = None
|
|
777
|
+
if self.output_provided:
|
|
778
|
+
# self.out_return always holds the user's tensor.
|
|
779
|
+
# self.out holds the user's tensor when operand memory space
|
|
780
|
+
# matches execution (same device; same object as self.out_return),
|
|
781
|
+
# or an internal device mirror when they differ (inputs on CPU).
|
|
782
|
+
# In both cases, release the inner tensor references.
|
|
783
|
+
self.out_return.tensor = None
|
|
784
|
+
self.out.tensor = None
|
|
785
|
+
self._operands_released = True
|
|
786
|
+
self.logger.info("User-provided operands have been released.")
|
|
787
|
+
|
|
788
|
+
def _reset_operands_unchecked(
|
|
789
|
+
self, *, a=None, b=None, c=None, d=None, out=None, stream: utils.AnyStream | int | None = None
|
|
790
|
+
):
|
|
791
|
+
# Since we have the caller's compatibility guarantee for the inputs, we can leverage
|
|
792
|
+
# the metadata stored during initialization to know if the inputs were on the GPU.
|
|
793
|
+
# If the memory space of the inputs matches the execution space, namely the inputs
|
|
794
|
+
# during initialization resided on the GPU, then we know/expect the newly provided
|
|
795
|
+
# inputs must also reside on the GPU. The TensorHolder wrappers are always alive
|
|
796
|
+
# (release_operands only clears .tensor), so we can unconditionally swap the inner
|
|
797
|
+
# tensor — no wrap_operand() needed.
|
|
798
|
+
if self.input_device_id != "cpu":
|
|
799
|
+
if a is not None:
|
|
800
|
+
self.a.tensor = a
|
|
801
|
+
if b is not None:
|
|
802
|
+
self.b.tensor = b
|
|
803
|
+
if c is not None:
|
|
804
|
+
assert self.c is not None
|
|
805
|
+
self.c.tensor = c
|
|
806
|
+
if d is not None:
|
|
807
|
+
assert self.d is not None
|
|
808
|
+
self.d.tensor = d
|
|
809
|
+
if out is not None:
|
|
810
|
+
self.out.tensor = out
|
|
811
|
+
self.out_return.tensor = out
|
|
812
|
+
|
|
813
|
+
self._operands_released = False
|
|
814
|
+
return
|
|
815
|
+
|
|
816
|
+
# if we are here, it means the inputs were on the CPU
|
|
817
|
+
# so we need to distinguish between the case where the operands
|
|
818
|
+
# were released and the case where they were not released.
|
|
819
|
+
if self._operands_released:
|
|
820
|
+
# CPU inputs after release: device mirrors were freed, need re-allocation.
|
|
821
|
+
stream_holder = utils.get_or_create_stream(self.execution_device_id, stream, self.internal_package)
|
|
822
|
+
if a is not None:
|
|
823
|
+
self.a = tensor_wrapper.wrap_operand(a).to(self.execution_device_id, stream_holder)
|
|
824
|
+
if b is not None:
|
|
825
|
+
self.b = tensor_wrapper.wrap_operand(b).to(self.execution_device_id, stream_holder)
|
|
826
|
+
if c is not None:
|
|
827
|
+
self.c = tensor_wrapper.wrap_operand(c).to(self.execution_device_id, stream_holder)
|
|
828
|
+
if d is not None:
|
|
829
|
+
self.d = tensor_wrapper.wrap_operand(d).to(self.execution_device_id, stream_holder)
|
|
830
|
+
if out is not None:
|
|
831
|
+
out_wrapped = tensor_wrapper.wrap_operand(out)
|
|
832
|
+
self.out = out_wrapped.to(self.execution_device_id, stream_holder)
|
|
833
|
+
self.out_return = out_wrapped
|
|
834
|
+
else:
|
|
835
|
+
# CPU inputs, not released: device mirrors are valid, copy into them.
|
|
836
|
+
stream_holder = utils.get_or_create_stream(self.execution_device_id, stream, self.internal_package)
|
|
837
|
+
if a is not None:
|
|
838
|
+
self.a.copy_(tensor_wrapper.wrap_operand(a), stream_holder=stream_holder)
|
|
839
|
+
if b is not None:
|
|
840
|
+
self.b.copy_(tensor_wrapper.wrap_operand(b), stream_holder=stream_holder)
|
|
841
|
+
if c is not None:
|
|
842
|
+
assert self.c is not None
|
|
843
|
+
self.c.copy_(tensor_wrapper.wrap_operand(c), stream_holder=stream_holder)
|
|
844
|
+
if d is not None:
|
|
845
|
+
assert self.d is not None
|
|
846
|
+
self.d.copy_(tensor_wrapper.wrap_operand(d), stream_holder=stream_holder)
|
|
847
|
+
if out is not None:
|
|
848
|
+
out_wrapped = tensor_wrapper.wrap_operand(out)
|
|
849
|
+
self.out.copy_(out_wrapped, stream_holder=stream_holder)
|
|
850
|
+
self.out_return = out_wrapped
|
|
851
|
+
|
|
852
|
+
self._operands_released = False
|
|
853
|
+
|
|
854
|
+
@utils.precondition(_check_valid_contraction)
|
|
855
|
+
@utils.atomic(_free_plan_resources, method=True)
|
|
856
|
+
def plan(self, *, stream: utils.AnyStream | int | None = None):
|
|
857
|
+
"""
|
|
858
|
+
Plan the tensor contraction. The planning phase can be optionally configured through
|
|
859
|
+
the property :attr:`plan_preference` (an object of type
|
|
860
|
+
:class:`ContractionPlanPreference`).
|
|
861
|
+
|
|
862
|
+
Args:
|
|
863
|
+
stream: {stream}
|
|
864
|
+
|
|
865
|
+
.. seealso::
|
|
866
|
+
:attr:`plan_preference`, :class:`ContractionPlanPreference`.
|
|
867
|
+
|
|
868
|
+
Note:
|
|
869
|
+
If the :attr:`plan_preference` has been updated, a :meth:`plan` call is
|
|
870
|
+
required to apply the changes.
|
|
871
|
+
"""
|
|
872
|
+
log_info = self.logger.isEnabledFor(logging.INFO)
|
|
873
|
+
log_debug = self.logger.isEnabledFor(logging.DEBUG)
|
|
874
|
+
|
|
875
|
+
# A new plan needs to be created at each plan() call
|
|
876
|
+
if self.plan_ptr is not None:
|
|
877
|
+
cutensor.destroy_plan(self.plan_ptr)
|
|
878
|
+
self.plan_ptr = None
|
|
879
|
+
if log_debug:
|
|
880
|
+
self.logger.debug("The previous plan has been destroyed.")
|
|
881
|
+
|
|
882
|
+
if log_info:
|
|
883
|
+
self.logger.info("= PLANNING PHASE =")
|
|
884
|
+
stream_holder = utils.get_or_create_stream(self.execution_device_id, stream, self.internal_package)
|
|
885
|
+
required_workspace_size_buffer = np.empty(
|
|
886
|
+
1, dtype=cutensor.get_plan_attribute_dtype(cutensor.PlanAttribute.REQUIRED_WORKSPACE)
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
with utils.cuda_call_ctx(stream_holder, self.blocking, timing=log_info) as (
|
|
890
|
+
self.last_compute_event,
|
|
891
|
+
elapsed,
|
|
892
|
+
):
|
|
893
|
+
self.plan_ptr = cutensor.create_plan(
|
|
894
|
+
self.handle, self.contraction_desc, self.plan_preference_ptr, self.memory_limit
|
|
895
|
+
)
|
|
896
|
+
cutensor.plan_get_attribute(
|
|
897
|
+
self.handle,
|
|
898
|
+
self.plan_ptr,
|
|
899
|
+
cutensor.PlanAttribute.REQUIRED_WORKSPACE,
|
|
900
|
+
required_workspace_size_buffer.ctypes.data,
|
|
901
|
+
required_workspace_size_buffer.itemsize,
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
if log_info and elapsed.data is not None:
|
|
905
|
+
self.logger.info(f"The planning phase took {elapsed.data:.3f} ms to complete.")
|
|
906
|
+
|
|
907
|
+
self.workspace.set_size(required_workspace_size_buffer.item())
|
|
908
|
+
if log_info:
|
|
909
|
+
self.logger.info(f"The required workspace size for the contraction operation is {self.workspace.size}.")
|
|
910
|
+
self.contraction_planned = True
|
|
911
|
+
|
|
912
|
+
@utils.precondition(_check_valid_contraction)
|
|
913
|
+
@utils.precondition(_check_planned, "Execution")
|
|
914
|
+
@utils.precondition(_check_valid_operands, "Execution")
|
|
915
|
+
def execute(self, *, alpha=1.0, beta=None, release_workspace=False, stream: utils.AnyStream | int | None = None):
|
|
916
|
+
"""
|
|
917
|
+
Execute a prepared tensor contraction.
|
|
918
|
+
|
|
919
|
+
Args:
|
|
920
|
+
alpha: {alpha}
|
|
921
|
+
|
|
922
|
+
beta: {beta}
|
|
923
|
+
|
|
924
|
+
release_workspace: {release_workspace}
|
|
925
|
+
|
|
926
|
+
stream: {stream}
|
|
927
|
+
|
|
928
|
+
Returns:
|
|
929
|
+
{result}
|
|
930
|
+
"""
|
|
931
|
+
if beta is None:
|
|
932
|
+
if self.num_inputs == 2 and self.c is not None:
|
|
933
|
+
raise ValueError("beta must be set when c is specified in a binary contraction")
|
|
934
|
+
elif self.num_inputs == 3 and self.d is not None:
|
|
935
|
+
raise ValueError("beta must be set when d is specified in a ternary contraction")
|
|
936
|
+
beta = 0.0
|
|
937
|
+
else:
|
|
938
|
+
if self.num_inputs == 2 and self.c is None:
|
|
939
|
+
raise ValueError("For binary contraction, beta can only be set if c is specified")
|
|
940
|
+
elif self.num_inputs == 3 and self.d is None:
|
|
941
|
+
raise ValueError("For ternary contraction, beta can only be set if d is specified")
|
|
942
|
+
|
|
943
|
+
log_info = self.logger.isEnabledFor(logging.INFO)
|
|
944
|
+
log_debug = self.logger.isEnabledFor(logging.DEBUG)
|
|
945
|
+
|
|
946
|
+
self.alpha[0] = alpha
|
|
947
|
+
self.beta[0] = beta
|
|
948
|
+
|
|
949
|
+
stream_holder = utils.get_or_create_stream(self.execution_device_id, stream, self.internal_package)
|
|
950
|
+
|
|
951
|
+
# The buffer to be used for cutensor execution
|
|
952
|
+
# If out was provided during initialization, this would have been created
|
|
953
|
+
# regardless of where it resided (CPU or GPU).
|
|
954
|
+
# If out was not provided during initialization, we need to create a new buffer
|
|
955
|
+
if self.out is None:
|
|
956
|
+
assert not self.output_provided, (
|
|
957
|
+
"Internal Error: out cannot be None if the output was provided during initialization."
|
|
958
|
+
)
|
|
959
|
+
self.out = self.a.__class__.empty(
|
|
960
|
+
self.output_shape,
|
|
961
|
+
device_id=self.execution_device_id,
|
|
962
|
+
dtype=self.data_type,
|
|
963
|
+
stream_holder=stream_holder,
|
|
964
|
+
strides=self.result_strides,
|
|
965
|
+
)
|
|
966
|
+
alignment = cutensor_utils.compute_pointer_alignment(self.out.data_ptr)
|
|
967
|
+
if alignment % self.tensor_descs["out"].alignment:
|
|
968
|
+
raise RuntimeError(
|
|
969
|
+
f"Output tensor pointer alignment is incompatible with the contraction plan: expected a multiple "
|
|
970
|
+
f"of {self.tensor_descs['out'].alignment} bytes, but detected {alignment} bytes at execution time. "
|
|
971
|
+
f"This can happen if the default allocator for {self.internal_package} was changed to one that "
|
|
972
|
+
f"does not provide the alignment assumed by the contraction plan."
|
|
973
|
+
)
|
|
974
|
+
|
|
975
|
+
if log_debug:
|
|
976
|
+
self.logger.debug(
|
|
977
|
+
f"The output tensor of type {type(self.out.tensor)} with shape {self.out.shape} has been created."
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
if log_info:
|
|
981
|
+
self.logger.info("= EXECUTION PHASE =")
|
|
982
|
+
self.logger.info("Starting tensor contraction calculation...")
|
|
983
|
+
self.logger.info(f"{self.call_prologue}")
|
|
984
|
+
self.logger.info(f"The specified stream for execute() is {stream_holder.obj}.")
|
|
985
|
+
|
|
986
|
+
with self.workspace.allocate_perhaps(
|
|
987
|
+
stream_holder,
|
|
988
|
+
get_last_event=lambda: self.last_compute_event,
|
|
989
|
+
) as workspace:
|
|
990
|
+
with utils.cuda_call_ctx(stream_holder, self.blocking, timing=log_info) as (
|
|
991
|
+
self.last_compute_event,
|
|
992
|
+
elapsed,
|
|
993
|
+
):
|
|
994
|
+
if self.num_inputs == 2:
|
|
995
|
+
cutensor.contract(
|
|
996
|
+
self.handle,
|
|
997
|
+
self.plan_ptr,
|
|
998
|
+
self.alpha.ctypes.data,
|
|
999
|
+
self.a.data_ptr,
|
|
1000
|
+
self.b.data_ptr,
|
|
1001
|
+
self.beta.ctypes.data,
|
|
1002
|
+
self.c.data_ptr if self.c is not None else self.out.data_ptr,
|
|
1003
|
+
self.out.data_ptr,
|
|
1004
|
+
workspace.raw_ptr,
|
|
1005
|
+
workspace.size,
|
|
1006
|
+
stream_holder.ptr,
|
|
1007
|
+
)
|
|
1008
|
+
else:
|
|
1009
|
+
assert self.c is not None, "Internal Error: ternary contraction requires operand c."
|
|
1010
|
+
cutensor.contract_trinary(
|
|
1011
|
+
self.handle,
|
|
1012
|
+
self.plan_ptr,
|
|
1013
|
+
self.alpha.ctypes.data,
|
|
1014
|
+
self.a.data_ptr,
|
|
1015
|
+
self.b.data_ptr,
|
|
1016
|
+
self.c.data_ptr,
|
|
1017
|
+
self.beta.ctypes.data,
|
|
1018
|
+
self.d.data_ptr if self.d is not None else self.out.data_ptr,
|
|
1019
|
+
self.out.data_ptr,
|
|
1020
|
+
workspace.raw_ptr,
|
|
1021
|
+
workspace.size,
|
|
1022
|
+
stream_holder.ptr,
|
|
1023
|
+
)
|
|
1024
|
+
|
|
1025
|
+
if release_workspace:
|
|
1026
|
+
workspace.release(self.last_compute_event)
|
|
1027
|
+
self.last_compute_event = None
|
|
1028
|
+
|
|
1029
|
+
if log_info and elapsed.data is not None:
|
|
1030
|
+
self.logger.info(f"The tensor contraction calculation took {elapsed.data:.3f} ms to complete.")
|
|
1031
|
+
|
|
1032
|
+
if self.output_provided:
|
|
1033
|
+
if self.execution_device_id != self.input_device_id:
|
|
1034
|
+
self.out_return.copy_(self.out, stream_holder)
|
|
1035
|
+
return self.out_return.tensor
|
|
1036
|
+
else:
|
|
1037
|
+
if self.execution_device_id != self.input_device_id:
|
|
1038
|
+
return self.out.to(self.input_device_id, stream_holder).tensor
|
|
1039
|
+
else:
|
|
1040
|
+
out = self.out
|
|
1041
|
+
# release the output tensor as the ownership is transferred to the caller
|
|
1042
|
+
self.out = None
|
|
1043
|
+
return out.tensor
|
|
1044
|
+
|
|
1045
|
+
@utils.precondition(_check_valid_contraction)
|
|
1046
|
+
def free(self):
|
|
1047
|
+
"""Free tensor contraction resources.
|
|
1048
|
+
|
|
1049
|
+
It is recommended that the contraction object be used
|
|
1050
|
+
within a context, but if it is not possible then this method must be
|
|
1051
|
+
called explicitly to ensure that the tensor contraction resources
|
|
1052
|
+
(especially internal library objects) are properly cleaned up.
|
|
1053
|
+
"""
|
|
1054
|
+
|
|
1055
|
+
if not self.valid_state:
|
|
1056
|
+
return
|
|
1057
|
+
|
|
1058
|
+
class_name = self.__class__.__name__
|
|
1059
|
+
try:
|
|
1060
|
+
self.workspace.release(self.last_compute_event)
|
|
1061
|
+
self.last_compute_event = None
|
|
1062
|
+
|
|
1063
|
+
self._free_plan_resources()
|
|
1064
|
+
|
|
1065
|
+
# Free handle if we own it.
|
|
1066
|
+
if self.handle is not None and self.own_handle:
|
|
1067
|
+
cutensor.destroy(self.handle)
|
|
1068
|
+
self.handle, self.own_handle = None, False
|
|
1069
|
+
|
|
1070
|
+
if self.contraction_desc is not None:
|
|
1071
|
+
cutensor.destroy_operation_descriptor(self.contraction_desc)
|
|
1072
|
+
self.contraction_desc = None
|
|
1073
|
+
|
|
1074
|
+
self.tensor_descs = None
|
|
1075
|
+
|
|
1076
|
+
# Set all attributes to None except for logger and valid_state
|
|
1077
|
+
_keep = {"logger", "valid_state"}
|
|
1078
|
+
for attr in list(vars(self)):
|
|
1079
|
+
if attr not in _keep:
|
|
1080
|
+
setattr(self, attr, None)
|
|
1081
|
+
|
|
1082
|
+
except Exception as e:
|
|
1083
|
+
self.logger.critical(f"Internal error: only part of the {class_name} object's resources have been released.")
|
|
1084
|
+
self.logger.critical(str(e))
|
|
1085
|
+
raise e
|
|
1086
|
+
finally:
|
|
1087
|
+
self.valid_state = False
|
|
1088
|
+
|
|
1089
|
+
self.logger.info(f"The {class_name} object's resources have been released.")
|
|
1090
|
+
|
|
1091
|
+
def __enter__(self):
|
|
1092
|
+
return self
|
|
1093
|
+
|
|
1094
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
1095
|
+
self.free()
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
@utils.docstring_decorator(SHARED_CONTRACTION_DOCUMENTATION, skip_missing=False)
|
|
1099
|
+
class BinaryContraction(_ElementaryContraction):
|
|
1100
|
+
"""
|
|
1101
|
+
Create a stateful object encapsulating the specified binary tensor contraction
|
|
1102
|
+
:math:`\\alpha a @ b + \\beta c` and the required resources to perform the operation.
|
|
1103
|
+
A stateful object can be used to amortize the cost of preparation (planning in the
|
|
1104
|
+
case of binary tensor contraction) across multiple executions (also see the
|
|
1105
|
+
:ref:`Stateful APIs<host api types>` section).
|
|
1106
|
+
|
|
1107
|
+
The function-form API :func:`binary_contraction` is a convenient alternative to using
|
|
1108
|
+
stateful objects for *single* use (the user needs to perform just one tensor
|
|
1109
|
+
contraction, for example), in which case there is no possibility of amortizing
|
|
1110
|
+
preparatory costs. The function-form APIs are just convenience wrappers around
|
|
1111
|
+
the stateful object APIs.
|
|
1112
|
+
|
|
1113
|
+
Using the stateful object typically involves the following steps:
|
|
1114
|
+
|
|
1115
|
+
1. **Problem Specification**: Initialize the object with a defined operation and
|
|
1116
|
+
options.
|
|
1117
|
+
2. **Preparation**: Use :meth:`plan` to determine the best algorithmic implementation
|
|
1118
|
+
for this specific binary tensor contraction operation.
|
|
1119
|
+
3. **Execution**: Perform the tensor contraction computation with :meth:`execute`.
|
|
1120
|
+
4. **Resource Management**: Ensure all resources are released either by explicitly
|
|
1121
|
+
calling :meth:`free` or by managing the stateful object within a context manager.
|
|
1122
|
+
|
|
1123
|
+
Detailed information on what's happening in the various phases described above can be
|
|
1124
|
+
obtained by passing in a :class:`logging.Logger` object to :class:`ContractionOptions`
|
|
1125
|
+
or by setting the appropriate options in the root logger object,
|
|
1126
|
+
which is used by default:
|
|
1127
|
+
|
|
1128
|
+
>>> import logging
|
|
1129
|
+
>>> logging.basicConfig(
|
|
1130
|
+
... level=logging.INFO,
|
|
1131
|
+
... format="%(asctime)s %(levelname)-8s %(message)s",
|
|
1132
|
+
... datefmt="%m-%d %H:%M:%S",
|
|
1133
|
+
... )
|
|
1134
|
+
|
|
1135
|
+
A user can select the desired logging level and, in general, take advantage of all of
|
|
1136
|
+
the functionality offered by the Python `logging` module.
|
|
1137
|
+
|
|
1138
|
+
Args:
|
|
1139
|
+
a: {a}
|
|
1140
|
+
|
|
1141
|
+
b: {b}
|
|
1142
|
+
|
|
1143
|
+
c: {addend}
|
|
1144
|
+
|
|
1145
|
+
out: {out}
|
|
1146
|
+
|
|
1147
|
+
qualifiers: {qualifiers}
|
|
1148
|
+
|
|
1149
|
+
stream: {stream}
|
|
1150
|
+
|
|
1151
|
+
options: {options}
|
|
1152
|
+
|
|
1153
|
+
execution: {execution}
|
|
1154
|
+
|
|
1155
|
+
.. seealso::
|
|
1156
|
+
:attr:`plan_preference`, :meth:`plan`, :meth:`reset_operands`,
|
|
1157
|
+
:meth:`release_operands`, :meth:`execute`
|
|
1158
|
+
|
|
1159
|
+
Examples:
|
|
1160
|
+
|
|
1161
|
+
>>> import numpy as np
|
|
1162
|
+
>>> import nvmath
|
|
1163
|
+
|
|
1164
|
+
Create two 3-D float64 ndarrays on the CPU:
|
|
1165
|
+
|
|
1166
|
+
>>> M, N, K = 32, 32, 32
|
|
1167
|
+
>>> a = np.random.rand(M, N, K)
|
|
1168
|
+
>>> b = np.random.rand(N, K, M)
|
|
1169
|
+
|
|
1170
|
+
We will define a binary tensor contraction operation.
|
|
1171
|
+
|
|
1172
|
+
Create a BinaryContraction object encapsulating the problem specification above:
|
|
1173
|
+
|
|
1174
|
+
>>> contraction = nvmath.tensor.BinaryContraction("ijk,jkl->il", a, b)
|
|
1175
|
+
|
|
1176
|
+
Options can be provided above to control the behavior of the operation using the
|
|
1177
|
+
`options` argument (see :class:`ContractionOptions`).
|
|
1178
|
+
|
|
1179
|
+
Next, plan the operation. Optionally, preferences can
|
|
1180
|
+
be specified for planning:
|
|
1181
|
+
|
|
1182
|
+
>>> contraction.plan()
|
|
1183
|
+
|
|
1184
|
+
Now execute the binary tensor contraction, and obtain the result `r1` as a NumPy
|
|
1185
|
+
ndarray.
|
|
1186
|
+
|
|
1187
|
+
>>> r1 = contraction.execute()
|
|
1188
|
+
|
|
1189
|
+
Finally, free the object's resources. To avoid having to explicitly making this
|
|
1190
|
+
call, it's recommended to use the BinaryContraction object as a context manager
|
|
1191
|
+
as shown below, if possible.
|
|
1192
|
+
|
|
1193
|
+
>>> contraction.free()
|
|
1194
|
+
|
|
1195
|
+
Note that all :class:`BinaryContraction` methods execute on the current
|
|
1196
|
+
stream by default. Alternatively, the `stream` argument can be used to run a
|
|
1197
|
+
method on a specified stream.
|
|
1198
|
+
|
|
1199
|
+
Let's now look at the same problem with CuPy ndarrays on the GPU.
|
|
1200
|
+
|
|
1201
|
+
Create a 3-D float64 CuPy ndarray on the GPU:
|
|
1202
|
+
|
|
1203
|
+
>>> import cupy as cp
|
|
1204
|
+
>>> a = cp.random.rand(M, N, K)
|
|
1205
|
+
>>> b = cp.random.rand(N, K, M)
|
|
1206
|
+
|
|
1207
|
+
Create an BinaryContraction object encapsulating the problem specification
|
|
1208
|
+
described earlier and use it as a context manager.
|
|
1209
|
+
|
|
1210
|
+
>>> with nvmath.tensor.BinaryContraction("ijk,jkl->il", a, b) as contraction:
|
|
1211
|
+
... contraction.plan()
|
|
1212
|
+
...
|
|
1213
|
+
... # Execute the operation to get the first result.
|
|
1214
|
+
... r1 = contraction.execute()
|
|
1215
|
+
...
|
|
1216
|
+
... # Update operands A and B in-place (see reset_operands() for an
|
|
1217
|
+
... # alternative).
|
|
1218
|
+
... a[:] = cp.random.rand(M, K)
|
|
1219
|
+
... b[:] = cp.random.rand(K, N)
|
|
1220
|
+
...
|
|
1221
|
+
... # Execute the operation to get the new result.
|
|
1222
|
+
... r2 = contraction.execute()
|
|
1223
|
+
|
|
1224
|
+
|
|
1225
|
+
All the resources used by the object are released at the end of the block.
|
|
1226
|
+
|
|
1227
|
+
Further examples can be found in the `nvmath/examples/tensor/contraction
|
|
1228
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/tensor/contraction>`_
|
|
1229
|
+
directory.
|
|
1230
|
+
"""
|
|
1231
|
+
|
|
1232
|
+
def __init__(
|
|
1233
|
+
self,
|
|
1234
|
+
expr,
|
|
1235
|
+
a,
|
|
1236
|
+
b,
|
|
1237
|
+
*,
|
|
1238
|
+
c=None,
|
|
1239
|
+
out=None,
|
|
1240
|
+
qualifiers=None,
|
|
1241
|
+
stream: utils.AnyStream | int | None = None,
|
|
1242
|
+
options: ContractionOptions | dict[str, Any] | None = None,
|
|
1243
|
+
execution: ExecutionCUDA | str | dict[str, Any] | None = None,
|
|
1244
|
+
):
|
|
1245
|
+
# Check here for a valid expr because it is a precondition for the constructor
|
|
1246
|
+
# of the base class where it is used to extract the number of operands.
|
|
1247
|
+
if not isinstance(expr, str) or expr.count(",") != 1:
|
|
1248
|
+
raise ValueError("Binary contraction requires a string with exactly 2 comma-separated operands")
|
|
1249
|
+
|
|
1250
|
+
super().__init__(expr, a, b, c=c, out=out, qualifiers=qualifiers, stream=stream, options=options, execution=execution)
|
|
1251
|
+
|
|
1252
|
+
def reset_operands(self, *, a=None, b=None, c=None, out=None, stream: utils.AnyStream | int | None = None):
|
|
1253
|
+
"""
|
|
1254
|
+
Reset one or more operands held by this :class:`BinaryContraction` instance.
|
|
1255
|
+
|
|
1256
|
+
.. versionchanged:: 0.9
|
|
1257
|
+
All parameters are now keyword-only.
|
|
1258
|
+
|
|
1259
|
+
Args:
|
|
1260
|
+
a: {a}
|
|
1261
|
+
|
|
1262
|
+
b: {b}
|
|
1263
|
+
|
|
1264
|
+
c: {addend}
|
|
1265
|
+
|
|
1266
|
+
out: {out}
|
|
1267
|
+
|
|
1268
|
+
stream: {stream}
|
|
1269
|
+
|
|
1270
|
+
{reset_operands_semantics}
|
|
1271
|
+
|
|
1272
|
+
Examples:
|
|
1273
|
+
|
|
1274
|
+
>>> import cupy as cp
|
|
1275
|
+
>>> import nvmath
|
|
1276
|
+
|
|
1277
|
+
Create two 3-D float64 ndarrays on the GPU:
|
|
1278
|
+
|
|
1279
|
+
>>> M, N, K = 128, 128, 256
|
|
1280
|
+
>>> a = cp.random.rand(M, K)
|
|
1281
|
+
>>> b = cp.random.rand(K, N)
|
|
1282
|
+
|
|
1283
|
+
Create an binary contraction object as a context manager
|
|
1284
|
+
|
|
1285
|
+
>>> with nvmath.tensor.BinaryContraction("ij,jk->ik", a, b) as contraction:
|
|
1286
|
+
... # Plan the operation.
|
|
1287
|
+
... contraction.plan()
|
|
1288
|
+
...
|
|
1289
|
+
... # Execute the contraction to get the first result.
|
|
1290
|
+
... r1 = contraction.execute()
|
|
1291
|
+
...
|
|
1292
|
+
... # Reset the operands to new CuPy ndarrays.
|
|
1293
|
+
... a1 = cp.random.rand(M, K)
|
|
1294
|
+
... b1 = cp.random.rand(K, N)
|
|
1295
|
+
... contraction.reset_operands(a=a1, b=b1)
|
|
1296
|
+
...
|
|
1297
|
+
... # Execute to get the new result corresponding to the updated operands.
|
|
1298
|
+
... r2 = contraction.execute()
|
|
1299
|
+
|
|
1300
|
+
With :meth:`reset_operands`, minimal overhead is achieved as problem
|
|
1301
|
+
specification and planning are only performed once.
|
|
1302
|
+
|
|
1303
|
+
For the particular example above, explicitly calling :meth:`reset_operands` is
|
|
1304
|
+
equivalent to updating the operands in-place, i.e, replacing
|
|
1305
|
+
``contraction.reset_operands(a=a1, b=b1)`` with ``a[:]=a1`` and ``b[:]=b1``.
|
|
1306
|
+
Note that updating the operand in-place should be adopted with caution as it can
|
|
1307
|
+
only yield the expected result under the additional constraint below:
|
|
1308
|
+
|
|
1309
|
+
- The operand is on the GPU (more precisely, the operand memory space should
|
|
1310
|
+
be accessible from the execution space).
|
|
1311
|
+
|
|
1312
|
+
For more details, please refer to `inplace update example
|
|
1313
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/tensor/contraction/example06_stateful_inplace.py>`_.
|
|
1314
|
+
|
|
1315
|
+
.. seealso::
|
|
1316
|
+
:meth:`reset_operands_unchecked`, :meth:`release_operands`
|
|
1317
|
+
"""
|
|
1318
|
+
super().reset_operands(a=a, b=b, c=c, out=out, stream=stream)
|
|
1319
|
+
|
|
1320
|
+
def reset_operands_unchecked(self, *, a=None, b=None, c=None, out=None, stream: utils.AnyStream | int | None = None):
|
|
1321
|
+
"""
|
|
1322
|
+
{reset_operands_unchecked}
|
|
1323
|
+
"""
|
|
1324
|
+
super()._reset_operands_unchecked(a=a, b=b, c=c, out=out, stream=stream)
|
|
1325
|
+
|
|
1326
|
+
def release_operands(self):
|
|
1327
|
+
"""
|
|
1328
|
+
{release_operands}
|
|
1329
|
+
"""
|
|
1330
|
+
self._release_operands()
|
|
1331
|
+
|
|
1332
|
+
|
|
1333
|
+
@utils.docstring_decorator(SHARED_CONTRACTION_DOCUMENTATION, skip_missing=False)
|
|
1334
|
+
class TernaryContraction(_ElementaryContraction):
|
|
1335
|
+
"""
|
|
1336
|
+
Create a stateful object encapsulating the specified ternary tensor contraction
|
|
1337
|
+
:math:`\\alpha a @ b @ c + \\beta d` and the required resources to perform the
|
|
1338
|
+
operation.
|
|
1339
|
+
A stateful object can be used to amortize the cost of preparation (planning in the
|
|
1340
|
+
case of ternary tensor contraction) across multiple executions (also see the
|
|
1341
|
+
:ref:`Stateful APIs<host api types>` section).
|
|
1342
|
+
|
|
1343
|
+
The function-form API :func:`ternary_contraction` is a convenient alternative to using
|
|
1344
|
+
stateful objects for *single* use (the user needs to perform just one tensor
|
|
1345
|
+
contraction, for example), in which case there is no possibility of amortizing
|
|
1346
|
+
preparatory costs. The function-form APIs are just convenience wrappers around
|
|
1347
|
+
the stateful object APIs.
|
|
1348
|
+
|
|
1349
|
+
Using the stateful object typically involves the following steps:
|
|
1350
|
+
|
|
1351
|
+
1. **Problem Specification**: Initialize the object with a defined operation and
|
|
1352
|
+
options.
|
|
1353
|
+
2. **Preparation**: Use :meth:`plan` to determine the best algorithmic implementation
|
|
1354
|
+
for this specific ternary tensor contraction operation.
|
|
1355
|
+
3. **Execution**: Perform the tensor contraction computation with :meth:`execute`.
|
|
1356
|
+
4. **Resource Management**: Ensure all resources are released either by explicitly
|
|
1357
|
+
calling :meth:`free` or by managing the stateful object within a context manager.
|
|
1358
|
+
|
|
1359
|
+
Detailed information on what's happening in the various phases described above can be
|
|
1360
|
+
obtained by passing in a :class:`logging.Logger` object to :class:`ContractionOptions`
|
|
1361
|
+
or by setting the appropriate options in the root logger object,
|
|
1362
|
+
which is used by default:
|
|
1363
|
+
|
|
1364
|
+
>>> import logging
|
|
1365
|
+
>>> logging.basicConfig(
|
|
1366
|
+
... level=logging.INFO,
|
|
1367
|
+
... format="%(asctime)s %(levelname)-8s %(message)s",
|
|
1368
|
+
... datefmt="%m-%d %H:%M:%S",
|
|
1369
|
+
... )
|
|
1370
|
+
|
|
1371
|
+
A user can select the desired logging level and, in general, take advantage of all of
|
|
1372
|
+
the functionality offered by the Python `logging` module.
|
|
1373
|
+
|
|
1374
|
+
Args:
|
|
1375
|
+
a: {a}
|
|
1376
|
+
|
|
1377
|
+
b: {b}
|
|
1378
|
+
|
|
1379
|
+
c: {c}
|
|
1380
|
+
|
|
1381
|
+
d: {addend}
|
|
1382
|
+
|
|
1383
|
+
out: {out}
|
|
1384
|
+
|
|
1385
|
+
qualifiers: {qualifiers}
|
|
1386
|
+
|
|
1387
|
+
stream: {stream}
|
|
1388
|
+
|
|
1389
|
+
options: {options}
|
|
1390
|
+
|
|
1391
|
+
execution: {execution}
|
|
1392
|
+
|
|
1393
|
+
.. seealso::
|
|
1394
|
+
:attr:`plan_preference`, :meth:`plan`, :meth:`reset_operands`,
|
|
1395
|
+
:meth:`release_operands`, :meth:`execute`
|
|
1396
|
+
|
|
1397
|
+
Examples:
|
|
1398
|
+
|
|
1399
|
+
>>> import numpy as np
|
|
1400
|
+
>>> import nvmath
|
|
1401
|
+
|
|
1402
|
+
Create three 3-D float64 ndarrays on the CPU:
|
|
1403
|
+
|
|
1404
|
+
>>> M, N, K = 32, 32, 32
|
|
1405
|
+
>>> a = np.random.rand(M, N, K)
|
|
1406
|
+
>>> b = np.random.rand(N, K, M)
|
|
1407
|
+
>>> c = np.random.rand(M, N)
|
|
1408
|
+
|
|
1409
|
+
We will define a ternary tensor contraction operation.
|
|
1410
|
+
|
|
1411
|
+
Create a TernaryContraction object encapsulating the problem specification above:
|
|
1412
|
+
|
|
1413
|
+
>>> expr = "ijk,jkl,ln->in"
|
|
1414
|
+
>>> contraction = nvmath.tensor.TernaryContraction(expr, a, b, c)
|
|
1415
|
+
|
|
1416
|
+
Options can be provided above to control the behavior of the operation using the
|
|
1417
|
+
`options` argument (see :class:`ContractionOptions`).
|
|
1418
|
+
|
|
1419
|
+
Next, plan the operation. Optionally, preferences can
|
|
1420
|
+
be specified for planning:
|
|
1421
|
+
|
|
1422
|
+
>>> contraction.plan()
|
|
1423
|
+
|
|
1424
|
+
Now execute the ternary tensor contraction, and obtain the result `r1` as
|
|
1425
|
+
a NumPy ndarray.
|
|
1426
|
+
|
|
1427
|
+
>>> r1 = contraction.execute()
|
|
1428
|
+
|
|
1429
|
+
Finally, free the object's resources. To avoid having to explicitly making this
|
|
1430
|
+
call, it's recommended to use the TernaryContraction object as a context manager
|
|
1431
|
+
as shown below, if possible.
|
|
1432
|
+
|
|
1433
|
+
>>> contraction.free()
|
|
1434
|
+
|
|
1435
|
+
Note that all :class:`TernaryContraction` methods execute on the current
|
|
1436
|
+
stream by default. Alternatively, the `stream` argument can be used to run a
|
|
1437
|
+
method on a specified stream.
|
|
1438
|
+
|
|
1439
|
+
Let's now look at the same problem with CuPy ndarrays on the GPU.
|
|
1440
|
+
|
|
1441
|
+
Create a 3-D float64 CuPy ndarray on the GPU:
|
|
1442
|
+
|
|
1443
|
+
>>> import cupy as cp
|
|
1444
|
+
>>> a = cp.random.rand(M, N, K)
|
|
1445
|
+
>>> b = cp.random.rand(N, K, M)
|
|
1446
|
+
>>> c = cp.random.rand(M, N)
|
|
1447
|
+
|
|
1448
|
+
Create an TernaryContraction object encapsulating the problem specification
|
|
1449
|
+
described earlier and use it as a context manager.
|
|
1450
|
+
|
|
1451
|
+
>>> expr = "ijk,jkl,ln->in"
|
|
1452
|
+
>>> with nvmath.tensor.TernaryContraction(expr, a, b, c) as contraction:
|
|
1453
|
+
... contraction.plan()
|
|
1454
|
+
...
|
|
1455
|
+
... # Execute the operation to get the first result.
|
|
1456
|
+
... r1 = contraction.execute()
|
|
1457
|
+
...
|
|
1458
|
+
... # Update operands A, B and C in-place (see reset_operands() for an
|
|
1459
|
+
... # alternative).
|
|
1460
|
+
... a[:] = cp.random.rand(M, N, K)
|
|
1461
|
+
... b[:] = cp.random.rand(N, K, M)
|
|
1462
|
+
... c[:] = cp.random.rand(M, N)
|
|
1463
|
+
...
|
|
1464
|
+
... # Execute the operation to get the new result.
|
|
1465
|
+
... r2 = contraction.execute()
|
|
1466
|
+
|
|
1467
|
+
|
|
1468
|
+
All the resources used by the object are released at the end of the block.
|
|
1469
|
+
|
|
1470
|
+
Further examples can be found in the `nvmath/examples/tensor/contraction
|
|
1471
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/tensor/contraction>`_
|
|
1472
|
+
directory.
|
|
1473
|
+
"""
|
|
1474
|
+
|
|
1475
|
+
def __init__(
|
|
1476
|
+
self,
|
|
1477
|
+
expr,
|
|
1478
|
+
a,
|
|
1479
|
+
b,
|
|
1480
|
+
c,
|
|
1481
|
+
*,
|
|
1482
|
+
d=None,
|
|
1483
|
+
out=None,
|
|
1484
|
+
qualifiers=None,
|
|
1485
|
+
stream: utils.AnyStream | int | None = None,
|
|
1486
|
+
options: ContractionOptions | dict[str, Any] | None = None,
|
|
1487
|
+
execution: ExecutionCUDA | str | dict[str, Any] | None = None,
|
|
1488
|
+
):
|
|
1489
|
+
# Check here for a valid expr because it is a precondition for the constructor
|
|
1490
|
+
# of the base class where it is used to extract the number of operands.
|
|
1491
|
+
if not isinstance(expr, str) or expr.count(",") != 2:
|
|
1492
|
+
raise ValueError("Ternary contraction requires a string with exactly 3 comma-separated operands")
|
|
1493
|
+
|
|
1494
|
+
super().__init__(
|
|
1495
|
+
expr, a, b, c=c, d=d, out=out, qualifiers=qualifiers, stream=stream, options=options, execution=execution
|
|
1496
|
+
)
|
|
1497
|
+
|
|
1498
|
+
def reset_operands(self, *, a=None, b=None, c=None, d=None, out=None, stream: utils.AnyStream | int | None = None):
|
|
1499
|
+
"""
|
|
1500
|
+
Reset one or more operands held by this :class:`TernaryContraction` instance.
|
|
1501
|
+
|
|
1502
|
+
.. versionchanged:: 0.9
|
|
1503
|
+
All parameters are now keyword-only.
|
|
1504
|
+
|
|
1505
|
+
Args:
|
|
1506
|
+
a: {a}
|
|
1507
|
+
|
|
1508
|
+
b: {b}
|
|
1509
|
+
|
|
1510
|
+
c: {c}
|
|
1511
|
+
|
|
1512
|
+
d: {addend}
|
|
1513
|
+
|
|
1514
|
+
out: {out}
|
|
1515
|
+
|
|
1516
|
+
stream: {stream}
|
|
1517
|
+
|
|
1518
|
+
{reset_operands_semantics}
|
|
1519
|
+
|
|
1520
|
+
Examples:
|
|
1521
|
+
|
|
1522
|
+
>>> import cupy as cp
|
|
1523
|
+
>>> import nvmath
|
|
1524
|
+
|
|
1525
|
+
Create two 3-D float64 ndarrays on the GPU:
|
|
1526
|
+
|
|
1527
|
+
>>> M, N, K = 12, 16, 32
|
|
1528
|
+
>>> a = cp.random.rand(M, M, N)
|
|
1529
|
+
>>> b = cp.random.rand(N, K)
|
|
1530
|
+
>>> c = cp.random.rand(K, K)
|
|
1531
|
+
|
|
1532
|
+
Create an ternary contraction object as a context manager
|
|
1533
|
+
|
|
1534
|
+
>>> expr = "ijk,kl,lm->ijm"
|
|
1535
|
+
>>> with nvmath.tensor.TernaryContraction(expr, a, b, c) as contraction:
|
|
1536
|
+
... # Plan the operation.
|
|
1537
|
+
... contraction.plan()
|
|
1538
|
+
...
|
|
1539
|
+
... # Execute the contraction to get the first result.
|
|
1540
|
+
... r1 = contraction.execute()
|
|
1541
|
+
...
|
|
1542
|
+
... # Reset the operands to new CuPy ndarrays.
|
|
1543
|
+
... a1 = cp.random.rand(M, M, N)
|
|
1544
|
+
... b1 = cp.random.rand(N, K)
|
|
1545
|
+
... c1 = cp.random.rand(K, K)
|
|
1546
|
+
... contraction.reset_operands(a=a1, b=b1, c=c1)
|
|
1547
|
+
...
|
|
1548
|
+
... # Execute to get the new result corresponding to the updated operands.
|
|
1549
|
+
... r2 = contraction.execute()
|
|
1550
|
+
|
|
1551
|
+
With :meth:`reset_operands`, minimal overhead is achieved as problem
|
|
1552
|
+
specification and planning are only performed once.
|
|
1553
|
+
|
|
1554
|
+
For the particular example above, explicitly calling :meth:`reset_operands`
|
|
1555
|
+
is equivalent to updating the operands in-place, i.e, replacing
|
|
1556
|
+
``contraction.reset_operands(a=a1, b=b1, c=c1)`` with ``a[:]=a1``
|
|
1557
|
+
and ``b[:]=b1`` and ``c[:]=c1``. Note that updating the operand in-place
|
|
1558
|
+
should be adopted with caution as it can only yield the expected result
|
|
1559
|
+
under the additional constraint below:
|
|
1560
|
+
|
|
1561
|
+
- The operand is on the GPU (more precisely, the operand memory space should
|
|
1562
|
+
be accessible from the execution space).
|
|
1563
|
+
|
|
1564
|
+
For more details, please refer to `inplace update example
|
|
1565
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/tensor/contraction/example06_stateful_inplace.py>`_.
|
|
1566
|
+
|
|
1567
|
+
.. seealso::
|
|
1568
|
+
:meth:`reset_operands_unchecked`, :meth:`release_operands`
|
|
1569
|
+
"""
|
|
1570
|
+
super().reset_operands(a=a, b=b, c=c, d=d, out=out, stream=stream)
|
|
1571
|
+
|
|
1572
|
+
def reset_operands_unchecked(
|
|
1573
|
+
self, *, a=None, b=None, c=None, d=None, out=None, stream: utils.AnyStream | int | None = None
|
|
1574
|
+
):
|
|
1575
|
+
"""
|
|
1576
|
+
{reset_operands_unchecked}
|
|
1577
|
+
"""
|
|
1578
|
+
super()._reset_operands_unchecked(a=a, b=b, c=c, d=d, out=out, stream=stream)
|
|
1579
|
+
|
|
1580
|
+
def release_operands(self):
|
|
1581
|
+
"""
|
|
1582
|
+
{release_operands}
|
|
1583
|
+
"""
|
|
1584
|
+
self._release_operands()
|
|
1585
|
+
|
|
1586
|
+
|
|
1587
|
+
@utils.docstring_decorator(SHARED_CONTRACTION_DOCUMENTATION, skip_missing=False)
|
|
1588
|
+
def binary_contraction(
|
|
1589
|
+
expr,
|
|
1590
|
+
a,
|
|
1591
|
+
b,
|
|
1592
|
+
*,
|
|
1593
|
+
c=None,
|
|
1594
|
+
alpha=1.0,
|
|
1595
|
+
beta=None,
|
|
1596
|
+
out=None,
|
|
1597
|
+
qualifiers=None,
|
|
1598
|
+
stream: utils.AnyStream | int | None = None,
|
|
1599
|
+
options: ContractionOptions | dict[str, Any] | None = None,
|
|
1600
|
+
execution: ExecutionCUDA | str | dict[str, Any] | None = None,
|
|
1601
|
+
):
|
|
1602
|
+
"""
|
|
1603
|
+
Evaluate the Einstein summation convention for binary contraction on the operands.
|
|
1604
|
+
|
|
1605
|
+
Explicit as well as implicit form is supported for the Einstein summation expression.
|
|
1606
|
+
|
|
1607
|
+
Additionally, the binary contraction can be performed with
|
|
1608
|
+
an additional operand, which is added to the result with a scale factor.
|
|
1609
|
+
|
|
1610
|
+
This function-form is a wrapper around the stateful
|
|
1611
|
+
:class:`BinaryContraction` object APIs and is meant for *single* use (the user needs
|
|
1612
|
+
to perform just one binary contraction, for example), in which case there is
|
|
1613
|
+
no possibility of amortizing preparatory costs.
|
|
1614
|
+
|
|
1615
|
+
Detailed information on what's happening within this function can be obtained by passing
|
|
1616
|
+
in a :class:`logging.Logger` object to :class:`ContractionOptions` or by setting the
|
|
1617
|
+
appropriate options in the root logger object, which is used by default:
|
|
1618
|
+
|
|
1619
|
+
>>> import logging
|
|
1620
|
+
>>> logging.basicConfig(
|
|
1621
|
+
... level=logging.INFO,
|
|
1622
|
+
... format="%(asctime)s %(levelname)-8s %(message)s",
|
|
1623
|
+
... datefmt="%m-%d %H:%M:%S",
|
|
1624
|
+
... )
|
|
1625
|
+
|
|
1626
|
+
A user can select the desired logging level and, in general, take advantage of all of
|
|
1627
|
+
the functionality offered by the Python `logging` module.
|
|
1628
|
+
|
|
1629
|
+
Args:
|
|
1630
|
+
expr: {expr}
|
|
1631
|
+
|
|
1632
|
+
a: {a}
|
|
1633
|
+
|
|
1634
|
+
b: {b}
|
|
1635
|
+
|
|
1636
|
+
c: {addend}
|
|
1637
|
+
|
|
1638
|
+
alpha: {alpha}
|
|
1639
|
+
|
|
1640
|
+
beta: {beta}
|
|
1641
|
+
|
|
1642
|
+
out: {out}
|
|
1643
|
+
|
|
1644
|
+
qualifiers: {qualifiers}
|
|
1645
|
+
|
|
1646
|
+
stream: {stream}
|
|
1647
|
+
|
|
1648
|
+
options: {options}
|
|
1649
|
+
|
|
1650
|
+
execution: {execution}
|
|
1651
|
+
|
|
1652
|
+
Returns:
|
|
1653
|
+
{result}
|
|
1654
|
+
|
|
1655
|
+
.. seealso::
|
|
1656
|
+
:class:`BinaryContraction`, :func:`ternary_contraction`,
|
|
1657
|
+
:class:`TernaryContraction`, :class:`ContractionOptions`,
|
|
1658
|
+
:class:`ContractionPlanPreference`
|
|
1659
|
+
|
|
1660
|
+
For tensor network contraction with arbitrary number of operands including
|
|
1661
|
+
contraction path finding, see cuQuantum:
|
|
1662
|
+
|
|
1663
|
+
- :external+cuquantum:py:func:`cuquantum.tensornet.contract`
|
|
1664
|
+
- :external+cuquantum:py:class:`cuquantum.tensornet.Network`
|
|
1665
|
+
|
|
1666
|
+
Examples:
|
|
1667
|
+
|
|
1668
|
+
>>> import cupy as cp
|
|
1669
|
+
>>> import nvmath
|
|
1670
|
+
|
|
1671
|
+
Create three float32 ndarrays on the GPU:
|
|
1672
|
+
|
|
1673
|
+
>>> M, N = 32, 64
|
|
1674
|
+
>>> a = cp.random.rand(M, M, N, N, dtype=cp.float32)
|
|
1675
|
+
>>> b = cp.random.rand(N, N, N, N, dtype=cp.float32)
|
|
1676
|
+
>>> c = cp.random.rand(M, M, N, N, dtype=cp.float32)
|
|
1677
|
+
|
|
1678
|
+
Perform the operation :math:`\\alpha \\sum a[i,j,k,l] * b[k,l,m,n] +
|
|
1679
|
+
\\beta c[i,j,m,n]` using :func:`binary_contraction`.
|
|
1680
|
+
The result `r` is also a CuPy float32 ndarray:
|
|
1681
|
+
|
|
1682
|
+
>>> r = nvmath.tensor.binary_contraction(
|
|
1683
|
+
... "ijkl,klmn->ijmn", a, b, c=c, alpha=1.23, beta=0.74
|
|
1684
|
+
... )
|
|
1685
|
+
|
|
1686
|
+
The result is equivalent to:
|
|
1687
|
+
|
|
1688
|
+
>>> r = 1.23 * cp.einsum("ijkl,klmn->ijmn", a, b) + 0.74 * c
|
|
1689
|
+
|
|
1690
|
+
Options can be provided to customize the operation:
|
|
1691
|
+
|
|
1692
|
+
>>> compute_type = nvmath.bindings.cutensor.ComputeDesc.COMPUTE_3XTF32()
|
|
1693
|
+
>>> o = nvmath.tensor.ContractionOptions(compute_type=compute_type)
|
|
1694
|
+
>>> r = nvmath.tensor.binary_contraction("ijkl,klmn->ijmn", a, b, options=o)
|
|
1695
|
+
|
|
1696
|
+
See `ContractionOptions` for the complete list of available options.
|
|
1697
|
+
|
|
1698
|
+
The package current stream is used by default, but a stream can be explicitly
|
|
1699
|
+
provided to the binary contraction operation. This can be done if the operands
|
|
1700
|
+
are computed on a different stream, for example:
|
|
1701
|
+
|
|
1702
|
+
>>> s = cp.cuda.Stream()
|
|
1703
|
+
>>> with s:
|
|
1704
|
+
... a = cp.random.rand(M, M, N, N)
|
|
1705
|
+
... b = cp.random.rand(N, N, N, N)
|
|
1706
|
+
>>> r = nvmath.tensor.binary_contraction("ijkl,klmn->ijmn", a, b, stream=s)
|
|
1707
|
+
|
|
1708
|
+
The operation above runs on stream `s` and is ordered with respect to the input
|
|
1709
|
+
computation.
|
|
1710
|
+
|
|
1711
|
+
Create NumPy ndarrays on the CPU.
|
|
1712
|
+
|
|
1713
|
+
>>> import numpy as np
|
|
1714
|
+
>>> a = np.random.rand(M, M, N, N)
|
|
1715
|
+
>>> b = np.random.rand(N, N, N, N)
|
|
1716
|
+
|
|
1717
|
+
Provide the NumPy ndarrays to :func:`binary_contraction`, with the result
|
|
1718
|
+
also being a NumPy ndarray:
|
|
1719
|
+
|
|
1720
|
+
>>> r = nvmath.tensor.binary_contraction("ijkl,klmn->ijmn", a, b)
|
|
1721
|
+
|
|
1722
|
+
Notes:
|
|
1723
|
+
- This function is a convenience wrapper around :class:`BinaryContraction` and is
|
|
1724
|
+
specifically meant for *single* use.
|
|
1725
|
+
|
|
1726
|
+
Further examples can be found in the `nvmath/examples/tensor/contraction
|
|
1727
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/tensor/contraction>`_
|
|
1728
|
+
directory.
|
|
1729
|
+
"""
|
|
1730
|
+
if c is None and beta is not None:
|
|
1731
|
+
raise ValueError("beta can only be set if c is specified in a binary contraction")
|
|
1732
|
+
elif c is not None and beta is None:
|
|
1733
|
+
raise ValueError("beta must be set when c is specified in a binary contraction")
|
|
1734
|
+
with BinaryContraction(
|
|
1735
|
+
expr, a, b, c=c, out=out, qualifiers=qualifiers, stream=stream, options=options, execution=execution
|
|
1736
|
+
) as contraction:
|
|
1737
|
+
contraction.plan(stream=stream)
|
|
1738
|
+
out = contraction.execute(alpha=alpha, beta=beta, stream=stream)
|
|
1739
|
+
return out
|
|
1740
|
+
|
|
1741
|
+
|
|
1742
|
+
@utils.docstring_decorator(SHARED_CONTRACTION_DOCUMENTATION, skip_missing=False)
|
|
1743
|
+
def ternary_contraction(
|
|
1744
|
+
expr,
|
|
1745
|
+
a,
|
|
1746
|
+
b,
|
|
1747
|
+
c,
|
|
1748
|
+
*,
|
|
1749
|
+
d=None,
|
|
1750
|
+
alpha=1.0,
|
|
1751
|
+
beta=None,
|
|
1752
|
+
out=None,
|
|
1753
|
+
qualifiers=None,
|
|
1754
|
+
stream: utils.AnyStream | int | None = None,
|
|
1755
|
+
options: ContractionOptions | dict[str, Any] | None = None,
|
|
1756
|
+
execution: ExecutionCUDA | str | dict[str, Any] | None = None,
|
|
1757
|
+
):
|
|
1758
|
+
"""
|
|
1759
|
+
Evaluate the Einstein summation convention for ternary contraction on the operands.
|
|
1760
|
+
|
|
1761
|
+
Explicit as well as implicit form is supported for the Einstein summation expression.
|
|
1762
|
+
|
|
1763
|
+
Additionally, the ternary contraction can be performed with
|
|
1764
|
+
an additional operand, which is added to the result with a scale factor.
|
|
1765
|
+
|
|
1766
|
+
This function-form is a wrapper around the stateful
|
|
1767
|
+
:class:`TernaryContraction` object APIs and is meant for *single* use (the user needs
|
|
1768
|
+
to perform just one ternary contraction, for example), in which case there is
|
|
1769
|
+
no possibility of amortizing preparatory costs.
|
|
1770
|
+
|
|
1771
|
+
Detailed information on what's happening within this function can be obtained by passing
|
|
1772
|
+
in a :class:`logging.Logger` object to :class:`ContractionOptions` or by setting the
|
|
1773
|
+
appropriate options in the root logger object, which is used by default:
|
|
1774
|
+
|
|
1775
|
+
>>> import logging
|
|
1776
|
+
>>> logging.basicConfig(
|
|
1777
|
+
... level=logging.INFO,
|
|
1778
|
+
... format="%(asctime)s %(levelname)-8s %(message)s",
|
|
1779
|
+
... datefmt="%m-%d %H:%M:%S",
|
|
1780
|
+
... )
|
|
1781
|
+
|
|
1782
|
+
A user can select the desired logging level and, in general, take advantage of all of
|
|
1783
|
+
the functionality offered by the Python `logging` module.
|
|
1784
|
+
|
|
1785
|
+
Args:
|
|
1786
|
+
expr: {expr}
|
|
1787
|
+
|
|
1788
|
+
a: {a}
|
|
1789
|
+
|
|
1790
|
+
b: {b}
|
|
1791
|
+
|
|
1792
|
+
c: {c}
|
|
1793
|
+
|
|
1794
|
+
d: {addend}
|
|
1795
|
+
|
|
1796
|
+
alpha: {alpha}
|
|
1797
|
+
|
|
1798
|
+
beta: {beta}
|
|
1799
|
+
|
|
1800
|
+
out: {out}
|
|
1801
|
+
|
|
1802
|
+
qualifiers: {qualifiers}
|
|
1803
|
+
|
|
1804
|
+
stream: {stream}
|
|
1805
|
+
|
|
1806
|
+
options: {options}
|
|
1807
|
+
|
|
1808
|
+
execution: {execution}
|
|
1809
|
+
|
|
1810
|
+
Returns:
|
|
1811
|
+
{result}
|
|
1812
|
+
|
|
1813
|
+
.. seealso::
|
|
1814
|
+
:class:`TernaryContraction`, :func:`binary_contraction`,
|
|
1815
|
+
:class:`BinaryContraction`, :class:`ContractionOptions`,
|
|
1816
|
+
:class:`ContractionPlanPreference`
|
|
1817
|
+
|
|
1818
|
+
For tensor network contraction with arbitrary number of operands including
|
|
1819
|
+
contraction path finding, see cuQuantum:
|
|
1820
|
+
|
|
1821
|
+
- :external+cuquantum:py:func:`cuquantum.tensornet.contract`
|
|
1822
|
+
- :external+cuquantum:py:class:`cuquantum.tensornet.Network`
|
|
1823
|
+
|
|
1824
|
+
Examples:
|
|
1825
|
+
|
|
1826
|
+
>>> import cupy as cp
|
|
1827
|
+
>>> import nvmath
|
|
1828
|
+
|
|
1829
|
+
Create three float32 ndarrays on the GPU:
|
|
1830
|
+
|
|
1831
|
+
>>> M, N, K = 16, 24, 32
|
|
1832
|
+
>>> a = cp.random.rand(M, M, dtype=cp.float32)
|
|
1833
|
+
>>> b = cp.random.rand(M, N, K, dtype=cp.float32)
|
|
1834
|
+
>>> c = cp.random.rand(N, K, M, dtype=cp.float32)
|
|
1835
|
+
>>> d = cp.random.rand(M, M, dtype=cp.float32)
|
|
1836
|
+
|
|
1837
|
+
Perform the operation :math:`\\alpha \\sum a[i,j] * b[j,k,l] * c[k,l,m] +
|
|
1838
|
+
\\beta d[i,m]` using :func:`ternary_contraction`.
|
|
1839
|
+
The result `r` is also a CuPy float32 ndarray:
|
|
1840
|
+
|
|
1841
|
+
>>> r = nvmath.tensor.ternary_contraction(
|
|
1842
|
+
... "ij,jkl,klm->im", a, b, c, d=d, alpha=0.63, beta=0.22
|
|
1843
|
+
... )
|
|
1844
|
+
|
|
1845
|
+
The result is equivalent to:
|
|
1846
|
+
|
|
1847
|
+
>>> r = 0.63 * cp.einsum("ij,jkl,klm->im", a, b, c) + 0.22 * d
|
|
1848
|
+
|
|
1849
|
+
Options can be provided to customize the operation:
|
|
1850
|
+
|
|
1851
|
+
>>> compute_type = nvmath.bindings.cutensor.ComputeDesc.COMPUTE_3XTF32()
|
|
1852
|
+
>>> o = nvmath.tensor.ContractionOptions(compute_type=compute_type)
|
|
1853
|
+
>>> r = nvmath.tensor.ternary_contraction("ij,jkl,klm->im", a, b, c, options=o)
|
|
1854
|
+
|
|
1855
|
+
See `ContractionOptions` for the complete list of available options.
|
|
1856
|
+
|
|
1857
|
+
The package current stream is used by default, but a stream can be explicitly
|
|
1858
|
+
provided to the ternary contraction operation. This can be done if the operands
|
|
1859
|
+
are computed on a different stream, for example:
|
|
1860
|
+
|
|
1861
|
+
>>> s = cp.cuda.Stream()
|
|
1862
|
+
>>> with s:
|
|
1863
|
+
... a = cp.random.rand(M, M, dtype=cp.float32)
|
|
1864
|
+
... b = cp.random.rand(M, N, K, dtype=cp.float32)
|
|
1865
|
+
... c = cp.random.rand(N, K, M, dtype=cp.float32)
|
|
1866
|
+
>>> r = nvmath.tensor.ternary_contraction("ij,jkl,klm->im", a, b, c, stream=s)
|
|
1867
|
+
|
|
1868
|
+
The operation above runs on stream `s` and is ordered with respect to the input
|
|
1869
|
+
computation.
|
|
1870
|
+
|
|
1871
|
+
Create NumPy ndarrays on the CPU.
|
|
1872
|
+
|
|
1873
|
+
>>> import numpy as np
|
|
1874
|
+
>>> a = np.random.rand(M, M)
|
|
1875
|
+
>>> b = np.random.rand(M, N, K)
|
|
1876
|
+
>>> c = np.random.rand(N, K, M)
|
|
1877
|
+
|
|
1878
|
+
Provide the NumPy ndarrays to :func:`ternary_contraction`, with the result
|
|
1879
|
+
also being a NumPy ndarray:
|
|
1880
|
+
|
|
1881
|
+
>>> r = nvmath.tensor.ternary_contraction("ij,jkl,klm->im", a, b, c)
|
|
1882
|
+
|
|
1883
|
+
Notes:
|
|
1884
|
+
- This function is a convenience wrapper around :class:`TernaryContraction` and is
|
|
1885
|
+
specifically meant for *single* use.
|
|
1886
|
+
|
|
1887
|
+
Further examples can be found in the `nvmath/examples/tensor/contraction
|
|
1888
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/tensor/contraction>`_
|
|
1889
|
+
directory.
|
|
1890
|
+
"""
|
|
1891
|
+
if d is None and beta is not None:
|
|
1892
|
+
raise ValueError("beta can only be set if d is specified in a ternary contraction")
|
|
1893
|
+
elif d is not None and beta is None:
|
|
1894
|
+
raise ValueError("beta must be set when d is specified in a ternary contraction")
|
|
1895
|
+
with TernaryContraction(
|
|
1896
|
+
expr, a, b, c, d=d, out=out, qualifiers=qualifiers, stream=stream, options=options, execution=execution
|
|
1897
|
+
) as contraction:
|
|
1898
|
+
contraction.plan(stream=stream)
|
|
1899
|
+
out = contraction.execute(alpha=alpha, beta=beta, stream=stream)
|
|
1900
|
+
return out
|