nvmath-python 1.0.0__cp314-cp314t-win_amd64.whl

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