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