nvmath-python 0.9.0__cp314-cp314-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. nvmath/__init__.pxd +0 -0
  2. nvmath/__init__.py +45 -0
  3. nvmath/_internal/__init__.py +0 -0
  4. nvmath/_internal/attribute_ifc_factory.py +330 -0
  5. nvmath/_internal/layout.py +70 -0
  6. nvmath/_internal/templates.py +383 -0
  7. nvmath/_utils.py +150 -0
  8. nvmath/bindings/__init__.py +51 -0
  9. nvmath/bindings/_internal/__init__.pxd +0 -0
  10. nvmath/bindings/_internal/__init__.py +0 -0
  11. nvmath/bindings/_internal/cublas.cp314-win_amd64.pyd +0 -0
  12. nvmath/bindings/_internal/cublas.pxd +530 -0
  13. nvmath/bindings/_internal/cublasLt.cp314-win_amd64.pyd +0 -0
  14. nvmath/bindings/_internal/cublasLt.pxd +59 -0
  15. nvmath/bindings/_internal/cublasMp.pxd +52 -0
  16. nvmath/bindings/_internal/cudss.cp314-win_amd64.pyd +0 -0
  17. nvmath/bindings/_internal/cudss.pxd +47 -0
  18. nvmath/bindings/_internal/cufft.cp314-win_amd64.pyd +0 -0
  19. nvmath/bindings/_internal/cufft.pxd +70 -0
  20. nvmath/bindings/_internal/cufftMp.pxd +77 -0
  21. nvmath/bindings/_internal/curand.cp314-win_amd64.pyd +0 -0
  22. nvmath/bindings/_internal/curand.pxd +42 -0
  23. nvmath/bindings/_internal/cusolver.cp314-win_amd64.pyd +0 -0
  24. nvmath/bindings/_internal/cusolver.pxd +15 -0
  25. nvmath/bindings/_internal/cusolverDn.cp314-win_amd64.pyd +0 -0
  26. nvmath/bindings/_internal/cusolverDn.pxd +406 -0
  27. nvmath/bindings/_internal/cusparse.cp314-win_amd64.pyd +0 -0
  28. nvmath/bindings/_internal/cusparse.pxd +277 -0
  29. nvmath/bindings/_internal/cusparseLt.cp314-win_amd64.pyd +0 -0
  30. nvmath/bindings/_internal/cusparseLt.pxd +48 -0
  31. nvmath/bindings/_internal/cutensor.cp314-win_amd64.pyd +0 -0
  32. nvmath/bindings/_internal/cutensor.pxd +58 -0
  33. nvmath/bindings/_internal/mathdx.cp314-win_amd64.pyd +0 -0
  34. nvmath/bindings/_internal/mathdx.pxd +116 -0
  35. nvmath/bindings/_internal/nvshmem.pxd +29 -0
  36. nvmath/bindings/_internal/utils.cp314-win_amd64.pyd +0 -0
  37. nvmath/bindings/_internal/utils.pxd +179 -0
  38. nvmath/bindings/_internal/utils.pyi +10 -0
  39. nvmath/bindings/cublas.cp314-win_amd64.pyd +0 -0
  40. nvmath/bindings/cublas.pxd +558 -0
  41. nvmath/bindings/cublas.pyi +746 -0
  42. nvmath/bindings/cublasLt.cp314-win_amd64.pyd +0 -0
  43. nvmath/bindings/cublasLt.pxd +109 -0
  44. nvmath/bindings/cublasLt.pyi +1337 -0
  45. nvmath/bindings/cublasMp.pxd +85 -0
  46. nvmath/bindings/cublasMp.pyi +219 -0
  47. nvmath/bindings/cudss.cp314-win_amd64.pyd +0 -0
  48. nvmath/bindings/cudss.pxd +86 -0
  49. nvmath/bindings/cudss.pyi +282 -0
  50. nvmath/bindings/cufft.cp314-win_amd64.pyd +0 -0
  51. nvmath/bindings/cufft.pxd +118 -0
  52. nvmath/bindings/cufft.pyi +241 -0
  53. nvmath/bindings/cufftMp.pxd +124 -0
  54. nvmath/bindings/cufftMp.pyi +260 -0
  55. nvmath/bindings/curand.cp314-win_amd64.pyd +0 -0
  56. nvmath/bindings/curand.pxd +71 -0
  57. nvmath/bindings/curand.pyi +159 -0
  58. nvmath/bindings/cusolver.cp314-win_amd64.pyd +0 -0
  59. nvmath/bindings/cusolver.pxd +62 -0
  60. nvmath/bindings/cusolver.pyi +242 -0
  61. nvmath/bindings/cusolverDn.cp314-win_amd64.pyd +0 -0
  62. nvmath/bindings/cusolverDn.pxd +430 -0
  63. nvmath/bindings/cusolverDn.pyi +416 -0
  64. nvmath/bindings/cusparse.cp314-win_amd64.pyd +0 -0
  65. nvmath/bindings/cusparse.pxd +338 -0
  66. nvmath/bindings/cusparse.pyi +659 -0
  67. nvmath/bindings/cusparseLt.cp314-win_amd64.pyd +0 -0
  68. nvmath/bindings/cusparseLt.pxd +99 -0
  69. nvmath/bindings/cusparseLt.pyi +198 -0
  70. nvmath/bindings/cutensor.cp314-win_amd64.pyd +0 -0
  71. nvmath/bindings/cutensor.pxd +98 -0
  72. nvmath/bindings/cutensor.pyi +294 -0
  73. nvmath/bindings/cycublas.cp314-win_amd64.pyd +0 -0
  74. nvmath/bindings/cycublas.pxd +684 -0
  75. nvmath/bindings/cycublasLt.cp314-win_amd64.pyd +0 -0
  76. nvmath/bindings/cycublasLt.pxd +1055 -0
  77. nvmath/bindings/cycublasMp.pxd +183 -0
  78. nvmath/bindings/cycudss.cp314-win_amd64.pyd +0 -0
  79. nvmath/bindings/cycudss.pxd +224 -0
  80. nvmath/bindings/cycufft.cp314-win_amd64.pyd +0 -0
  81. nvmath/bindings/cycufft.pxd +326 -0
  82. nvmath/bindings/cycufftMp.pxd +334 -0
  83. nvmath/bindings/cycurand.cp314-win_amd64.pyd +0 -0
  84. nvmath/bindings/cycurand.pxd +146 -0
  85. nvmath/bindings/cycusolver.cp314-win_amd64.pyd +0 -0
  86. nvmath/bindings/cycusolver.pxd +154 -0
  87. nvmath/bindings/cycusolverDn.cp314-win_amd64.pyd +0 -0
  88. nvmath/bindings/cycusolverDn.pxd +446 -0
  89. nvmath/bindings/cycusparse.cp314-win_amd64.pyd +0 -0
  90. nvmath/bindings/cycusparse.pxd +470 -0
  91. nvmath/bindings/cycusparseLt.cp314-win_amd64.pyd +0 -0
  92. nvmath/bindings/cycusparseLt.pxd +150 -0
  93. nvmath/bindings/cycutensor.cp314-win_amd64.pyd +0 -0
  94. nvmath/bindings/cycutensor.pxd +192 -0
  95. nvmath/bindings/cymathdx.cp314-win_amd64.pyd +0 -0
  96. nvmath/bindings/cymathdx.pxd +552 -0
  97. nvmath/bindings/cynvshmem.pxd +126 -0
  98. nvmath/bindings/mathdx.cp314-win_amd64.pyd +0 -0
  99. nvmath/bindings/mathdx.pxd +182 -0
  100. nvmath/bindings/mathdx.pyi +1244 -0
  101. nvmath/bindings/nvpl/__init__.pxd +0 -0
  102. nvmath/bindings/nvpl/__init__.py +13 -0
  103. nvmath/bindings/nvpl/_internal/__init__.pxd +0 -0
  104. nvmath/bindings/nvpl/_internal/__init__.py +0 -0
  105. nvmath/bindings/nvpl/_internal/blas.cp314-win_amd64.pyd +0 -0
  106. nvmath/bindings/nvpl/_internal/blas.pxd +125 -0
  107. nvmath/bindings/nvpl/_internal/fft.cp314-win_amd64.pyd +0 -0
  108. nvmath/bindings/nvpl/_internal/fft.pxd +36 -0
  109. nvmath/bindings/nvpl/blas.cp314-win_amd64.pyd +0 -0
  110. nvmath/bindings/nvpl/blas.pxd +85 -0
  111. nvmath/bindings/nvpl/blas.pyi +122 -0
  112. nvmath/bindings/nvpl/cyblas.cp314-win_amd64.pyd +0 -0
  113. nvmath/bindings/nvpl/cyblas.pxd +166 -0
  114. nvmath/bindings/nvpl/cyfft.cp314-win_amd64.pyd +0 -0
  115. nvmath/bindings/nvpl/cyfft.pxd +92 -0
  116. nvmath/bindings/nvpl/fft.cp314-win_amd64.pyd +0 -0
  117. nvmath/bindings/nvpl/fft.pxd +100 -0
  118. nvmath/bindings/nvpl/fft.pyi +100 -0
  119. nvmath/bindings/nvshmem.pxd +54 -0
  120. nvmath/bindings/nvshmem.pyi +179 -0
  121. nvmath/device/__init__.py +21 -0
  122. nvmath/device/_deprecated.py +33 -0
  123. nvmath/device/common.py +313 -0
  124. nvmath/device/common_backend.py +131 -0
  125. nvmath/device/common_cuda.py +201 -0
  126. nvmath/device/common_numba.py +310 -0
  127. nvmath/device/common_opaque_tensor.py +201 -0
  128. nvmath/device/cublasdx.py +1805 -0
  129. nvmath/device/cublasdx_backend.py +807 -0
  130. nvmath/device/cublasdx_numba.py +1612 -0
  131. nvmath/device/cufftdx.py +510 -0
  132. nvmath/device/cufftdx_backend.py +196 -0
  133. nvmath/device/cufftdx_numba.py +196 -0
  134. nvmath/device/curand_kernel.py +9147 -0
  135. nvmath/device/cusolverdx.py +2690 -0
  136. nvmath/device/cusolverdx_backend.py +440 -0
  137. nvmath/device/cusolverdx_numba.py +1624 -0
  138. nvmath/device/llvm_array.py +29 -0
  139. nvmath/device/random.py +445 -0
  140. nvmath/device/random_helpers.py +23 -0
  141. nvmath/device/random_states.py +187 -0
  142. nvmath/device/types.py +147 -0
  143. nvmath/device/vector_types_numba.py +203 -0
  144. nvmath/distributed/__init__.py +205 -0
  145. nvmath/distributed/_internal/__init__.py +0 -0
  146. nvmath/distributed/_internal/nvshmem.py +302 -0
  147. nvmath/distributed/_internal/tensor_ifc.py +67 -0
  148. nvmath/distributed/_internal/tensor_ifc_cupy.py +62 -0
  149. nvmath/distributed/_internal/tensor_ifc_host_device.py +165 -0
  150. nvmath/distributed/_internal/tensor_ifc_numpy.py +41 -0
  151. nvmath/distributed/_internal/tensor_ifc_torch.py +141 -0
  152. nvmath/distributed/_internal/tensor_wrapper.py +78 -0
  153. nvmath/distributed/_utils.py +167 -0
  154. nvmath/distributed/distribution.py +770 -0
  155. nvmath/distributed/fft/__init__.py +7 -0
  156. nvmath/distributed/fft/_configuration.py +79 -0
  157. nvmath/distributed/fft/fft.py +2801 -0
  158. nvmath/distributed/linalg/__init__.py +12 -0
  159. nvmath/distributed/linalg/_internal/__init__.py +3 -0
  160. nvmath/distributed/linalg/_internal/epilog_protocol.py +496 -0
  161. nvmath/distributed/linalg/_internal/matmul_desc_ifc.py +28 -0
  162. nvmath/distributed/linalg/advanced/__init__.py +8 -0
  163. nvmath/distributed/linalg/advanced/_configuration.py +166 -0
  164. nvmath/distributed/linalg/advanced/matmulmod.py +2908 -0
  165. nvmath/distributed/process_group.py +408 -0
  166. nvmath/distributed/reshape/__init__.py +6 -0
  167. nvmath/distributed/reshape/_configuration.py +39 -0
  168. nvmath/distributed/reshape/reshape.py +1256 -0
  169. nvmath/fft/__init__.py +7 -0
  170. nvmath/fft/_configuration.py +208 -0
  171. nvmath/fft/_exec_utils.py +109 -0
  172. nvmath/fft/_helpers.py +237 -0
  173. nvmath/fft/fft.py +2903 -0
  174. nvmath/internal/__init__.pxd +3 -0
  175. nvmath/internal/__init__.py +10 -0
  176. nvmath/internal/_bindings.cp314-win_amd64.pyd +0 -0
  177. nvmath/internal/_bindings.pxd +12 -0
  178. nvmath/internal/_device_utils.py +54 -0
  179. nvmath/internal/enum_utils.py +142 -0
  180. nvmath/internal/formatters.py +87 -0
  181. nvmath/internal/mem_limit.py +51 -0
  182. nvmath/internal/memory.cp314-win_amd64.pyd +0 -0
  183. nvmath/internal/memory.pxd +10 -0
  184. nvmath/internal/memory.pyi +46 -0
  185. nvmath/internal/ndbuffer/__init__.pxd +3 -0
  186. nvmath/internal/ndbuffer/__init__.py +7 -0
  187. nvmath/internal/ndbuffer/copy_kernel/args.h +34 -0
  188. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/array_view.h +52 -0
  189. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/elementwise.h +68 -0
  190. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/grid_indexer.h +69 -0
  191. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/transposed.h +240 -0
  192. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/type_utils.h +39 -0
  193. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/utils.h +132 -0
  194. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/vec.h +159 -0
  195. nvmath/internal/ndbuffer/copy_kernel/elementwise.h +53 -0
  196. nvmath/internal/ndbuffer/copy_kernel/transposed.h +58 -0
  197. nvmath/internal/ndbuffer/copy_kernel.cp314-win_amd64.pyd +0 -0
  198. nvmath/internal/ndbuffer/copy_kernel.pxd +9 -0
  199. nvmath/internal/ndbuffer/copy_kernel.pyi +9 -0
  200. nvmath/internal/ndbuffer/data_layout.cp314-win_amd64.pyd +0 -0
  201. nvmath/internal/ndbuffer/data_layout.pxd +72 -0
  202. nvmath/internal/ndbuffer/data_layout.pyi +23 -0
  203. nvmath/internal/ndbuffer/jit.cp314-win_amd64.pyd +0 -0
  204. nvmath/internal/ndbuffer/jit.pxd +7 -0
  205. nvmath/internal/ndbuffer/jit.pyi +6 -0
  206. nvmath/internal/ndbuffer/ndbuffer.cp314-win_amd64.pyd +0 -0
  207. nvmath/internal/ndbuffer/ndbuffer.pxd +36 -0
  208. nvmath/internal/ndbuffer/ndbuffer.pyi +41 -0
  209. nvmath/internal/ndbuffer/package_utils.cp314-win_amd64.pyd +0 -0
  210. nvmath/internal/ndbuffer/package_utils.pxd +11 -0
  211. nvmath/internal/ndbuffer/package_utils.pyi +12 -0
  212. nvmath/internal/package_ifc.py +147 -0
  213. nvmath/internal/package_ifc_cuda.py +49 -0
  214. nvmath/internal/package_ifc_cupy.py +49 -0
  215. nvmath/internal/package_ifc_torch.py +31 -0
  216. nvmath/internal/package_wrapper.py +14 -0
  217. nvmath/internal/tensor_ifc.py +194 -0
  218. nvmath/internal/tensor_ifc_cupy.py +241 -0
  219. nvmath/internal/tensor_ifc_ndbuffer.py +153 -0
  220. nvmath/internal/tensor_ifc_numpy.py +183 -0
  221. nvmath/internal/tensor_ifc_torch.py +176 -0
  222. nvmath/internal/tensor_wrapper.py +157 -0
  223. nvmath/internal/typemaps.py +162 -0
  224. nvmath/internal/utils.py +771 -0
  225. nvmath/linalg/__init__.py +46 -0
  226. nvmath/linalg/_internal/__init__.py +3 -0
  227. nvmath/linalg/_internal/algo_cap_ifc.py +82 -0
  228. nvmath/linalg/_internal/algo_config_ifc.py +43 -0
  229. nvmath/linalg/_internal/batch.py +217 -0
  230. nvmath/linalg/_internal/enum_to_tuples.py +64 -0
  231. nvmath/linalg/_internal/epilog_protocol.py +766 -0
  232. nvmath/linalg/_internal/layout.py +624 -0
  233. nvmath/linalg/_internal/matmul_desc_ifc.py +28 -0
  234. nvmath/linalg/_internal/matmul_pref_ifc.py +27 -0
  235. nvmath/linalg/_internal/matrix_layout_ifc.py +26 -0
  236. nvmath/linalg/_internal/typemaps.py +144 -0
  237. nvmath/linalg/_internal/utils.py +134 -0
  238. nvmath/linalg/advanced/__init__.py +8 -0
  239. nvmath/linalg/advanced/_algorithmmod.py +149 -0
  240. nvmath/linalg/advanced/_configuration.py +349 -0
  241. nvmath/linalg/advanced/helpers/__init__.py +5 -0
  242. nvmath/linalg/advanced/helpers/matmul.py +1348 -0
  243. nvmath/linalg/advanced/matmulmod.py +3937 -0
  244. nvmath/linalg/generic/__init__.py +43 -0
  245. nvmath/linalg/generic/_configuration/__init__.py +37 -0
  246. nvmath/linalg/generic/_configuration/layout.py +263 -0
  247. nvmath/linalg/generic/_configuration/match.py +609 -0
  248. nvmath/linalg/generic/_configuration/qualifiers.py +494 -0
  249. nvmath/linalg/generic/_configuration/wrap.py +217 -0
  250. nvmath/linalg/generic/_dtype.py +15 -0
  251. nvmath/linalg/generic/matmulmod.py +1060 -0
  252. nvmath/memory.py +282 -0
  253. nvmath/sparse/__init__.py +38 -0
  254. nvmath/sparse/_internal/__init__.py +21 -0
  255. nvmath/sparse/_internal/common_utils.py +144 -0
  256. nvmath/sparse/_internal/cudss_config_ifc.py +741 -0
  257. nvmath/sparse/_internal/cudss_data_ifc.py +399 -0
  258. nvmath/sparse/_internal/cudss_utils.py +445 -0
  259. nvmath/sparse/_internal/cusparse_utils.py +382 -0
  260. nvmath/sparse/_internal/sparse_bsc_ifc.py +300 -0
  261. nvmath/sparse/_internal/sparse_bsr_ifc.py +303 -0
  262. nvmath/sparse/_internal/sparse_coo_ifc.py +254 -0
  263. nvmath/sparse/_internal/sparse_csc_ifc.py +266 -0
  264. nvmath/sparse/_internal/sparse_csr_ifc.py +266 -0
  265. nvmath/sparse/_internal/sparse_dia_ifc.py +239 -0
  266. nvmath/sparse/_internal/sparse_format_helpers.py +601 -0
  267. nvmath/sparse/_internal/sparse_tensor_ifc.py +121 -0
  268. nvmath/sparse/_internal/sparse_ust_ifc.py +146 -0
  269. nvmath/sparse/_internal/utils.py +56 -0
  270. nvmath/sparse/advanced/__init__.py +7 -0
  271. nvmath/sparse/advanced/_configuration.py +230 -0
  272. nvmath/sparse/advanced/direct_solver.py +1884 -0
  273. nvmath/sparse/generic/__init__.py +7 -0
  274. nvmath/sparse/generic/_configuration.py +129 -0
  275. nvmath/sparse/generic/_helpers.py +137 -0
  276. nvmath/sparse/generic/_thunks.py +21 -0
  277. nvmath/sparse/generic/matmulmod.py +2426 -0
  278. nvmath/sparse/ust/__init__.py +7 -0
  279. nvmath/sparse/ust/_converters.py +378 -0
  280. nvmath/sparse/ust/_drawer.py +552 -0
  281. nvmath/sparse/ust/_emitter.py +553 -0
  282. nvmath/sparse/ust/_jit.py +212 -0
  283. nvmath/sparse/ust/_kernel.py +346 -0
  284. nvmath/sparse/ust/_semiring.py +71 -0
  285. nvmath/sparse/ust/_utils.py +102 -0
  286. nvmath/sparse/ust/interfaces/__init__.py +0 -0
  287. nvmath/sparse/ust/interfaces/torch_interface.py +476 -0
  288. nvmath/sparse/ust/semiring_ops.py +71 -0
  289. nvmath/sparse/ust/tensor.py +1027 -0
  290. nvmath/sparse/ust/tensor_drawer.py +552 -0
  291. nvmath/sparse/ust/tensor_emitter.py +558 -0
  292. nvmath/sparse/ust/tensor_format.py +935 -0
  293. nvmath/sparse/ust/tensor_jit.py +212 -0
  294. nvmath/sparse/ust/tensor_kernel.py +348 -0
  295. nvmath/sparse/ust/tensor_utils.py +380 -0
  296. nvmath/tensor/__init__.py +6 -0
  297. nvmath/tensor/_configuration.py +95 -0
  298. nvmath/tensor/_internal/__init__.py +3 -0
  299. nvmath/tensor/_internal/cutensor_config_ifc.py +247 -0
  300. nvmath/tensor/_internal/cutensor_utils.py +162 -0
  301. nvmath/tensor/_internal/data.py +43 -0
  302. nvmath/tensor/_internal/einsum_parser.py +444 -0
  303. nvmath/tensor/_internal/typemaps.py +96 -0
  304. nvmath/tensor/contract.py +1861 -0
  305. nvmath_python-0.9.0.dist-info/METADATA +127 -0
  306. nvmath_python-0.9.0.dist-info/RECORD +309 -0
  307. nvmath_python-0.9.0.dist-info/WHEEL +5 -0
  308. nvmath_python-0.9.0.dist-info/licenses/LICENSE +177 -0
  309. nvmath_python-0.9.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,2426 @@
1
+ # Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ __all__ = ["Matmul", "matmul"]
6
+
7
+ import functools
8
+ import logging
9
+ import operator
10
+ import re
11
+ from collections.abc import Mapping, Sequence
12
+ from dataclasses import dataclass
13
+ from enum import IntEnum
14
+
15
+ try:
16
+ import cuda.core as cc
17
+ except ImportError:
18
+ import cuda.core.experimental as cc
19
+
20
+ import numpy as np
21
+
22
+ from nvmath import memory
23
+ from nvmath._internal.layout import check_monotonic_strides, is_contiguous_in_memory
24
+ from nvmath.bindings import cusparse
25
+ from nvmath.internal import formatters, tensor_wrapper, typemaps, utils
26
+ from nvmath.sparse._internal import common_utils as sp_utils
27
+ from nvmath.sparse._internal import cusparse_utils
28
+ from nvmath.sparse._internal.sparse_ust_ifc import USTensorHolder
29
+ from nvmath.sparse._internal.utils import calculate_strides
30
+ from nvmath.sparse.ust._kernel import KernelGen
31
+ from nvmath.sparse.ust.tensor import Tensor as UST
32
+
33
+ from ._configuration import ComputeType, ExecutionCUDA, MatmulOptions, matrix_qualifiers_dtype
34
+ from ._helpers import compile_prolog
35
+ from ._thunks import default_prolog
36
+
37
+ # TODO: make sure KernelGen is thread-safe.
38
+ KERNEL_CACHE = KernelGen(False)
39
+
40
+
41
+ CUSPARSE_FORMATS = ["CSR", "CSC", "BSR", "BSC", "COO"]
42
+
43
+ CODEGEN_FORMATS = ["UST", "DIA"] + CUSPARSE_FORMATS
44
+
45
+ # Supported index and data types.
46
+
47
+ CUSPARSE_INDEX_TYPES = {"int32", "int64"}
48
+
49
+ CODEGEN_INDEX_TYPES = {"int32", "int64"}
50
+
51
+ VALID_INDEX_TYPES = CUSPARSE_INDEX_TYPES | CODEGEN_INDEX_TYPES
52
+
53
+ CUSPARSE_DTYPES = {"float16", "bfloat16", "float32", "float64", "complex32", "complex64", "complex128"}
54
+
55
+ CODEGEN_DTYPES = {"float16", "bfloat16", "float32", "float64", "complex64", "complex128"}
56
+
57
+ VALID_DTYPES = CUSPARSE_DTYPES | CODEGEN_DTYPES
58
+
59
+ CUSPARSE_COMPUTE_TYPES = {"float32", "float64", "complex64", "complex128"}
60
+
61
+ CODEGEN_COMPUTE_TYPES = {"float32", "float64", "complex64", "complex128"}
62
+
63
+ VALID_COMPUTE_TYPES = CUSPARSE_COMPUTE_TYPES | CODEGEN_COMPUTE_TYPES
64
+
65
+
66
+ # TODO:
67
+ # For the dense tensors forming the sparse tensor representation, check that they are
68
+ # dense and contiguous in C-order.
69
+
70
+
71
+ class BackendSupport(IntEnum):
72
+ """An IntEnum class for capturing dispatch and codegen support for the MM."""
73
+
74
+ NOT_SUPPORTED = 0
75
+ PROVISIONALLY_SUPPORTED = 1
76
+ SUPPORTED = 2
77
+
78
+
79
+ class Api(IntEnum):
80
+ """An IntEnum class for capturing the codegen or dispatch API."""
81
+
82
+ NONE = 0
83
+ CODEGEN = 1
84
+ MM = 2
85
+ MM_OP = 3
86
+
87
+
88
+ class InvalidMatmulState(Exception):
89
+ pass
90
+
91
+
92
+ # TODO: generic (semantic) compatibility check.
93
+ # dispatch or codegen specific support checks (dtype, compute type, batching,
94
+ # num dense dim) etc.
95
+
96
+
97
+ def _get_cusparse_op_code(qualifier):
98
+ if qualifier["is_transpose"] and qualifier["is_conjugate"]:
99
+ return cusparse.Operation.CONJUGATE_TRANSPOSE
100
+
101
+ if qualifier["is_transpose"]:
102
+ return cusparse.Operation.TRANSPOSE
103
+
104
+ assert not qualifier["is_conjugate"], "Internal error."
105
+
106
+ return cusparse.Operation.NON_TRANSPOSE
107
+
108
+
109
+ def _is_regular_precision(dtype_name):
110
+ e = r"(complex|b?float)(\d+)"
111
+ m = re.search(e, dtype_name)
112
+ assert m is not None, "Internal error."
113
+ size = int(m.group(2))
114
+ is_complex = m.group(1) == "complex"
115
+ if is_complex:
116
+ size //= 2
117
+ return size >= 32
118
+
119
+
120
+ # Require that conjugate and transpose be specified together.
121
+ @dataclass
122
+ class DenseLayoutTraits:
123
+ """An internal data class for capturing dense matrix traits."""
124
+
125
+ order: cusparse.Order
126
+ ld: int
127
+ mm_shape: Sequence[int]
128
+ batch_shape: Sequence[int]
129
+ batch_count: int
130
+ batch_offset: int # Based on strides
131
+
132
+
133
+ @dataclass
134
+ class SparseLayoutTraits:
135
+ """An internal data class for capturing dense matrix traits."""
136
+
137
+ sparse_format: str
138
+ mm_shape: Sequence[int]
139
+ batch_count: int
140
+ batch_broadcast: bool = False
141
+
142
+
143
+ @dataclass
144
+ class SpMMTraits:
145
+ """An internal data class for capturing the matrix multiplication traits."""
146
+
147
+ M: int
148
+ N: int
149
+ K: int
150
+ a_layout_traits: SparseLayoutTraits
151
+ b_layout_traits: DenseLayoutTraits
152
+ c_layout_traits: DenseLayoutTraits
153
+ batch_count: int
154
+ batch_shape: Sequence[int]
155
+
156
+
157
+ def get_dense_matrix_layout_traits(
158
+ mm_shape: Sequence[int],
159
+ mm_strides: Sequence[int],
160
+ batch_strides: Sequence[int],
161
+ ordering: cusparse.Order | None = None,
162
+ orientation: cusparse.Order | None = None,
163
+ ) -> tuple[cusparse.Order, int, int]:
164
+ """
165
+ The 'ordering' option specifies the layout order, if it's not None, as in the case of
166
+ the D matrix whose layout is determined by the other operands' layout. It is required if
167
+ the matrix is degenerate (a vector or scalar: len(mm_shape) < 2).
168
+
169
+ The 'orientation' option (ROW or COL) is required to infer the correct leading dimension
170
+ for degenerate matrices (a vector or scalar: len(mm_shape) < 2).
171
+ """
172
+ if len(mm_shape) < 2: # The result D can be a scalar or vector.
173
+ assert ordering is not None, "Internal Error: 'ordering' must be specified for degenerate matrices."
174
+ assert orientation is not None, "Internal Error: 'orientation' must be specified for degenerate matrices."
175
+ batch_offset = min(batch_strides) if batch_strides else 0
176
+ order = ordering
177
+ if len(mm_shape) < 1:
178
+ ld = 1
179
+ elif order != orientation:
180
+ # For a ROW vector in COL order, the LD should be 1 since we promote the row
181
+ # vector to a matrix as (1, M). Similarly, for a COL vector in ROW order, the LD
182
+ # should be 1 as well since we promote the column vector to a matrix as (M, 1).
183
+ ld = 1
184
+ else:
185
+ ld = max(mm_strides[0], mm_shape[0])
186
+ return order, ld, batch_offset
187
+
188
+ M, N = mm_shape
189
+
190
+ if ordering is not None:
191
+ order = ordering
192
+ message = f"Internal Error: incompatible ordering '{ordering}' and strides {mm_strides}"
193
+ if order == cusparse.Order.ROW:
194
+ assert mm_strides[0] >= mm_strides[1] and mm_strides[1] == 1, message
195
+ else:
196
+ assert mm_strides[1] >= mm_strides[0] and mm_strides[0] == 1, message
197
+ else:
198
+ # Important: start with the first dimension so that cases like (M, 1) : (1, 1) or
199
+ # (1, M) : (1, 1) in CuTe notation map to COL.
200
+ if mm_strides[0] == 1:
201
+ order = cusparse.Order.COL
202
+ elif mm_strides[1] == 1:
203
+ order = cusparse.Order.ROW
204
+ else:
205
+ if M == 1:
206
+ order = cusparse.Order.COL
207
+ elif N == 1:
208
+ order = cusparse.Order.ROW
209
+ else:
210
+ raise ValueError("Unsupported layout.")
211
+
212
+ ld = max(M, mm_strides[1]) if order == cusparse.Order.COL else max(N, mm_strides[0])
213
+
214
+ # Batch dimensions should be contiguous in memory, which we have already checked. The
215
+ # batch_offset should be based on the lowest stride in the batch dimension to account
216
+ # for embedded matrices.
217
+ batch_offset = min(batch_strides) if batch_strides else 0
218
+
219
+ return order, ld, batch_offset
220
+
221
+
222
+ def get_spmm_traits(a, b, c, *, qualifiers, inplace, logger):
223
+ """
224
+ First check A and B compatibility:
225
+
226
+ 1. The sparse operand is always 2D or higher. Gives M, K.
227
+ 2. The dense operand B can be a vector or matrix. If it's
228
+ a vector, an implicit singleton extent is added (but it
229
+ doesn't appear in the result. Implicitly N=1 in this case.
230
+ 3. C can be a vector or matrix compatible with (M,N).
231
+ 4. The dense matrix batch dimensions must be in C order.
232
+ 5. M, K, N must be determined considering the transpose flag.
233
+
234
+ 1. Check MM compatibility (K):
235
+ a. First pad A and/or B MM dimensions if 1-D according to NumPy convention.
236
+ b. The padding is used to determine M, N, and K but should not appear in the output
237
+ dimensions.
238
+ c. If both A and B are N-D, the dimensions must match.
239
+ 2. Check batch dimensions:
240
+ a. One of A or B can have missing batch extents, in which case it is broadcast,
241
+ otherwise
242
+ b. A and B must have the same batch ordering.
243
+ c. In addition, the batch dimensions must be tileable (contiguous in memory).
244
+
245
+ Then check C:
246
+
247
+ C can be None. If C is passed in, it must be matrix. Batching rule is the
248
+ same as above.
249
+ """
250
+
251
+ # TODO: remove this guard once we add the number of sparse/dense dimension attributes
252
+ # to UST.
253
+ if not isinstance(a, USTensorHolder):
254
+ a_num_dense_dim = a.attr_name_map["num_dense_dim"](a.tensor)
255
+ if a_num_dense_dim > 0:
256
+ raise ValueError(
257
+ "The sparse matrix contains tensor elements, which is not supported for sparse matrix multiplication."
258
+ )
259
+
260
+ a_shape = list(a.shape)
261
+ a_batch_shape, a_mm_shape = a_shape[:-2], a_shape[-2:]
262
+
263
+ b_shape, b_strides = list(b.shape), list(b.strides)
264
+ b_batch_shape, b_mm_shape = b_shape[:-2], b_shape[-2:]
265
+ b_batch_strides, b_mm_strides = b_strides[:-2], b_strides[-2:]
266
+
267
+ # Handle 1D `a` and `b` according to our semantics.
268
+ d_mm_shape = []
269
+ a_vector = False
270
+ if len(a_mm_shape) == 1:
271
+ a_mm_shape = [1] + a_mm_shape
272
+ a_vector = True
273
+ else:
274
+ d_mm_shape.append(a_mm_shape[0]) # The first mode for d applies only when a is not a vector.
275
+
276
+ if len(b_mm_shape) == 1:
277
+ s, d = b_mm_shape[0], b_mm_strides[0]
278
+ b_mm_shape = b_mm_shape + [1]
279
+ b_mm_strides = b_mm_strides + [s * d]
280
+ else:
281
+ d_mm_shape.append(b_mm_shape[1]) # The second mode for d applies only when b is not a vector.
282
+
283
+ logger.debug(f"The MM shape for operand A is {a_mm_shape}.")
284
+ logger.debug(f"The MM shape for operand B is {b_mm_shape} with strides {b_mm_strides}.")
285
+ logger.debug(f"The MM shape for operand D is {d_mm_shape}.")
286
+
287
+ a_qualifiers, b_qualifiers, c_qualifiers = qualifiers
288
+ M0, K0 = a_mm_shape
289
+ if a_qualifiers["is_transpose"]:
290
+ M0, K0 = K0, M0
291
+
292
+ K1, N0 = b_mm_shape
293
+ if b_qualifiers["is_transpose"]:
294
+ K1, N0 = N0, K1
295
+
296
+ if K0 != K1:
297
+ raise ValueError(
298
+ f"The 'K' extent must match for the operands: K={K0} in operand A is not equal to K={K1} in operand B. \
299
+ The transpose qualifiers for A and B are {a_qualifiers['is_transpose'].item(), b_qualifiers['is_transpose'].item()}."
300
+ )
301
+
302
+ # The batch dimensions of all component dense arrays of the sparse tensor `a` should
303
+ # be tileable in C-order.
304
+ batch_shape = []
305
+ if len(a_batch_shape) > 0:
306
+ batch_shape = a_batch_shape
307
+
308
+ # Check if the dense operand `b` batch dimensions are tileable as well as compatible
309
+ # with `a`.
310
+ if len(b_batch_shape) > 0:
311
+ if not (
312
+ is_contiguous_in_memory(b_batch_shape, b_batch_strides) and check_monotonic_strides(b_batch_strides, reverse=True)
313
+ ):
314
+ message = (
315
+ f"The batch layout for B corresponding to shape = {b_batch_shape} and strides = {b_batch_strides} "
316
+ "is currently not supported because it is not tileable and in C-order."
317
+ )
318
+ raise ValueError(message)
319
+ logger.debug(
320
+ f"The batch layout for B corresponding to shape = {b_batch_shape} and strides = {b_batch_strides} \
321
+ IS tileable in C-order."
322
+ )
323
+
324
+ if not batch_shape:
325
+ batch_shape = b_batch_shape
326
+
327
+ if len(b_batch_shape) > 0 and b_batch_shape != batch_shape:
328
+ raise ValueError(f"The batch dimensions of operands A {batch_shape} and B {b_batch_shape} must match.")
329
+
330
+ num_batch_dim = len(batch_shape)
331
+ logger.debug(f"The batch shape is {batch_shape} with batch axis in C-order.")
332
+
333
+ batch_count = functools.reduce(operator.mul, batch_shape, 1)
334
+
335
+ # Create sparse matrix layout traits for `a`.
336
+ a_layout_traits = SparseLayoutTraits(
337
+ sparse_format=a.format_name, mm_shape=a_mm_shape, batch_count=batch_count, batch_broadcast=len(a_batch_shape) == 0
338
+ )
339
+
340
+ # Create dense matrix layout traits for `b`.
341
+ b_order, b_ld, b_batch_offset = get_dense_matrix_layout_traits(b_mm_shape, b_mm_strides, b_batch_strides)
342
+ b_layout_traits = DenseLayoutTraits(
343
+ order=b_order,
344
+ ld=b_ld,
345
+ mm_shape=b_mm_shape,
346
+ batch_shape=b_batch_shape,
347
+ batch_offset=b_batch_offset,
348
+ batch_count=batch_count,
349
+ )
350
+
351
+ # Process matrix c, if provided.
352
+ c_layout_traits = None
353
+ if c is not None:
354
+ # 1. C can be a vector, as long as it's columns don't need to be broadcast.
355
+ # 2. C can be a matrix of dimension (M, N).
356
+ # 3. C can be batched matrices of dimension (..., M, N).
357
+ c_shape, c_strides = list(c.shape), list(c.strides)
358
+
359
+ c_batch_shape, c_mm_shape = c_shape[:-2], c_shape[-2:]
360
+ c_batch_strides, c_mm_strides = c_strides[:-2], c_strides[-2:]
361
+ if len(c_mm_shape) == 0:
362
+ c_mm_shape = [1, 1]
363
+ c_mm_strides = [1, 1]
364
+ elif len(c_mm_shape) == 1:
365
+ # raise ValueError(f"C cannot be a vector. C shape: {c_mm_shape}")
366
+ s, d = c_mm_shape[0], c_mm_strides[0]
367
+ c_mm_shape = [1] + c_mm_shape if a_vector else c_mm_shape + [1]
368
+ c_mm_strides = c_mm_strides + [s * d]
369
+ logger.debug(f"The MM shape for operand C is {c_mm_shape} with strides {c_mm_strides}.")
370
+
371
+ Mc, Nc = c_mm_shape
372
+ if Mc != M0:
373
+ raise ValueError(
374
+ f"The M dimension of the C matrix ({Mc}) must match the M dimension of A ({M0}). \
375
+ The transpose qualifier for A is {a_qualifiers['is_transpose']}."
376
+ )
377
+
378
+ if Nc != N0:
379
+ raise ValueError(
380
+ f"The N dimension of the C matrix ({Nc}) must match the N dimension of B ({N0}). \
381
+ The transpose qualifier for B is {b_qualifiers['is_transpose']}."
382
+ )
383
+
384
+ # For the inplace option, C must be batched if an operand is batched.
385
+ if inplace or len(c_batch_shape) > 0:
386
+ if c_batch_shape != batch_shape:
387
+ raise ValueError(
388
+ f"The batch dimension of operand C {c_batch_shape} must match with that of the other operands "
389
+ f"{batch_shape}."
390
+ )
391
+
392
+ if c_batch_shape and not (
393
+ is_contiguous_in_memory(c_batch_shape, c_batch_strides)
394
+ and check_monotonic_strides(c_batch_strides, reverse=True)
395
+ ):
396
+ raise ValueError("The batch axes of operand C must be in C-order.")
397
+
398
+ c_order, c_ld, c_batch_offset = get_dense_matrix_layout_traits(c_mm_shape, c_mm_strides, c_batch_strides)
399
+ else:
400
+ # Compute the shape and strides for the result `c`.
401
+ c_mm_shape = d_mm_shape
402
+ c_shape = batch_shape + c_mm_shape
403
+
404
+ c_axis_order = list(range(0, num_batch_dim))
405
+ # Use the result_ordering from b's ordering.
406
+ if b_order == cusparse.Order.ROW:
407
+ c_axis_order += [num_batch_dim + 1, num_batch_dim]
408
+ elif b_order == cusparse.Order.COL:
409
+ c_axis_order += [num_batch_dim, num_batch_dim + 1]
410
+
411
+ c_strides = calculate_strides(c_shape, c_axis_order)
412
+
413
+ c_batch_shape, _ = c_shape[:-2], c_shape[-2:]
414
+ c_batch_strides, c_mm_strides = c_strides[:-2], c_strides[-2:]
415
+
416
+ # For degenerate matrices, we need to specify the result orientation.
417
+ result_orientation = None
418
+ if len(c_mm_shape) < 2:
419
+ if M0 == 1:
420
+ result_orientation = cusparse.Order.ROW
421
+ elif N0 == 1:
422
+ result_orientation = cusparse.Order.COL
423
+
424
+ c_order, c_ld, c_batch_offset = get_dense_matrix_layout_traits(
425
+ c_mm_shape, c_mm_strides, c_batch_strides, ordering=cusparse.Order.ROW, orientation=result_orientation
426
+ )
427
+
428
+ logger.debug(f"The layout order for operand C is {c_order.name}, with LD {c_ld}, and batch offset {c_batch_offset}.")
429
+ # Create dense matrix layout traits for `c`.
430
+ c_layout_traits = DenseLayoutTraits(
431
+ order=c_order,
432
+ ld=c_ld,
433
+ mm_shape=c_mm_shape,
434
+ batch_shape=c_batch_shape,
435
+ batch_offset=c_batch_offset,
436
+ batch_count=batch_count,
437
+ )
438
+
439
+ M, N, K = M0, N0, K0
440
+ logger.debug(f"The SpMM operand dimensions are M={M}, N={N}, K={K}.")
441
+
442
+ # Create the SpMM traits.
443
+ spmm_traits = SpMMTraits(
444
+ M=M,
445
+ N=N,
446
+ K=K,
447
+ a_layout_traits=a_layout_traits,
448
+ b_layout_traits=b_layout_traits,
449
+ c_layout_traits=c_layout_traits,
450
+ batch_count=batch_count,
451
+ batch_shape=batch_shape,
452
+ )
453
+
454
+ return spmm_traits
455
+
456
+
457
+ SPARSE_MM_DOCUMENTATION = utils.COMMON_SHARED_DOC_MAP.copy()
458
+ SPARSE_MM_DOCUMENTATION.update(
459
+ {
460
+ "a": """\
461
+ A *sparse* tensor representing the first operand ``a`` in the sparse matrix multiplication
462
+ (SpMM) from one of the supported sparse packages: SciPy, CuPy, PyTorch, or a
463
+ :class:`universal sparse tensor (UST) <nvmath.sparse.ust.Tensor>` object
464
+ (see :ref:`semantics <spmm_semantics>`). The sparse representation may be in any of the formats supported by
465
+ the sparse package (CSR, BSC, COO, ...), including novel formats defined using the UST DSL.
466
+ """.replace("\n", " "),
467
+ #
468
+ "b": """\
469
+ A *dense* tensor representing the second operand ``b`` in the SpMM (see :ref:`semantics <spmm_semantics>`).
470
+ The currently supported types are :class:`numpy.ndarray`, :class:`cupy.ndarray`,
471
+ :class:`torch.Tensor`, and :class:`nvmath.sparse.ust.Tensor`.""".replace("\n", " "),
472
+ "c": """\
473
+ A *dense* tensor representing the addend ``c`` in the SpMM (see :ref:`semantics <spmm_semantics>`). The currently
474
+ supported types are :class:`numpy.ndarray`, :class:`cupy.ndarray`, :class:`torch.Tensor`,
475
+ and :class:`nvmath.sparse.ust.Tensor`.""".replace("\n", " "),
476
+ #
477
+ "alpha": """\
478
+ The scale factor for the matrix multiplication term as a real or complex number. The default is
479
+ :math:`1.0`.""".replace("\n", " "),
480
+ #
481
+ "beta": """\
482
+ The scale factor for the addend term in the matrix multiplication as a real or complex number.
483
+ The default is :math:`1.0`.""".replace("\n", " "),
484
+ #
485
+ "qualifiers": """\
486
+ If desired, specify the matrix qualifiers as a :class:`numpy.ndarray` of
487
+ :data:`~nvmath.sparse.matmul_matrix_qualifiers_dtype` objects of length 3 corresponding to
488
+ the operands ``a``, ``b``, and ``c``. See :ref:`matrix-tensor-qualifiers` for the motivation behind
489
+ qualifiers.""".replace("\n", " "),
490
+ #
491
+ "options": """\
492
+ Specify options for the sparse matrix multiplication as a :class:`~nvmath.sparse.MatmulOptions`
493
+ object. Alternatively, a `dict` containing the parameters for the ``MatmulOptions`` constructor
494
+ can also be provided. If not specified, the value will be set to the default-constructed
495
+ ``MatmulOptions`` object.""".replace("\n", " "),
496
+ #
497
+ "execution": """\
498
+ Specify execution space options for the SpMM as a :class:`ExecutionCUDA` object (the only
499
+ execution space currently supported). If not specified, a :class:`ExecutionCUDA` object will
500
+ be default-constructed.""".replace("\n", " "),
501
+ #
502
+ "prologs": """\
503
+ A dict mapping an operand label (``"a"``, ``"b"``, ``"c"``) to its prolog operation in LTO-IR
504
+ format (as a :class:`bytes` object). The prolog is a user-written unary function in Python
505
+ that returns the transformed value, which has the data type of the operand to which it is
506
+ applied. This function can be compiled to LTO-IR using the helper
507
+ :func:`~nvmath.sparse.compile_matmul_prolog` or your own compiler of choice. If not
508
+ specified, no prolog will be applied to the operands.""".replace("\n", " "),
509
+ #
510
+ "epilog": """\
511
+ The epilog operation in LTO-IR format (as a :class:`bytes` object). The epilog is a
512
+ user-written unary function in Python that returns the transformed value, which has the
513
+ data type of the SpMM result. This function can be compiled to LTO-IR using the helper
514
+ :func:`~nvmath.sparse.compile_matmul_epilog` or your own compiler of choice. If not
515
+ specified, no epilog will be applied to the SpMM result.""".replace("\n", " "),
516
+ #
517
+ "semiring": """\
518
+ A dict mapping the semiring operations (``"mul"``, ``"add"``, ``"atomic_add"``) to LTO-IR
519
+ code (as a :class:`bytes` object). Each semiring operation is a binary function in Python
520
+ that returns a value. These function can be compiled to LTO-IR using the helpers
521
+ :func:`~nvmath.sparse.compile_matmul_mul`, :func:`~nvmath.sparse.compile_matmul_add`, or
522
+ :func:`~nvmath.sparse.compile_matmul_atomic_add` or your own compiler of choice. If not
523
+ specified, the standard definitions of these operations from elementary algebra
524
+ will be used.""".replace("\n", " "),
525
+ #
526
+ "compute_capability": """\
527
+ The target compute capability, specified as a string (``'80'``, ``'89'``, ...). The
528
+ default is the compute capability of the current device.""".replace("\n", " "),
529
+ #
530
+ "result": """\
531
+ The result of the sparse matrix multiplication (epilog applied). Currently only in-place
532
+ SpMM is supported (the result of the computation is written into the addend ``c``).
533
+ """.replace("\n", " "),
534
+ #
535
+ "release_operands": utils._release_operand_docstring(True),
536
+ #
537
+ "reset_operands_unchecked": utils._reset_operand_unchecked_docstring(True),
538
+ #
539
+ "stream": """\
540
+ Provide the CUDA stream to use for executing the operation. Acceptable inputs include
541
+ ``cudaStream_t`` (as Python :class:`int`), :class:`cupy.cuda.Stream`, and
542
+ :class:`torch.cuda.Stream`. If a stream is not provided, the current stream for the
543
+ operand device will be queried from the dense operand ``b`` (and ``c``) package.
544
+ """.replace("\n", " "),
545
+ #
546
+ "semantics": """\
547
+ .. _spmm_semantics:
548
+
549
+ The semantics of the matrix multiplication follows :external:py:data:`numpy.matmul` semantics, with some restrictions on
550
+ broadcasting. In addition, the semantics for the fused matrix addition are described below.
551
+
552
+ * For in-place matrix multiplication (where the result is written into ``c``) the result has the same shape as ``c``.
553
+ * The operand ``a`` must be a sparse matrix or batched sparse matrix. Popular named formats like BSC, BSR, COO,
554
+ CSR, ... are supported in addition to custom formats defined using the UST DSL.
555
+ * The operands ``b`` and ``c`` must be "dense" matrices (that is, their layout is strided).
556
+ * If the operands ``a`` and ``b`` are matrices, they are multiplied according to the rules of matrix
557
+ multiplication.
558
+ * If argument ``b`` is 1-D, it is promoted to a matrix by appending ``1`` to its dimensions. After matrix
559
+ multiplication, the appended ``1`` is removed from the result's dimensions if the operation is not in-place.
560
+ * If ``a`` or ``b`` is N-D (N > 2), then the operand is treated as a batch of matrices. If both ``a`` and ``b``
561
+ are N-D, their batch dimensions must match. If exactly one of ``a`` or ``b`` is N-D, the other operand is
562
+ broadcast.
563
+ * The operand for the matrix addition ``c`` must be a matrix of shape (M, N), or the batched equivalent
564
+ (..., M, N). Here M and N are the dimensions of the result of the matrix multiplication. If batch dimensions
565
+ are not present, ``c`` is broadcast across batches as needed. If the operation is in-place, ``c`` cannot be
566
+ broadcast since it must be large enough to hold the result.
567
+ """.strip(),
568
+ }
569
+ )
570
+
571
+
572
+ @utils.docstring_decorator(SPARSE_MM_DOCUMENTATION, skip_missing=False)
573
+ class Matmul:
574
+ """
575
+ Create a stateful object encapsulating the specified matrix multiplication computation,
576
+ which is one of :math:`epilog(\\alpha \\, op_h(a) \\, @ \\, op_h(b) + \\beta \\, c)` or
577
+ :math:`epilog(prolog_a(op_t(a)) \\, @ \\, prolog_b(op_t(b)) + prolog_c(c))`, along with
578
+ the required resources to perform the operation. The :math:`op_h` and :math:`op_t`
579
+ operators optionally specify transpose/hermitian or transpose operations respectively
580
+ via the ``qualifiers`` argument. In addition, the scalar multiplication and addition
581
+ operators ("semiring") can be customized by the user, if desired.
582
+
583
+ .. note::
584
+ The complex conjugate operation is mutually exclusive with prolog since it can be
585
+ absorbed into the prolog.
586
+
587
+ .. note::
588
+ Currently only in-place sparse matrix multiplication is supported, so operand ``c``
589
+ must be provided. This restriction will be removed in a future release.
590
+
591
+ A stateful object can be used to amortize the cost of preparation (planning in the
592
+ case of matrix multiplication) across multiple executions (also see the
593
+ :ref:`Stateful APIs <host api types>` section). Prolog, epilog, and semiring
594
+ operations can be specified in :meth:`plan`.
595
+
596
+ The function-form API :func:`matmul` is a convenient alternative to using stateful
597
+ objects for *single* use (the user needs to perform just one matrix multiplication, for
598
+ example), in which case there is no possibility of amortizing preparatory costs. The
599
+ function-form APIs are just convenience wrappers around the stateful object APIs.
600
+
601
+ Using the stateful object typically involves the following steps:
602
+
603
+ 1. **Problem Specification**: Initialize the object with a defined operation and
604
+ options.
605
+ 2. **Preparation**: Use :meth:`plan` to determine the best algorithmic implementation
606
+ for this specific matrix multiplication operation.
607
+ 3. **Execution**: Perform the matrix multiplication computation with :meth:`execute`.
608
+ 4. **Resource Management**: Ensure all resources are released either by explicitly
609
+ calling :meth:`free` or by managing the stateful object within a context manager.
610
+
611
+ Detailed information on what's happening in the various phases described above can be
612
+ obtained by passing in a :class:`logging.Logger` object to :class:`MatmulOptions` or by
613
+ setting the appropriate options in the root logger object, which is used by default:
614
+
615
+ >>> import logging
616
+ >>> logging.basicConfig(
617
+ ... level=logging.INFO,
618
+ ... format="%(asctime)s %(levelname)-8s %(message)s",
619
+ ... datefmt="%m-%d %H:%M:%S",
620
+ ... )
621
+
622
+ A user can select the desired logging level and, in general, take advantage of all of
623
+ the functionality offered by the Python `logging` module.
624
+
625
+ Args:
626
+ a: {a}
627
+
628
+ b: {b}
629
+
630
+ c: {c}
631
+
632
+ alpha: {alpha}
633
+
634
+ beta: {beta}
635
+
636
+ qualifiers: {qualifiers}
637
+
638
+ options: {options}
639
+
640
+ execution: {execution}
641
+
642
+ stream: {stream}
643
+
644
+ Semantics:
645
+ {semantics}
646
+
647
+ .. seealso::
648
+ :class:`MatmulOptions`, :class:`ExecutionCUDA`, :meth:`plan`,
649
+ :meth:`release_operands`, :meth:`reset_operands`,
650
+ :meth:`reset_operands_unchecked`, :meth:`execute`, :func:`matmul`.
651
+
652
+ Examples:
653
+
654
+ >>> import cupy as cp
655
+ >>> import cupyx.scipy.sparse as sp
656
+ >>> import nvmath
657
+
658
+ The problem parameters.
659
+
660
+ >>> m, n, k = 4, 2, 4
661
+ >>> index_type, dtype = "int32", "float64"
662
+
663
+ Create a sparse float64 ndarray in CSR format on the GPU.
664
+
665
+ >>> crow_indices = cp.array([0, 2, 4, 6, 8], dtype=index_type)
666
+ >>> col_indices = cp.array([0, 1, 0, 1, 2, 3, 2, 3], dtype=index_type)
667
+ >>> values = cp.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=dtype)
668
+
669
+ >>> a = sp.csr_matrix((values, col_indices, crow_indices), shape=(m, k))
670
+
671
+ Create the dense operands ``b`` and ``c``.
672
+
673
+ >>> b = cp.ones((k, n), dtype=dtype)
674
+ >>> c = cp.zeros((m, n), dtype=dtype)
675
+
676
+ We will define a sparse matrix multiplication (SpMM) operation
677
+ :math:`c := \\alpha \\, a \\, @ \\, b + \\beta \\, c` using the generic sparse matrix
678
+ multiplication interface.
679
+
680
+ >>> mm = nvmath.sparse.Matmul(a, b, c=c, alpha=1.2, beta=1.0)
681
+
682
+ Options can be provided above to control the behavior of the operation using the
683
+ `options` argument (see :class:`MatmulOptions`).
684
+
685
+ Next, plan the operation. Optionally, user-defined prologs, epilog, and semiring
686
+ can be provided.
687
+
688
+ >>> mm.plan()
689
+
690
+ Now execute the matrix multiplication. The result ``r`` is also a CuPy ndarray, and
691
+ specifically in this example, it is the same as operand ``c`` since the operation
692
+ is in-place.
693
+
694
+ >>> r = mm.execute()
695
+ >>> assert r is c, "Error: the operation is not in-place."
696
+
697
+ Finally, free the object's resources. To avoid having to explicitly make this
698
+ call, it's recommended to use the Matmul object as a context manager as shown below,
699
+ if possible.
700
+
701
+ >>> mm.free()
702
+
703
+ .. note:: All :class:`Matmul` methods execute on the package current stream by default.
704
+ Alternatively, the ``stream`` argument can be used to run a method on a specified
705
+ stream.
706
+
707
+ Let's now look at the SpMM :math:`c := a.T \\, @ \\, b.H + c` with PyTorch sparse/dense
708
+ tensors on the CPU.
709
+
710
+ >>> import numpy as np
711
+ >>> import torch
712
+
713
+ >>> m, n, k = 2, 2, 2
714
+ >>> index_type, dtype = torch.int32, torch.float32
715
+
716
+ Create and coalesce a sparse COO tensor on the CPU.
717
+
718
+ >>> indices = torch.tensor([[0, 1], [0, 1]], dtype=index_type)
719
+ >>> values = torch.tensor([2.0, 4.0], dtype=dtype) + 1.0j
720
+ >>> a = torch.sparse_coo_tensor(indices, values, (k, m))
721
+ >>> a = a.coalesce()
722
+
723
+ Create the dense operands ``b`` and ``c``.
724
+
725
+ >>> b = torch.ones(n, k, dtype=dtype) + 1.0j
726
+ >>> c = torch.zeros(m, n, dtype=dtype) + 0.0j
727
+
728
+ The transpose/hermitian operations on ``a`` and ``b`` will be specified using
729
+ :ref:`qualifiers <matrix-tensor-qualifiers>`.
730
+
731
+ >>> qualifiers = np.zeros((3,), dtype=nvmath.sparse.matmul_matrix_qualifiers_dtype)
732
+ >>> qualifiers[0]["is_transpose"] = 1
733
+ >>> qualifiers[1]["is_transpose"] = qualifiers[1]["is_conjugate"] = 1
734
+
735
+ Create the SpMM operation and use it as a context manager.
736
+
737
+ >>> with nvmath.sparse.Matmul(a, b, c=c, qualifiers=qualifiers) as mm:
738
+ ... # Plan the operation.
739
+ ... mm.plan()
740
+ ...
741
+ ... # Execute it.
742
+ ... r = mm.execute()
743
+
744
+ All the resources used by the object are released at the end of the block.
745
+
746
+ Finally, let's see how to perform a matrix multiplication on a novel format
747
+ using UST operands.
748
+
749
+ >>> device_id = 0
750
+ >>> dtype = torch.float64
751
+ >>> m, n, k = 3, 2, 8
752
+
753
+ Create a dense torch tensor and view it as UST.
754
+
755
+ >>> a = torch.tensor(
756
+ ... [[1, 0, 0, 0, 0, 0, 0, 2], [0, 0, 3, 4, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 5]],
757
+ ... dtype=dtype,
758
+ ... device=device_id,
759
+ ... )
760
+ >>> a = nvmath.sparse.ust.Tensor.from_package(a)
761
+
762
+ Create a delta-compression format, using the predefined type or directly with
763
+ the UST DSL.
764
+
765
+ >>> delta = nvmath.sparse.ust.NamedFormats.DELTA(2)
766
+
767
+ Convert the torch tensor to the delta-compressed format.
768
+
769
+ >>> a = a.convert(tensor_format=delta)
770
+
771
+ Create dense operands ``b`` and ``c`` for the SpMM.
772
+
773
+ >>> b = torch.ones(k, n, dtype=dtype, device=device_id)
774
+ >>> b = nvmath.sparse.ust.Tensor.from_package(b)
775
+ >>> c = torch.zeros(m, n, dtype=dtype, device=device_id)
776
+ >>> c = nvmath.sparse.ust.Tensor.from_package(c)
777
+
778
+ Create, plan, and execute the SpMM operation.
779
+
780
+ >>> with nvmath.sparse.Matmul(a, b, c=c) as mm:
781
+ ... # Plan the SpMM.
782
+ ... mm.plan()
783
+ ...
784
+ ... # Execute it.
785
+ ... r = mm.execute()
786
+
787
+ View the UST result as a torch tensor.
788
+
789
+ >>> r = r.to_package()
790
+
791
+ Examples sampling the vast space of possibilities can be found in the
792
+ `nvmath/examples/sparse/generic/matmul
793
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/sparse/generic/matmul>`_
794
+ directory.
795
+ """ # noqa: W505
796
+
797
+ def __init__(
798
+ self,
799
+ a,
800
+ b,
801
+ /,
802
+ c=None,
803
+ *,
804
+ alpha=None,
805
+ beta=None,
806
+ qualifiers=None,
807
+ options=None,
808
+ execution: ExecutionCUDA | None = None,
809
+ stream: utils.AnyStream | int | None = None,
810
+ ):
811
+ options = utils.check_or_create_options(MatmulOptions, options, "matrix multiplication options")
812
+ assert options is not None
813
+ self.options = options
814
+
815
+ self.execution = execution = utils.check_or_create_options(
816
+ ExecutionCUDA, execution, "matrix multiplication execution options"
817
+ )
818
+
819
+ if c is None:
820
+ raise NotImplementedError("The operand C is currently required since only inplace SpMM is supported.")
821
+
822
+ self.logger = options.logger if options.logger is not None else logging.getLogger()
823
+
824
+ # Wrap operand 'a'.
825
+ try:
826
+ self.a = a = sp_utils.wrap_sparse_operands(a)
827
+ except Exception as e:
828
+ raise TypeError(
829
+ """The operand 'a' must be an N-D sparse array/tensor from one of the supported packages: nvmath.sparse.ust,
830
+ CuPy, PyTorch, or SciPy."""
831
+ ) from e
832
+
833
+ self.logger.info(f"The sparse format for operand A is '{a.format_name}'.")
834
+
835
+ # Note the sparse wrapper type for the fast path in reset_operands_unchecked().
836
+ self.a_wrapper_type = self.a.__class__
837
+ self.a_attr_name_map = self.a.attr_name_map
838
+ self.logger.debug(f"The sparse wrapper type for operand A is '{self.a_wrapper_type}'.")
839
+
840
+ if sp_utils.sparse_or_dense(b) != "dense":
841
+ raise ValueError(f"The operand `b` {type(b)} must be dense.")
842
+
843
+ self.ust_operands = []
844
+ # TODO: create unified sparse-dense abstraction for UST later. For now we'll
845
+ # extract the dense tensor from the dense UST for `b` and `c`.
846
+ if (b_package := utils.infer_object_package(b)) == "nvmath":
847
+ self.ust_operands.append(b)
848
+ # Though b.wrapped_operand is the TensorHolder
849
+ # we look for, create a new one, so that
850
+ # matmul is free to modify it in place if needed.
851
+ self.b = b = tensor_wrapper.wrap_operand(b.wrapped_operand.tensor)
852
+ else:
853
+ # Wrap operand 'b' (currently limit to dense operand).
854
+ self.b = b = tensor_wrapper.wrap_operand(b) # type:ignore
855
+
856
+ self.operands = operands = [a, b]
857
+
858
+ # For now, assume 'c' is provided.
859
+ if sp_utils.sparse_or_dense(c) != "dense":
860
+ raise ValueError(f"The operand `c` {type(c)} must be dense.")
861
+
862
+ if (c_package := utils.infer_object_package(c)) == "nvmath":
863
+ self.ust_operands.append(c)
864
+ # Though c.wrapped_operand is the TensorHolder
865
+ # we look for, create a new one, so that
866
+ # matmul is free to modify it in place if needed.
867
+ self.c = c = tensor_wrapper.wrap_operand(c.wrapped_operand.tensor)
868
+ else:
869
+ self.c = c = tensor_wrapper.wrap_operand(c)
870
+ operands.append(c)
871
+
872
+ # The number of operands, since `c` is optional. If `c` is not provided,
873
+ # set Beta = 0.
874
+ self.num_operands = len(operands)
875
+
876
+ if self.num_operands == 3 and b_package != c_package:
877
+ raise TypeError(f"The operands 'b' ({b_package}) and 'c' ({c_package}) do NOT belong to the same package.")
878
+
879
+ self.logger.info(f"The data type of operand A is '{a.dtype}', and that of operand B is '{b.dtype}'.")
880
+ if c is not None:
881
+ self.logger.info(f"The data type of operand C is '{c.dtype}'.")
882
+ self.logger.info(f"The index type of operand A is '{a.index_type}'.")
883
+
884
+ # Currently, only SpMM is supported and so the index type is obtained from `a`.
885
+ self.index_type_name = a.index_type
886
+ self.index_type = typemaps.NAME_TO_DATA_TYPE[a.index_type]
887
+ if self.index_type_name not in VALID_INDEX_TYPES:
888
+ raise TypeError(
889
+ f"The index type {self.index_type_name} is not supported. The supported index types are {VALID_INDEX_TYPES}."
890
+ )
891
+
892
+ # Currently we require `c` and only inplace update of `c` is supported.
893
+ assert self.num_operands == 3, "Internal Error."
894
+ self.inplace = True # Currently only inplace update of `c` is supported.
895
+ if self.inplace:
896
+ self.logger.info("The operation will be performed inplace with operand C.")
897
+
898
+ # Determine the data types for a and b.
899
+ self.a_dtype = typemaps.NAME_TO_DATA_TYPE[a.dtype]
900
+ self.b_dtype = typemaps.NAME_TO_DATA_TYPE[b.dtype]
901
+ self.a_dtype_name = a.dtype
902
+ self.b_dtype_name = b.dtype
903
+
904
+ self.is_complex = "complex" in self.a_dtype_name or "complex" in self.b_dtype_name
905
+
906
+ # Determine the data type for c.
907
+ if self.num_operands == 3:
908
+ self.c_dtype = typemaps.NAME_TO_DATA_TYPE[c.dtype]
909
+ elif self.num_operands == 2:
910
+ self.c_dtype = self.a_dtype
911
+ self.c_dtype_name = typemaps.DATA_TYPE_TO_NAME[self.c_dtype]
912
+
913
+ # The common (semantic) checks for the problem such as compatibility,
914
+ # tileability etc.
915
+ value_type_names = {self.a_dtype_name, self.b_dtype_name, self.c_dtype_name}
916
+ if len(value_type_names) != 1:
917
+ raise NotImplementedError(
918
+ f"The dtype for the operands {self.a_dtype_name}, {self.b_dtype_name}, and \
919
+ {self.c_dtype_name} don't match. Mixing operands of different precisions is currently not supported."
920
+ )
921
+ self.value_type_name = value_type_names.pop()
922
+
923
+ if self.value_type_name not in VALID_DTYPES:
924
+ raise TypeError(
925
+ f"The dtype (value type) {self.value_type_name} is not supported. The supported dtypes are {VALID_DTYPES}."
926
+ )
927
+ self.value_type = typemaps.NAME_TO_DATA_TYPE[self.a_dtype_name]
928
+ self.regular_precision = _is_regular_precision(self.value_type_name)
929
+
930
+ # Set compute type.
931
+ if self.options.compute_type is None:
932
+ default_compute_type = ComputeType.CUDA_C_32F if self.is_complex else ComputeType.CUDA_R_32F
933
+ self.compute_type = self.a_dtype if self.regular_precision else default_compute_type
934
+ else:
935
+ try:
936
+ self.compute_type = ComputeType(self.options.compute_type)
937
+ except Exception as e:
938
+ message = f"The specified compute type {self.options.compute_type} is not a valid compute type."
939
+ raise TypeError(message) from e
940
+ self.compute_type_name = typemaps.DATA_TYPE_TO_NAME[self.compute_type]
941
+
942
+ if self.is_complex and "complex" not in self.compute_type_name:
943
+ raise ValueError(f"The specified compute type {self.compute_type_name} is complex.")
944
+ if not self.is_complex and "complex" in self.compute_type_name:
945
+ raise ValueError(
946
+ f"The specified compute type {self.compute_type_name} is complex, while the \
947
+ operands' dtype {self.value_type} is real."
948
+ )
949
+
950
+ self.logger.info(f"The compute type for the matrix multiplication is {self.compute_type_name}.")
951
+
952
+ # Set alpha and beta: note that the currently-supported compute types are valid
953
+ # NumPy dtypes.
954
+ self.alpha = np.zeros((1,), dtype=self.compute_type_name)
955
+ try:
956
+ self.alpha[0] = alpha if alpha is not None else 1
957
+ except (ValueError, TypeError) as e:
958
+ raise ValueError(f"The value provided for alpha {alpha} is not convertible to dtype '{self.alpha.dtype}'.") from e
959
+
960
+ self.beta = np.zeros((1,), dtype=self.compute_type_name)
961
+ if beta is not None and self.num_operands == 2:
962
+ self.logger.warning(f"Matmul: The provided beta value {beta} is ignored since operand C is not specified.")
963
+ try:
964
+ default_beta = 1.0 if self.inplace else 0.0
965
+ self.beta[0] = beta if beta is not None and self.num_operands == 3 else default_beta
966
+ except (ValueError, TypeError) as e:
967
+ raise ValueError(f"The value provided for beta {beta} is not convertible to dtype '{self.beta.dtype}'.") from e
968
+
969
+ # Check for compatible operand packages.
970
+ self.sparse_package = utils.infer_object_package(self.a.tensor)
971
+
972
+ self.dense_package: str
973
+ if self.ust_operands:
974
+ orig_dense_package = {utils.infer_object_package(o) for o in self.ust_operands}
975
+ if len(orig_dense_package) != 1:
976
+ raise TypeError(f"The dense operands `b` and `c` don't belong to the same package: {orig_dense_package}.")
977
+ orig_dense_package = orig_dense_package.pop() # type: ignore[assignment]
978
+ # Use the wrapped operands to get the dense package for dense UST.
979
+ self.dense_package = utils.get_operands_package(operands[1:])
980
+ else:
981
+ self.dense_package = orig_dense_package = utils.get_operands_package(operands[1:]) # type: ignore
982
+
983
+ if (self.sparse_package, orig_dense_package) not in cusparse_utils.COMPATIBLE_SP_DN_PACKAGES:
984
+ raise TypeError(
985
+ f"""The sparse operand package {self.sparse_package} and dense operand(s) package {orig_dense_package} are
986
+ not part of the compatible choices: {cusparse_utils.COMPATIBLE_SP_DN_PACKAGES}."""
987
+ )
988
+
989
+ a_dense_package = self.a.dense_tensorholder_type.name
990
+ addendum = ""
991
+ if self.sparse_package == "nvmath":
992
+ addendum = f", with the dense representation package {a_dense_package}"
993
+ self.logger.info(f"The sparse operand package is {self.sparse_package}{addendum}.")
994
+ self.logger.info(f"The dense operand(s) package is {orig_dense_package}{addendum}.")
995
+
996
+ # The dense package must match also match the dense package used to represent the
997
+ # sparse tensor to correctly handle stream ordering etc.
998
+ if self.sparse_package == "nvmath" and a_dense_package != self.dense_package:
999
+ raise TypeError(
1000
+ f"The UST operands `a` uses a different representation package ({a_dense_package}) from that \
1001
+ of the other operand(s) ({self.dense_package})."
1002
+ )
1003
+
1004
+ # Memory space.
1005
+ self.memory_space = "cuda"
1006
+ self.device_id = utils.get_operands_device_id(operands)
1007
+ if self.device_id == "cpu":
1008
+ if self.dense_package == "numpy":
1009
+ self.dense_package = "cuda"
1010
+ self.memory_space = "cpu"
1011
+ self.device_id = self.execution.device_id # type: ignore[union-attr]
1012
+
1013
+ # An UST operand backed by CPU-only packages like NumPy is not supported.
1014
+ if self.memory_space == "cpu" and self.ust_operands and self.dense_package == "cuda":
1015
+ raise NotImplementedError("The Matmul operation does not currently support UST operands backed by NumPy.")
1016
+
1017
+ # Execution space.
1018
+ self.execution_space = "cuda"
1019
+ self.logger.info(
1020
+ f"The input operands' memory space is {self.memory_space}, and the execution space is on device {self.device_id}."
1021
+ )
1022
+
1023
+ # Set stream.
1024
+ stream_holder = utils.get_or_create_stream(self.device_id, stream, self.dense_package)
1025
+ self.logger.info(f"The specified stream for the Matmul ctor is {stream_holder.obj}.")
1026
+
1027
+ # Copy operands to device if needed.
1028
+ self.cpu_c_ref = None
1029
+ if self.memory_space == "cpu":
1030
+ if self.inplace:
1031
+ self.cpu_c_ref = self.ust_operands[1] if self.ust_operands else self.operands[2]
1032
+
1033
+ # Note that UST *code generation* is currently not supported for CPU memory
1034
+ # space while dispatch is, it's checked later in plan().
1035
+
1036
+ self.operands = [o.to(self.device_id, stream_holder) for o in self.operands]
1037
+
1038
+ # Update the operand aliases to point to the mirrors.
1039
+ if self.num_operands == 2:
1040
+ self.a, self.b = self.operands
1041
+ else:
1042
+ self.a, self.b, self.c = self.operands
1043
+
1044
+ # Replace the CPU UST operands with new device UST by *creating* them from the
1045
+ # dense operands.
1046
+ if self.ust_operands:
1047
+ self.ust_operands[0] = UST.from_package(self.b.tensor, stream_holder.external)
1048
+ if self.num_operands == 3:
1049
+ self.ust_operands[1] = UST.from_package(self.c.tensor, stream_holder.external)
1050
+
1051
+ # Set qualifiers.
1052
+ self.qualifiers = qualifiers if qualifiers is not None else np.zeros((3,), dtype=matrix_qualifiers_dtype)
1053
+ if self.qualifiers.dtype != matrix_qualifiers_dtype:
1054
+ raise ValueError(
1055
+ "The qualifiers must be specified as a NumPy array of length 3 corresponding to the operands A, B, and "
1056
+ "C of type 'matrix_qualifiers_dtype'."
1057
+ )
1058
+
1059
+ # Set qualifiers based on torch lazy conjugation flag if not provided.
1060
+ self.qualifiers[1]["is_conjugate"] = self.qualifiers[1]["is_conjugate"] ^ self.operands[1].is_conjugate
1061
+ if self.num_operands == 3:
1062
+ self.qualifiers[2]["is_conjugate"] = self.qualifiers[2]["is_conjugate"] ^ self.operands[2].is_conjugate
1063
+ if self.qualifiers[2]["is_conjugate"]:
1064
+ raise NotImplementedError("The conjugate flag is currently not supported for operand C.")
1065
+ if self.qualifiers[2]["is_transpose"]:
1066
+ raise NotImplementedError("The transpose flag is currently not supported for operand C.")
1067
+
1068
+ # The SpMM traits.
1069
+ self.spmm_traits = get_spmm_traits(a, b, c, qualifiers=self.qualifiers, inplace=self.inplace, logger=self.logger)
1070
+
1071
+ # Set blocking or non-blocking behavior.
1072
+ self.blocking = self.options.blocking is True or self.memory_space == "cpu"
1073
+ if self.blocking:
1074
+ self.call_prologue = "This call is blocking and will return only after the operation is complete."
1075
+ else:
1076
+ self.call_prologue = (
1077
+ "This call is non-blocking and will return immediately after the operation is launched on the device."
1078
+ )
1079
+
1080
+ # The result class is that of the wrapped dense device operand 'b'.
1081
+ self.result_class = self.operands[1].__class__
1082
+
1083
+ # Set memory allocator.
1084
+ self.allocator = (
1085
+ options.allocator
1086
+ if options.allocator is not None
1087
+ else memory._MEMORY_MANAGER[self.dense_package](self.device_id, self.logger)
1088
+ )
1089
+
1090
+ # Set memory limit.
1091
+ self.memory_limit = utils.get_memory_limit_from_device_id(self.options.memory_limit, self.device_id)
1092
+ self.logger.info(f"The memory limit is {formatters.MemoryStr(self.memory_limit)}.")
1093
+
1094
+ # Determine provisional support for dispatch and codegen here, finalize later
1095
+ # in plan.
1096
+ if self.options.codegen:
1097
+ self.logger.info(
1098
+ "The Matmul operation will use a custom kernel if possible or raise an \
1099
+ error since the codegen option is True."
1100
+ )
1101
+
1102
+ if (
1103
+ sp_utils.sparse_or_dense(a.tensor) == "sparse"
1104
+ and self.index_type_name in CUSPARSE_INDEX_TYPES
1105
+ and self.value_type_name in CUSPARSE_DTYPES
1106
+ and a.format_name in cusparse_utils.SUPPORTED_NAMED_FORMATS
1107
+ ):
1108
+ self.dispatch_init = BackendSupport.PROVISIONALLY_SUPPORTED
1109
+ else:
1110
+ self.dispatch_init = BackendSupport.NOT_SUPPORTED
1111
+ self.logger.debug(f"The dispatch viability of the Matmul operation is {self.dispatch_init.name}.")
1112
+
1113
+ if self.options.codegen and self.sparse_package != "nvmath":
1114
+ raise NotImplementedError("The code generation path is only available for UST operands.")
1115
+
1116
+ if (
1117
+ self.index_type_name in CODEGEN_INDEX_TYPES
1118
+ and self.value_type_name in CODEGEN_DTYPES
1119
+ and self.sparse_package == "nvmath"
1120
+ and a.format_name in CODEGEN_FORMATS
1121
+ and self.memory_space == "cuda"
1122
+ and not (self.spmm_traits.a_layout_traits.batch_broadcast and len(self.spmm_traits.b_layout_traits.batch_shape) > 0)
1123
+ and a.num_dimensions <= 4
1124
+ ):
1125
+ self.codegen_init = BackendSupport.PROVISIONALLY_SUPPORTED
1126
+ else:
1127
+ self.codegen_init = BackendSupport.NOT_SUPPORTED
1128
+ self.logger.debug(f"The codegen viability of the Matmul operation is {self.codegen_init.name}.")
1129
+
1130
+ if self.dispatch_init == BackendSupport.NOT_SUPPORTED and self.codegen_init == BackendSupport.NOT_SUPPORTED:
1131
+ raise NotImplementedError("The matrix multiplication is not currently supported for the specified operands.")
1132
+
1133
+ # The finalization of dispatch vs codegen, which we'll determine later in plan().
1134
+ self.dispatch: BackendSupport | None = None
1135
+ self.codegen: BackendSupport | None = None
1136
+
1137
+ # The actual API to use, which we'll determine later in plan().
1138
+ self.api = Api.NONE
1139
+
1140
+ # Capture operand extents, strides, and lazy conjugation for consistency check
1141
+ # when resetting operands.
1142
+ self.operand_extents = tuple(o.shape for o in self.operands)
1143
+ self.operand_strides = (None,) + tuple(o.strides for o in self.operands[1:])
1144
+
1145
+ # PyTorch currently doesn't support lazy conjugation for sparse tensors.
1146
+ assert not self.operands[0].values.is_conjugate, "Internal error."
1147
+ self.lazy_conjugation = (None, self.operands[1].is_conjugate, False)
1148
+
1149
+ # Library attributes.
1150
+ self.handle = None
1151
+ self.own_handle: bool | None = None
1152
+
1153
+ # Plan attributes.
1154
+ self.a_ifc = None # type: ignore
1155
+ self.b_ifc = None # type: ignore
1156
+ self.c_ifc = None # type: ignore
1157
+ # For dispatch to the SpMMOp API.
1158
+ self.mm_op_plan = None
1159
+
1160
+ self.mm_planned = False
1161
+
1162
+ # Workspace attributes.
1163
+ self.workspace_ptr: None | memory.MemoryPointer = None
1164
+ self.workspace_size = 0
1165
+ self.workspace_allocated_size = 0
1166
+ self.workspace_allocated_here = False
1167
+
1168
+ # Attributes to establish stream ordering.
1169
+ self.workspace_stream: cc.Stream | None = None
1170
+ self.last_compute_event: cc.Event | None = None
1171
+
1172
+ # Track whether the operands have been released.
1173
+ self.operands_released = False
1174
+
1175
+ self.valid_state = True
1176
+ self.logger.info("The Matmul operation has been created.")
1177
+
1178
+ def __enter__(self):
1179
+ return self
1180
+
1181
+ def __exit__(self, exc_type, exc_value, traceback):
1182
+ self.free()
1183
+
1184
+ def _check_valid_matmul(self, *args, **kwargs):
1185
+ """
1186
+ Check if the Matmul object is alive and well.
1187
+ """
1188
+ if not self.valid_state:
1189
+ raise InvalidMatmulState("The Matmul object cannot be used after resources are free'd")
1190
+
1191
+ def _check_valid_operands(self, *args, **kwargs):
1192
+ """
1193
+ Check if the operands are available for the operation.
1194
+ """
1195
+ what = kwargs["what"]
1196
+ if self.operands_released:
1197
+ raise RuntimeError(
1198
+ f"{what} cannot be performed if the operands have been set to None. Use reset_operands() to set the "
1199
+ f"desired input before performing the {what.lower()}."
1200
+ )
1201
+
1202
+ def _free_plan_resources(self, exception: Exception | None = None) -> bool:
1203
+ """
1204
+ Free resources allocated in planning.
1205
+ """
1206
+
1207
+ # Destroy plan.
1208
+ if self.mm_op_plan is not None:
1209
+ cusparse.sp_mm_op_destroy_plan(self.mm_op_plan)
1210
+ self.mm_op_plan = None
1211
+
1212
+ # Destroy matrix layouts.
1213
+ if self.a_ifc is not None and self.a_ifc.descriptor is not None:
1214
+ cusparse.destroy_sp_mat(self.a_ifc.descriptor)
1215
+ self.a_ifc.descriptor = None
1216
+ if self.b_ifc is not None and self.b_ifc.descriptor is not None:
1217
+ cusparse.destroy_dn_mat(self.b_ifc.descriptor)
1218
+ self.b_ifc.descriptor = None
1219
+ if self.c_ifc is not None and self.c_ifc.descriptor is not None:
1220
+ cusparse.destroy_dn_mat(self.c_ifc.descriptor)
1221
+ self.c_ifc.descriptor = None
1222
+
1223
+ self.mm_planned = False
1224
+ return True
1225
+
1226
+ def _check_planned(self, *args, **kwargs):
1227
+ what = kwargs["what"]
1228
+ if not self.mm_planned:
1229
+ raise RuntimeError(f"{what} cannot be performed before plan() has been called.")
1230
+
1231
+ def _free_workspace_memory(self, exception: Exception | None = None) -> bool:
1232
+ """
1233
+ Free workspace by releasing the MemoryPointer object.
1234
+ """
1235
+ if self.workspace_ptr is None:
1236
+ return True
1237
+
1238
+ self.workspace_ptr = None
1239
+ self.workspace_allocated_size = 0
1240
+ self.logger.debug("[_free_workspace_memory] The workspace has been released.")
1241
+
1242
+ return True
1243
+
1244
+ def _reset_workspace_allocation_tracking(self):
1245
+ """
1246
+ Reset workspace allocation tracking attributes to False at the end of the methods
1247
+ where workspace memory is potentially allocated. This is necessary to prevent any
1248
+ exceptions raised before method entry from using stale tracking values.
1249
+ """
1250
+ self.workspace_allocated_here = False
1251
+
1252
+ @utils.precondition(_check_valid_matmul)
1253
+ def _release_workspace_memory_perhaps(self, release_workspace):
1254
+ """
1255
+ Free workspace memory if it's larger than the specified limit.
1256
+ """
1257
+ if not release_workspace:
1258
+ return True
1259
+
1260
+ # Establish ordering wrt the computation and free workspace if requested.
1261
+ if self.last_compute_event is not None:
1262
+ self.workspace_stream.wait(self.last_compute_event)
1263
+ self.logger.debug("Established ordering with respect to the computation before releasing the workspace.")
1264
+ self.last_compute_event = None
1265
+
1266
+ self.logger.debug("[_release_workspace_memory_perhaps] The workspace memory will be released.")
1267
+ return self._free_workspace_memory()
1268
+
1269
+ def _release_workspace_memory_perhaps_wrapper(self, exception: Exception | None = None) -> bool:
1270
+ """
1271
+ This is used in @atomic.
1272
+ """
1273
+ self._release_workspace_memory_perhaps(release_workspace=self.workspace_allocated_here)
1274
+ self._reset_workspace_allocation_tracking()
1275
+ return True
1276
+
1277
+ @utils.precondition(_check_valid_matmul)
1278
+ @utils.precondition(_check_planned, "Workspace memory allocation")
1279
+ @utils.atomic(_free_workspace_memory, method=True)
1280
+ def _allocate_workspace_memory(self, stream_holder: utils.StreamHolder):
1281
+ """
1282
+ Allocate workspace memory using the specified allocator.
1283
+ """
1284
+
1285
+ assert self.workspace_size is not None, "Internal Error."
1286
+ assert self.workspace_allocated_here is False, "Internal Error."
1287
+
1288
+ if self.workspace_size == 0: # For performance, bypass allocator for workspace size == 0.
1289
+ self.workspace_ptr = memory.MemoryPointer(0, 0, finalizer=None)
1290
+ else:
1291
+ self.logger.debug("Allocating workspace for performing the matrix multiplication...")
1292
+ with utils.device_ctx(self.device_id), stream_holder.ctx:
1293
+ try:
1294
+ if isinstance(self.allocator, memory.BaseCUDAMemoryManagerAsync):
1295
+ self.workspace_ptr = self.allocator.memalloc_async(self.workspace_size, stream_holder.obj)
1296
+ else:
1297
+ self.workspace_ptr = self.allocator.memalloc(self.workspace_size)
1298
+ self.workspace_allocated_here = True
1299
+ except TypeError as e:
1300
+ message = (
1301
+ "The method 'memalloc' in the allocator object must conform to the interface in the "
1302
+ "'BaseCUDAMemoryManager' protocol."
1303
+ )
1304
+ raise TypeError(message) from e
1305
+
1306
+ self.workspace_allocated_size = self.workspace_size
1307
+ self.workspace_stream = stream_holder.obj
1308
+ self.logger.debug(
1309
+ f"Finished allocating device workspace of size {formatters.MemoryStr(self.workspace_size)} in the context "
1310
+ f"of stream {self.workspace_stream}."
1311
+ )
1312
+
1313
+ def _allocate_workspace_memory_perhaps(self, stream_holder: utils.StreamHolder):
1314
+ """
1315
+ Allocate workspace memory using the specified allocator, if it hasn't already been
1316
+ done.
1317
+ """
1318
+
1319
+ if self.workspace_ptr is not None and self.workspace_allocated_size >= self.workspace_size:
1320
+ return
1321
+
1322
+ return self._allocate_workspace_memory(stream_holder)
1323
+
1324
+ @utils.precondition(_check_valid_matmul)
1325
+ @utils.precondition(_check_valid_operands, "Planning")
1326
+ @utils.atomic(_free_plan_resources, method=True)
1327
+ def plan(
1328
+ self, *, prologs=None, epilog=None, semiring=None, compute_capability=None, stream: utils.AnyStream | int | None = None
1329
+ ):
1330
+ r"""
1331
+ Plan the sparse matrix multiplication operation, considering the prolog(s), epilog, and
1332
+ semiring operations.
1333
+
1334
+ Args:
1335
+ prologs: {prologs}
1336
+
1337
+ epilog: {epilog}
1338
+
1339
+ semiring: {semiring}
1340
+
1341
+ compute_capability: {compute_capability}
1342
+
1343
+ stream: {stream}
1344
+
1345
+ Examples:
1346
+
1347
+ We'll see how to use prologs specify the SpMM
1348
+ :math:`c := 3.14 \, sin(a) \, @ \, b + c`, where the elementwise transformations
1349
+ are fully-fused into the matrix multiplication.
1350
+
1351
+ >>> import math
1352
+ >>> import cupy as cp
1353
+ >>> import cupyx.scipy.sparse as sp
1354
+ >>> import nvmath
1355
+
1356
+
1357
+ Create a sparse operand in DIA format and view it as UST.
1358
+
1359
+ >>> n = 4
1360
+ >>> values = cp.array(
1361
+ ... [[0.0, 2.0, 3.0, 4.0], [10.0, 20.0, 30.0, 40.0], [-1.0, -2.0, -3.0, 0.0]]
1362
+ ... )
1363
+ >>> offsets = cp.array([1, 0, -1], dtype=cp.int32)
1364
+ >>> a = sp.dia_matrix((values, offsets), shape=(n, n))
1365
+ >>> a = nvmath.sparse.ust.Tensor.from_package(a)
1366
+
1367
+ Dense ``b`` and ``c``, also viewed as UST.
1368
+
1369
+ >>> b = cp.ones((n, n))
1370
+ >>> b = nvmath.sparse.ust.Tensor.from_package(b)
1371
+ >>> c = cp.zeros((n, n))
1372
+ >>> c = nvmath.sparse.ust.Tensor.from_package(c)
1373
+
1374
+ Define the prolog for ``a``, and compile to LTO-IR using the helper (or
1375
+ your own compiler).
1376
+
1377
+ >>> def transform_a(a):
1378
+ ... return 3.14 * math.sin(a)
1379
+ >>>
1380
+ >>> prolog_a = nvmath.sparse.compile_matmul_prolog(
1381
+ ... transform_a, operand_label="a", dtype="float64"
1382
+ ... )
1383
+
1384
+ Create, plan, and execute the SpMM.
1385
+
1386
+ >>> with nvmath.sparse.Matmul(a, b, c, beta=1.0) as mm:
1387
+ ... # Plan the SpMM operation with the prologs.
1388
+ ... mm.plan(prologs={{"a": prolog_a}})
1389
+ ...
1390
+ ... # Execute the SpMM.
1391
+ ... r = mm.execute()
1392
+
1393
+ Further examples can be found in the `nvmath/examples/sparse/generic/matmul
1394
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/sparse/generic/matmul>`_
1395
+ directory.
1396
+ """ # noqa: W505
1397
+
1398
+ # Set self.dispatch and self.codegen from the corresponding initial values. This
1399
+ # is to enable replanning.
1400
+ self.dispatch = self.dispatch_init
1401
+ self.codegen = self.codegen_init
1402
+
1403
+ self.logger.info("= PLANNING PHASE =")
1404
+
1405
+ # Create stream holder.
1406
+ stream_holder = utils.get_or_create_stream(self.device_id, stream, self.dense_package)
1407
+ self.logger.info(f"The specified stream for the Matmul plan is {stream_holder.obj}.")
1408
+
1409
+ if prologs is not None:
1410
+ if not isinstance(prologs, Mapping):
1411
+ raise TypeError(
1412
+ f"The prologs argument must be a mapping from operand name ('a', 'b', 'c') \
1413
+ to the LTO-IR buffers generated by the compile helper functions. The specified type is {type(prologs)}."
1414
+ )
1415
+ elif not prologs:
1416
+ # Treat empty prologs as if it were None.
1417
+ prologs = None
1418
+ self.logger.warning(f"Matmul plan(): The specified prologs {prologs} is ignored since it is an empty mapping.")
1419
+
1420
+ if semiring is not None:
1421
+ if not isinstance(semiring, Mapping):
1422
+ raise TypeError(
1423
+ f"The semiring argument must be a mapping from operation name ('add', 'mul') \
1424
+ to the LTO-IR buffers generated by the compile helper functions. The specified type is {type(semiring)}."
1425
+ )
1426
+
1427
+ if "add" not in semiring or "mul" not in semiring:
1428
+ raise ValueError(
1429
+ f"Both 'add' and 'mul' operations should be specified to define the semiring \
1430
+ structure: specified operations = {semiring.keys()}."
1431
+ )
1432
+ self.logger.info("The user-specified semiring operations will be used instead of the default.")
1433
+
1434
+ # Check if it still can be codegen'd. Provisional code generation becomes final.
1435
+ if self.sparse_package == "nvmath" and (
1436
+ (
1437
+ self.alpha[0] != 1.0
1438
+ or self.beta[0] != 1.0
1439
+ or self.qualifiers[0]["is_conjugate"]
1440
+ or self.qualifiers[1]["is_conjugate"]
1441
+ or self.compute_type_name not in CODEGEN_COMPUTE_TYPES
1442
+ )
1443
+ and prologs is not None
1444
+ ):
1445
+ self.codegen = BackendSupport.NOT_SUPPORTED
1446
+ self.logger.debug(
1447
+ f"The Matmul operation is currently not supported for the code generation path: \
1448
+ alpha = {self.alpha[0]}, beta = {self.beta[0]}, a_conjugate = {bool(self.qualifiers[0]['is_conjugate'])}, b_conjugate = \
1449
+ {bool(self.qualifiers[1]['is_conjugate'])}, compute type = {self.compute_type_name}"
1450
+ )
1451
+
1452
+ # Check if it still can be dispatched and if it can which function to dispatch
1453
+ # it to. Provisional dispatch becomes final.
1454
+ if (
1455
+ (self.qualifiers[0]["is_conjugate"] and not self.qualifiers[0]["is_transpose"])
1456
+ or (self.qualifiers[1]["is_conjugate"] and not self.qualifiers[1]["is_transpose"])
1457
+ or self.compute_type_name not in CUSPARSE_COMPUTE_TYPES
1458
+ or prologs is not None
1459
+ ):
1460
+ self.dispatch = BackendSupport.NOT_SUPPORTED
1461
+ self.logger.debug(
1462
+ f"The Matmul operation is currently not supported for dispatch: a_transpose = \
1463
+ {bool(self.qualifiers[0]['is_transpose'])}, a_conjugate = {bool(self.qualifiers[0]['is_conjugate'])}, \
1464
+ b_transpose = {bool(self.qualifiers[1]['is_transpose'])}, b_conjugate = {bool(self.qualifiers[1]['is_conjugate'])}, \
1465
+ compute type = {self.compute_type_name}, prologs specified = {prologs is not None}."
1466
+ )
1467
+
1468
+ # User-specified code generation, if true, takes precedence.
1469
+ if self.codegen == BackendSupport.NOT_SUPPORTED and self.options.codegen:
1470
+ message = ""
1471
+ if self.qualifiers[0]["is_conjugate"] or self.qualifiers[1]["is_conjugate"]:
1472
+ message += "The conjugate operation cannot be specified if prologs are provided \
1473
+ for the code generation path."
1474
+ if self.alpha[0] != 1.0 or self.beta[0] != 1.0:
1475
+ message += "The scalars alpha and/or beta cannot be specified if prologs are provided \
1476
+ for the code generation path."
1477
+ raise NotImplementedError(
1478
+ f"The code generation option is requested (options.codegen=True) but the code generation path is \
1479
+ currently not supported.\n{message}"
1480
+ )
1481
+
1482
+ # User-specified codegen option > dispatch > codegen.
1483
+ if self.options.codegen:
1484
+ if self.codegen != BackendSupport.PROVISIONALLY_SUPPORTED:
1485
+ raise NotImplementedError(
1486
+ "The matrix multiplication is not currently supported for the code generation \
1487
+ path, and options.codegen is set to True ."
1488
+ )
1489
+ self.codegen = BackendSupport.SUPPORTED
1490
+ self.api = Api.CODEGEN
1491
+
1492
+ if not self.options.codegen and self.dispatch == BackendSupport.PROVISIONALLY_SUPPORTED:
1493
+ if semiring is None and epilog is None:
1494
+ self.logger.debug("MM plan (dispatch): neither semiring or epilog is specified.")
1495
+ self.api = Api.MM
1496
+ self.dispatch = BackendSupport.SUPPORTED
1497
+ elif semiring is not None and epilog is not None:
1498
+ self.logger.debug(
1499
+ "MM plan (dispatch): both semiring and epilog are specified. No support due \
1500
+ to 2-argument epilog for now."
1501
+ )
1502
+ self.api = Api.MM_OP
1503
+ self.dispatch = BackendSupport.NOT_SUPPORTED
1504
+ else:
1505
+ self.logger.debug(
1506
+ f"MM plan (dispatch): exactly one of semiring {semiring is not None} or epilog \
1507
+ {epilog is not None} is specified."
1508
+ )
1509
+ self.dispatch = BackendSupport.NOT_SUPPORTED
1510
+
1511
+ if self.dispatch == BackendSupport.NOT_SUPPORTED and self.codegen == BackendSupport.PROVISIONALLY_SUPPORTED:
1512
+ self.codegen = BackendSupport.SUPPORTED
1513
+ self.api = Api.CODEGEN
1514
+
1515
+ if self.dispatch == BackendSupport.NOT_SUPPORTED and self.codegen == BackendSupport.NOT_SUPPORTED:
1516
+ raise NotImplementedError("The matrix multiplication is not currently supported for the specified operands.")
1517
+
1518
+ if self.api == Api.CODEGEN:
1519
+ assert self.codegen == BackendSupport.SUPPORTED, "Internal error."
1520
+ if semiring is not None and "atomic_add" not in semiring:
1521
+ raise ValueError(
1522
+ "The 'atomic_add' operation needs to be specified in the semiring for the \
1523
+ code generation path."
1524
+ )
1525
+ self.logger.info("The Matmul kernel will be generated by the UST library and compiled JIT.")
1526
+ elif self.api in [Api.MM, Api.MM_OP]:
1527
+ assert self.dispatch == BackendSupport.SUPPORTED, "Internal error."
1528
+ self.logger.info("The Matmul operation will be dispatched to the cuSPARSE library.")
1529
+ else:
1530
+ raise AssertionError("Internal error: API not available.")
1531
+
1532
+ if self.api == Api.CODEGEN:
1533
+ from nvmath.sparse.ust._emitter import populate_matmul_parameters
1534
+
1535
+ assert self.memory_space == "cuda", "Internal error."
1536
+
1537
+ if self.beta[0] != 1.0:
1538
+ raise NotImplementedError("The code generation path is not supported for beta != 1.")
1539
+
1540
+ # Create prolog LTO-IR for alpha, beta, and conjugate operation.
1541
+ if prologs is None:
1542
+ prologs = {}
1543
+
1544
+ if self.alpha[0] != 1.0 or self.qualifiers[0]["is_conjugate"]:
1545
+ prolog_a = default_prolog(self.alpha[0], is_conjugate=self.qualifiers[0]["is_conjugate"])
1546
+ prolog_a = compile_prolog(
1547
+ prolog_a, operand_label="a", dtype=self.compute_type_name, compute_capability=compute_capability
1548
+ )
1549
+ prologs["a"] = prolog_a
1550
+
1551
+ if self.qualifiers[1]["is_conjugate"]:
1552
+ prolog_b = default_prolog(1.0, is_conjugate=self.qualifiers[1]["is_conjugate"])
1553
+ prolog_b = compile_prolog(
1554
+ prolog_b, operand_label="b", dtype=self.compute_type_name, compute_capability=compute_capability
1555
+ )
1556
+ prologs["b"] = prolog_b
1557
+
1558
+ a, b, c = self.a.tensor, *self.ust_operands
1559
+ self.kernel = KERNEL_CACHE.generate_matmul(
1560
+ a,
1561
+ b,
1562
+ c,
1563
+ compute_type=self.compute_type_name,
1564
+ prologs=prologs,
1565
+ epilog=epilog,
1566
+ semiring=semiring,
1567
+ transpose_a=self.qualifiers[0]["is_transpose"] > 0,
1568
+ transpose_b=self.qualifiers[1]["is_transpose"] > 0,
1569
+ )
1570
+
1571
+ self.kernel_parameters, self.kernel_problem_size = populate_matmul_parameters(a, b, c, dense_bc=True)
1572
+
1573
+ self.mm_planned = True
1574
+ self.logger.info("The Matmul planning is complete for the code generation path.")
1575
+ return
1576
+
1577
+ # Capture epilog and semiring LTO-IR if specified.
1578
+ assert (semiring is None and epilog is None) ^ (semiring is not None and epilog is not None), "Internal error."
1579
+ if semiring is not None and epilog is not None:
1580
+ if "add" not in semiring or "mul" not in semiring:
1581
+ raise ValueError("The semiring option must provide LTO-IR code for both 'add' and 'mul' operations.")
1582
+
1583
+ if not isinstance(semiring["add"], bytes) or not isinstance(semiring["mul"], bytes):
1584
+ raise ValueError("The LTO-IR code for 'add' and 'mul' semiring operation must be bytestring objects.")
1585
+
1586
+ # Create handle.
1587
+ with utils.device_ctx(self.device_id):
1588
+ self.own_handle = True
1589
+ self.handle = cusparse.create()
1590
+ self.logger.info(f"The library handle has been created: {self.handle}.")
1591
+
1592
+ # Set stream.
1593
+ cusparse.set_stream(self.handle, stream_holder.ptr)
1594
+
1595
+ if (v := cusparse.get_version(self.handle)) < 12501 and self.a.format_name == "CSC" and self.spmm_traits.N == 1:
1596
+ raise NotImplementedError(f"This version ({v}) of cuSPARSE does not support CSC with n == 1.")
1597
+
1598
+ # Set the pointer mode.
1599
+ cusparse.set_pointer_mode(self.handle, cusparse.PointerMode.HOST)
1600
+ self.a_ifc = getattr(cusparse_utils, self.a.format_name + "Ifc")(self.a, self.spmm_traits.a_layout_traits)
1601
+ self.b_ifc = cusparse_utils.DenseMatrixIfc(self.b, self.spmm_traits.b_layout_traits) # type: ignore[assignment]
1602
+ self.c_ifc = cusparse_utils.DenseMatrixIfc(self.c, self.spmm_traits.c_layout_traits) # type: ignore[assignment]
1603
+
1604
+ # Transpose
1605
+ self.op_a = _get_cusparse_op_code(self.qualifiers[0])
1606
+ self.op_b = _get_cusparse_op_code(self.qualifiers[1])
1607
+
1608
+ # Create matrix descriptors.
1609
+ self.a_ifc.create() # type: ignore[attr-defined]
1610
+ self.b_ifc.create() # type: ignore[attr-defined]
1611
+ self.c_ifc.create() # type: ignore[attr-defined]
1612
+
1613
+ if self.api == Api.MM:
1614
+ # Compute workspace.
1615
+ self.workspace_size = cusparse.sp_mm_buffer_size(
1616
+ self.handle,
1617
+ self.op_a,
1618
+ self.op_b,
1619
+ self.alpha.ctypes.data,
1620
+ self.a_ifc.descriptor, # type: ignore[attr-defined]
1621
+ self.b_ifc.descriptor, # type: ignore[attr-defined]
1622
+ self.beta.ctypes.data,
1623
+ self.c_ifc.descriptor, # type: ignore[attr-defined]
1624
+ self.compute_type,
1625
+ cusparse.SpMMAlg.DEFAULT,
1626
+ )
1627
+ elif self.api == Api.MM_OP:
1628
+ # TODO: Fix `sp_mm_op_create_plan` to accept bytes objects.
1629
+ add_buffer = np.frombuffer(semiring["add"], dtype=np.int8)
1630
+ mul_buffer = np.frombuffer(semiring["mul"], dtype=np.int8)
1631
+ epilog_buffer = np.frombuffer(epilog, dtype=np.int8)
1632
+
1633
+ # Plan and compute workspace.
1634
+ with utils.device_ctx(self.device_id):
1635
+ self.mm_op_plan, self.workspace_size = cusparse.sp_mm_op_create_plan(
1636
+ self.handle,
1637
+ self.op_a,
1638
+ self.op_b,
1639
+ self.a_ifc.descriptor, # type: ignore[attr-defined]
1640
+ self.b_ifc.descriptor, # type: ignore[attr-defined]
1641
+ self.c_ifc.descriptor, # type: ignore[attr-defined]
1642
+ self.compute_type,
1643
+ cusparse.SpMMAlg.DEFAULT,
1644
+ add_buffer.ctypes.data,
1645
+ len(semiring["add"]),
1646
+ mul_buffer.ctypes.data,
1647
+ len(semiring["mul"]),
1648
+ epilog_buffer.ctypes.data,
1649
+ len(epilog),
1650
+ )
1651
+ else:
1652
+ raise AssertionError("Internal error.")
1653
+
1654
+ self.logger.info(f"The memory limit is {formatters.MemoryStr(self.memory_limit)}.")
1655
+ if self.workspace_size > self.memory_limit:
1656
+ raise RuntimeError(
1657
+ f"The memory required for the computation is {self.workspace_size} \
1658
+ ({formatters.MemoryStr(self.workspace_size)}), while the specified memory limit is {self.memory_limit} \
1659
+ ({formatters.MemoryStr(self.memory_limit)})."
1660
+ )
1661
+
1662
+ # TODO: preprocessing based on format and algorithm.
1663
+
1664
+ self.mm_planned = True
1665
+ self.logger.info("The Matmul planning is complete for the library dispatch path.")
1666
+
1667
+ @utils.precondition(_check_valid_matmul)
1668
+ def _check_and_set_dense_operand(self, operand, operand_name, operand_index, operand_ifc, stream_holder):
1669
+ assert self.operands is not None, "Internal Error."
1670
+ assert 0 < operand_index < 3, "Internal Error."
1671
+
1672
+ package = utils.infer_object_package(operand.tensor)
1673
+
1674
+ # Conjugate flag of the provided operands must match the original qualifiers
1675
+ if self.lazy_conjugation[operand_index] != operand.is_conjugate:
1676
+ raise ValueError(f"The provided operand {operand_name} has different conjugate flag than the original operand")
1677
+
1678
+ memory_space = operand.device
1679
+ if memory_space != self.memory_space:
1680
+ raise TypeError(
1681
+ f"The memory space for '{operand_name}' ({memory_space}) doesn't match the original one ({self.memory_space})."
1682
+ )
1683
+
1684
+ device_id = operand.device_id
1685
+ if device_id != "cpu" and device_id != self.device_id:
1686
+ raise TypeError(
1687
+ f"The device id for '{operand_name}' ({device_id}) doesn't match the original one ({self.device_id})."
1688
+ )
1689
+
1690
+ value_type = operand.dtype
1691
+ shape = operand.shape
1692
+ strides = operand.strides
1693
+
1694
+ # Handle cupy <> numpy asymmetry.
1695
+ if package == "numpy":
1696
+ package = "cuda"
1697
+
1698
+ # Check package, device ID, shape, strides, and dtype.
1699
+ if package != self.dense_package:
1700
+ message = f"The package for '{operand_name}' ({package}) doesn't match the original one ({self.dense_package})."
1701
+ raise TypeError(message)
1702
+
1703
+ if value_type != self.value_type_name:
1704
+ message = f"The dtype for '{operand_name}' ({value_type}) doesn't match the original one ({self.value_type_name})."
1705
+ raise TypeError(message)
1706
+
1707
+ required_shape = self.operand_extents[operand_index]
1708
+ if shape != required_shape:
1709
+ message = f"The shape of '{operand_name}' ({shape}) doesn't match the original one ({required_shape})."
1710
+ raise TypeError(message)
1711
+
1712
+ required_strides = self.operand_strides[operand_index]
1713
+ if strides != required_strides:
1714
+ message = f"The strides of '{operand_name}' ({strides}) don't match the original one ({required_strides})."
1715
+ raise TypeError(message)
1716
+
1717
+ if device_id == "cpu":
1718
+ # Copy operand in the original buffer if it exists or create a new one
1719
+ # otherwise.
1720
+ o = getattr(self, operand_name)
1721
+ if o is None:
1722
+ o = operand.to(self.device_id, stream_holder)
1723
+ else:
1724
+ o.copy_(operand, stream_holder)
1725
+ else:
1726
+ o = operand
1727
+ setattr(self, operand_name, o)
1728
+
1729
+ # Update alias.
1730
+ self.operands[operand_index] = o
1731
+
1732
+ # Update the pointer values for dispatch. For codegen update it in
1733
+ # release_operands since individual operand pointers can't be updated.
1734
+ if self.api in [Api.MM, Api.MM_OP]:
1735
+ operand_ifc.update(o)
1736
+ elif self.api != Api.CODEGEN:
1737
+ raise AssertionError(f"Internal error: unsupported backend {self.api}.")
1738
+
1739
+ @utils.precondition(_check_valid_matmul)
1740
+ def reset_operands(
1741
+ self,
1742
+ *,
1743
+ a=None,
1744
+ b=None,
1745
+ c=None,
1746
+ alpha=None,
1747
+ beta=None,
1748
+ stream: utils.AnyStream | int | None = None,
1749
+ ):
1750
+ """
1751
+ Reset one or more operands held by this :class:`Matmul` instance.
1752
+ Only the operands explicitly passed are updated; omitted operands retain
1753
+ their current values.
1754
+
1755
+ This method will perform various checks on the new operands to make sure:
1756
+
1757
+ - The shapes, index and data types match those of the old ones.
1758
+
1759
+ - The packages that the operands belong to match those of the old ones.
1760
+
1761
+ - If input tensors are on GPU, the device must match.
1762
+
1763
+ Args:
1764
+ a: {a}
1765
+
1766
+ b: {b}
1767
+
1768
+ c: {c}
1769
+
1770
+ alpha: {alpha}
1771
+
1772
+ beta: {beta}
1773
+
1774
+ stream: {stream}
1775
+
1776
+ Examples:
1777
+
1778
+
1779
+ >>> import torch
1780
+ >>> import nvmath
1781
+
1782
+ Prepare sample input data.
1783
+
1784
+ >>> device_id = 0
1785
+ >>> dtype = torch.float64
1786
+ >>> m, n, k = 4, 2, 8
1787
+
1788
+ Create a torch CSR tensor.
1789
+
1790
+ >>> a = torch.ones(m, k, dtype=dtype, device=device_id)
1791
+ >>> a = a.to_sparse_csr()
1792
+
1793
+ Dense ``b`` and ``c``.
1794
+
1795
+ >>> b = torch.ones(k, n, dtype=dtype, device=device_id)
1796
+ >>> c = torch.ones(m, n, dtype=dtype, device=device_id)
1797
+
1798
+ Create a stateful object that specifies the operation
1799
+ :math:`c := \\alpha \\, a \\, @ \\, b + \\beta \\, c`.
1800
+
1801
+ >>> alpha, beta = 1.2, 2.4
1802
+ >>> with nvmath.sparse.Matmul(a, b, c, alpha=alpha, beta=beta) as mm:
1803
+ ... # Plan the operation.
1804
+ ... mm.plan()
1805
+ ...
1806
+ ... # The first execution.
1807
+ ... r = mm.execute()
1808
+ ...
1809
+ ... # The operands can be reset in-place using the array library.
1810
+ ... # Note that since `c` has been updated in-place, it also needs to
1811
+ ... # be reset unless accumulating into it is desired.
1812
+ ... b *= 3.14
1813
+ ... c[:] = 1.0
1814
+ ...
1815
+ ... # Execute the operation with updated `b`.
1816
+ ... r = mm.execute()
1817
+ ...
1818
+ ... # The operands can also be reset using the reset_operand[_unchecked]
1819
+ ... # methods. This is needed when the memory space doesn't match the
1820
+ ... # execution space or if the operands have not been updated in-place.
1821
+ ... # Any operands that are not reset retain their prior values.
1822
+ ...
1823
+ ... # The sparse matrix needs to be compatible (have the same sparse
1824
+ ... # format, shape, dtype, NNZ etc.).
1825
+ ... a = 2.718 * torch.ones(m, k, dtype=dtype, device=device_id)
1826
+ ... a = a.to_sparse_csr()
1827
+ ...
1828
+ ... c = 6.28 * torch.ones(m, n, dtype=dtype, device=device_id)
1829
+ ...
1830
+ ... mm.reset_operands(a=a, c=c)
1831
+ ...
1832
+ ... # Execute the operation with the reset `a` and `c`, `b` retains
1833
+ ... # its previous value.
1834
+ ... r = mm.execute()
1835
+
1836
+ For more details, please refer to `the reset operand examples here
1837
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/sparse/generic/matmul/>`_.
1838
+ """ # noqa: W505
1839
+
1840
+ self.logger.info("Resetting operands...")
1841
+
1842
+ # If operands have been released, all required operands must be provided
1843
+ if self.operands_released and (a is None or b is None or c is None):
1844
+ raise ValueError("After release_operands(), 'a', 'b', and 'c' must be provided to reset_operands().")
1845
+
1846
+ if a is None and b is None and c is None and alpha is None and beta is None:
1847
+ msg = "Calling reset_operands() with all arguments set to None is not allowed. "
1848
+ msg += "Use release_operands() to release all operands."
1849
+ raise ValueError(msg)
1850
+
1851
+ # Update alpha.
1852
+ if alpha is not None:
1853
+ if self.api == Api.CODEGEN:
1854
+ raise NotImplementedError("The value of `alpha` cannot be currently reset for the code generation path.")
1855
+
1856
+ try:
1857
+ self.alpha[0] = alpha
1858
+ except (ValueError, TypeError) as e:
1859
+ raise ValueError(
1860
+ f"The value provided for alpha {alpha} is not convertible to dtype '{self.alpha.dtype}'."
1861
+ ) from e
1862
+ self.logger.info("The factor `alpha` has been reset to %s.", alpha)
1863
+
1864
+ # Update beta.
1865
+ if beta is not None:
1866
+ if self.api == Api.CODEGEN:
1867
+ raise NotImplementedError("The value of `beta` cannot be currently reset for the code generation path.")
1868
+
1869
+ if self.num_operands == 2:
1870
+ self.logger.warning(f"Matmul: The provided beta value {beta} is ignored since operand C is not specified.")
1871
+ else:
1872
+ try:
1873
+ self.beta[0] = beta
1874
+ except (ValueError, TypeError) as e:
1875
+ raise ValueError(
1876
+ f"The value provided for beta {beta} is not convertible to dtype '{self.beta.dtype}'."
1877
+ ) from e
1878
+ self.logger.info("The factor `beta` has been reset to %s.", beta)
1879
+
1880
+ stream_holder = utils.get_or_create_stream(self.device_id, stream, self.dense_package)
1881
+
1882
+ # Update a.
1883
+ if a is not None:
1884
+ # Wrap A.
1885
+ try:
1886
+ a = sp_utils.wrap_sparse_operands(a)
1887
+ except Exception as e:
1888
+ raise TypeError(
1889
+ "The operand 'a' must be an N-D sparse array/tensor tensor from one of the supported \
1890
+ packages: nvmath, CuPy, PyTorch, or SciPy."
1891
+ ) from e
1892
+
1893
+ sparse_package = utils.infer_object_package(a.tensor)
1894
+ device_id = a.device_id
1895
+ memory_space = a.device
1896
+ value_type = a.dtype
1897
+ index_type = a.index_type
1898
+
1899
+ shape = a.shape
1900
+ a_shape = self.operand_extents[0]
1901
+
1902
+ # Check package, device ID, dtype, and index type.
1903
+ if sparse_package != self.sparse_package:
1904
+ raise TypeError(
1905
+ f"The package for 'a' ({sparse_package}) doesn't match the original one ({self.sparse_package})."
1906
+ )
1907
+
1908
+ if sparse_package == "nvmath" and a.dense_tensorholder_type.name != self.dense_package:
1909
+ raise TypeError(
1910
+ f"The UST operand 'a' representation package ({a.dense_tensorholder_type.name}) doesn't match the "
1911
+ f"original one ({self.dense_package})."
1912
+ )
1913
+
1914
+ if memory_space != self.memory_space:
1915
+ raise TypeError(
1916
+ f"The memory space for 'a' ({memory_space}) doesn't match the original one ({self.memory_space})."
1917
+ )
1918
+
1919
+ if device_id != "cpu" and device_id != self.device_id:
1920
+ raise TypeError(f"The device id for 'a' ({device_id}) doesn't match the original one ({self.device_id}).")
1921
+
1922
+ if value_type != self.value_type_name:
1923
+ raise TypeError(f"The dtype for 'a' ({value_type}) doesn't match the original one ({self.value_type_name}).")
1924
+
1925
+ if index_type != self.index_type_name:
1926
+ raise TypeError(
1927
+ f"The index type for 'a' ({index_type}) doesn't match the original one ({self.index_type_name})."
1928
+ )
1929
+
1930
+ if shape != a_shape:
1931
+ raise TypeError(f"The shape of 'a' ({shape}) doesn't match the original one ({a_shape}).")
1932
+
1933
+ # Copy operand in the original buffer if it exists or create a new one.
1934
+ if device_id == "cpu":
1935
+ if self.a is not None:
1936
+ self.a.copy_(a, stream_holder)
1937
+ else:
1938
+ self.a = a.to(self.device_id, stream_holder)
1939
+ else:
1940
+ self.a = a
1941
+
1942
+ # Update alias.
1943
+ self.operands[0] = self.a
1944
+
1945
+ # Update the pointer values in the existing device buffers.
1946
+ if self.api in [Api.MM, Api.MM_OP]:
1947
+ self.a_ifc.update(self.a) # type: ignore[attr-defined]
1948
+ elif self.api != Api.CODEGEN:
1949
+ raise AssertionError(f"Internal error: unsupported backend {self.api}.")
1950
+
1951
+ self.logger.info("The operand `a` has been reset.")
1952
+
1953
+ # TODO: unify treatment of TensorHolder and UST for dense operands.
1954
+ # Update `b`.
1955
+ if b is not None:
1956
+ if sp_utils.sparse_or_dense(b) != "dense":
1957
+ raise ValueError(f"The operand `b` {type(b)} must be dense.")
1958
+ package = utils.infer_object_package(b)
1959
+ if (package == "nvmath") != (self.sparse_package == "nvmath"):
1960
+ raise TypeError(f"The package for 'b' ({package}) doesn't match the original one ({self.sparse_package}).")
1961
+ b_wrapped = tensor_wrapper.wrap_operand(b.wrapped_operand.tensor if package == "nvmath" else b)
1962
+ # Check and update self.operands and the alias.
1963
+ self._check_and_set_dense_operand(b_wrapped, "b", 1, self.b_ifc, stream_holder)
1964
+ if package == "nvmath":
1965
+ if self.memory_space == "cuda":
1966
+ self.ust_operands[0] = b
1967
+ else:
1968
+ self.ust_operands[0] = UST.from_package(self.b.tensor, stream_holder.external)
1969
+ self.logger.info("The operand `b` has been reset.")
1970
+
1971
+ # Update `c`.
1972
+ if c is not None:
1973
+ if sp_utils.sparse_or_dense(c) != "dense":
1974
+ raise ValueError(f"The operand `c` {type(c)} must be dense.")
1975
+ package = utils.infer_object_package(c)
1976
+ if (package == "nvmath") != (self.sparse_package == "nvmath"):
1977
+ raise TypeError(f"The package for 'c' ({package}) doesn't match the original one ({self.sparse_package}).")
1978
+ c_wrapped = tensor_wrapper.wrap_operand(c.wrapped_operand.tensor if package == "nvmath" else c)
1979
+ # Check and update self.operands and the alias.
1980
+ self._check_and_set_dense_operand(c_wrapped, "c", 2, self.c_ifc, stream_holder)
1981
+ if package == "nvmath":
1982
+ if self.memory_space == "cuda":
1983
+ self.ust_operands[1] = c
1984
+ else:
1985
+ self.ust_operands[1] = UST.from_package(self.c.tensor, stream_holder.external)
1986
+ if self.memory_space == "cpu" and self.inplace:
1987
+ self.cpu_c_ref = c if package == "nvmath" else c_wrapped
1988
+ self.logger.info("The operand `c` has been reset.")
1989
+
1990
+ # For codegen, all pointers will have to be reset since individual pointers
1991
+ # can't be updated.
1992
+ if (a is not None or b is not None or c is not None) and self.api == Api.CODEGEN:
1993
+ from nvmath.sparse.ust._emitter import populate_matmul_parameters
1994
+
1995
+ assert self.memory_space == "cuda", "Internal error."
1996
+
1997
+ a, b, c = self.a.tensor, *self.ust_operands
1998
+ self.kernel_parameters, self.kernel_problem_size = populate_matmul_parameters(a, b, c, dense_bc=True)
1999
+
2000
+ # Update release operands state.
2001
+ self.operands_released = False
2002
+
2003
+ def reset_operands_unchecked(
2004
+ self,
2005
+ *,
2006
+ a=None,
2007
+ b=None,
2008
+ c=None,
2009
+ alpha=None,
2010
+ beta=None,
2011
+ stream: utils.AnyStream | int | None = None,
2012
+ ):
2013
+ """
2014
+ {reset_operands_unchecked}
2015
+ """
2016
+
2017
+ if alpha is not None:
2018
+ self.alpha[0] = alpha
2019
+
2020
+ if beta is not None:
2021
+ self.beta[0] = beta
2022
+
2023
+ if self.memory_space == "cuda":
2024
+ if a is not None:
2025
+ self.operands[0].reset_unchecked(a)
2026
+ self.a = self.operands[0]
2027
+
2028
+ if self.api in [Api.MM, Api.MM_OP]:
2029
+ self.a_ifc.update(self.a) # type: ignore[attr-defined]
2030
+
2031
+ if b is not None:
2032
+ if self.ust_operands:
2033
+ self.ust_operands[0] = b
2034
+ # Recall `b` is the wrapped dense non-UST operand.
2035
+ self.operands[1].tensor = b.wrapped_operand.tensor
2036
+ else:
2037
+ self.operands[1].tensor = b
2038
+
2039
+ # Update the alias.
2040
+ self.b = self.operands[1]
2041
+
2042
+ if self.api in [Api.MM, Api.MM_OP]:
2043
+ self.b_ifc.update(self.b) # type: ignore[attr-defined]
2044
+
2045
+ if c is not None:
2046
+ if self.ust_operands:
2047
+ self.ust_operands[1] = c
2048
+ # Recall `c` is the wrapped dense non-UST operand.
2049
+ self.operands[2].tensor = c.wrapped_operand.tensor
2050
+ else:
2051
+ self.operands[2].tensor = c
2052
+
2053
+ # Update the alias.
2054
+ self.c = self.operands[2]
2055
+
2056
+ if self.api in [Api.MM, Api.MM_OP]:
2057
+ self.c_ifc.update(self.c) # type: ignore[attr-defined]
2058
+
2059
+ else:
2060
+ # Handle CPU memory space.
2061
+
2062
+ stream_holder = utils.get_or_create_stream(self.device_id, stream, self.dense_package)
2063
+
2064
+ if a is not None:
2065
+ a_wrapped = self.a_wrapper_type.create_from_tensor(a, attr_name_map=self.a_attr_name_map)
2066
+ self.a = self.operands[0] = a_wrapped.to(self.device_id, stream_holder)
2067
+
2068
+ if self.api in [Api.MM, Api.MM_OP]:
2069
+ self.a_ifc.update(self.a) # type: ignore[attr-defined]
2070
+
2071
+ # We can't use the dense wrapper type `self.[b,c].__class__` when going across
2072
+ # memory spaces due to asymmetries (NumPyTensor <> NDBufferTensor, etc).
2073
+ if b is not None:
2074
+ b_wrapped = tensor_wrapper.wrap_operand(b.wrapped_operand.tensor if self.sparse_package == "nvmath" else b)
2075
+ self.b = self.operands[1] = b_wrapped.to(self.device_id, stream_holder)
2076
+ if self.ust_operands:
2077
+ self.ust_operands[0] = UST.from_package(self.b.tensor, stream_holder.external)
2078
+
2079
+ if self.api in [Api.MM, Api.MM_OP]:
2080
+ self.b_ifc.update(self.b) # type: ignore[attr-defined]
2081
+
2082
+ if c is not None:
2083
+ c_wrapped = tensor_wrapper.wrap_operand(c.wrapped_operand.tensor if self.sparse_package == "nvmath" else c)
2084
+ self.c = self.operands[2] = c_wrapped.to(self.device_id, stream_holder)
2085
+ if self.ust_operands:
2086
+ self.ust_operands[1] = UST.from_package(self.c.tensor, stream_holder.external)
2087
+
2088
+ if self.api in [Api.MM, Api.MM_OP]:
2089
+ self.c_ifc.update(self.c) # type: ignore[attr-defined]
2090
+
2091
+ if self.inplace:
2092
+ self.cpu_c_ref = c if self.sparse_package == "nvmath" else c_wrapped
2093
+
2094
+ if self.api == Api.CODEGEN:
2095
+ from nvmath.sparse.ust._emitter import populate_matmul_parameters
2096
+
2097
+ a, b, c = self.a.tensor, *self.ust_operands
2098
+ self.kernel_parameters, self.kernel_problem_size = populate_matmul_parameters(a, b, c, dense_bc=True)
2099
+
2100
+ # Update release operands state.
2101
+ self.operands_released = False
2102
+
2103
+ @utils.precondition(_check_valid_matmul)
2104
+ def release_operands(self):
2105
+ """
2106
+ {release_operands}
2107
+ """
2108
+
2109
+ # Fast exit.
2110
+ if self.operands_released:
2111
+ return
2112
+
2113
+ # Set the sparse operand to None.
2114
+ self.operands[0].release()
2115
+
2116
+ # For the dense operands, keep the wrapper but set the internal references to None.
2117
+ self.operands[1].tensor = None
2118
+ if self.ust_operands:
2119
+ self.ust_operands[0] = None
2120
+
2121
+ # Set the aliases to None as well.
2122
+ self.a = self.b = None
2123
+
2124
+ if self.num_operands == 3:
2125
+ self.operands[2].tensor = self.c = None
2126
+ if self.ust_operands:
2127
+ self.ust_operands[1] = None
2128
+
2129
+ if self.inplace:
2130
+ self.cpu_c_ref = None
2131
+
2132
+ self.operands_released = True
2133
+ self.logger.info("The user-provided operands have been released.")
2134
+
2135
+ @utils.precondition(_check_valid_matmul)
2136
+ @utils.precondition(_check_planned, "Execution")
2137
+ @utils.precondition(_check_valid_operands, "Execution")
2138
+ @utils.atomic(_release_workspace_memory_perhaps_wrapper, method=True)
2139
+ def execute(self, *, release_workspace=False, stream: utils.AnyStream | int | None = None):
2140
+ """
2141
+ Execute a prepared (planned) sparse matrix multiplication.
2142
+
2143
+ Args:
2144
+ release_workspace: {release_workspace}
2145
+
2146
+ stream: {stream}
2147
+
2148
+ Returns:
2149
+ {result}
2150
+ """
2151
+ log_info = self.logger.isEnabledFor(logging.INFO)
2152
+
2153
+ if log_info:
2154
+ self.logger.info("= EXECUTION PHASE =")
2155
+ stream_holder = utils.get_or_create_stream(self.device_id, stream, self.dense_package)
2156
+ if log_info:
2157
+ self.logger.info(f"The specified stream for execute() is {stream_holder.obj}.")
2158
+
2159
+ # TODO: create empty tensor for the result.
2160
+
2161
+ if log_info:
2162
+ message = ""
2163
+ if self.api == Api.CODEGEN:
2164
+ message = "(codegen path)"
2165
+ elif self.api == Api.MM:
2166
+ message = "(library dispatch path to SpMM)"
2167
+ elif self.api == Api.MM_OP:
2168
+ message = "(library dispatch path to SpMMOp)"
2169
+ else:
2170
+ raise AssertionError("Internal error.")
2171
+ self.logger.info(f"Starting matrix multiplication {message}...")
2172
+ self.logger.info(f"{self.call_prologue}")
2173
+ # TODO: clean this up.
2174
+ if self.api == Api.CODEGEN:
2175
+ from nvmath.sparse.ust._jit import launch_kernel
2176
+
2177
+ launch_kernel(
2178
+ self.kernel,
2179
+ self.kernel_parameters,
2180
+ self.kernel_problem_size,
2181
+ device_id=self.device_id,
2182
+ stream_holder=stream_holder,
2183
+ blocking=True,
2184
+ )
2185
+ self.last_compute_event = None
2186
+ else:
2187
+ # Set stream for library execution.
2188
+ cusparse.set_stream(self.handle, stream_holder.ptr)
2189
+
2190
+ # Allocate workspace if needed.
2191
+ self._allocate_workspace_memory_perhaps(stream_holder)
2192
+
2193
+ raw_workspace_ptr = utils.get_ptr_from_memory_pointer(self.workspace_ptr)
2194
+ with utils.cuda_call_ctx(stream_holder, self.blocking, timing=log_info) as (
2195
+ self.last_compute_event,
2196
+ elapsed,
2197
+ ):
2198
+ if self.api == Api.MM:
2199
+ cusparse.sp_mm(
2200
+ self.handle,
2201
+ self.op_a,
2202
+ self.op_b,
2203
+ self.alpha.ctypes.data,
2204
+ self.a_ifc.descriptor, # type: ignore[attr-defined]
2205
+ self.b_ifc.descriptor, # type: ignore[attr-defined]
2206
+ self.beta.ctypes.data,
2207
+ self.c_ifc.descriptor, # type: ignore[attr-defined]
2208
+ self.compute_type,
2209
+ cusparse.SpMMAlg.DEFAULT,
2210
+ raw_workspace_ptr,
2211
+ )
2212
+ elif self.api == Api.MM_OP:
2213
+ cusparse.sp_mm_op(self.mm_op_plan, raw_workspace_ptr)
2214
+ else:
2215
+ raise AssertionError("Internal error.")
2216
+
2217
+ if log_info and elapsed.data is not None:
2218
+ self.logger.info(f"The matrix multiplication calculation took {elapsed.data:.3f} ms to complete.")
2219
+
2220
+ # Establish ordering wrt the computation and free workspace if requested.
2221
+ if release_workspace:
2222
+ self._release_workspace_memory_perhaps(True)
2223
+
2224
+ # Return the result.
2225
+ if self.memory_space == "cpu":
2226
+ # Currently only inplace is supported.
2227
+ if self.ust_operands:
2228
+ self.cpu_c_ref.copy_(self.ust_operands[1], stream_holder) # type: ignore
2229
+ out = self.cpu_c_ref
2230
+ else:
2231
+ self.cpu_c_ref.copy_(self.c, stream_holder) # type: ignore
2232
+ out = self.cpu_c_ref.tensor # type: ignore
2233
+ else:
2234
+ if self.ust_operands:
2235
+ out = self.ust_operands[1]
2236
+ else:
2237
+ out = self.c.tensor
2238
+
2239
+ # Release internal reference to the result to permit recycling of memory.
2240
+ # TODO: handle result allocated internally.
2241
+ self._reset_workspace_allocation_tracking()
2242
+
2243
+ return out
2244
+
2245
+ def free(self):
2246
+ """Free Matmul resources.
2247
+
2248
+ It is recommended that the :class:`Matmul` object be used within a context, but if
2249
+ it is not possible then this method must be called explicitly to ensure that the
2250
+ matrix multiplication resources (especially internal library objects) are properly
2251
+ cleaned up.
2252
+ """
2253
+
2254
+ if not self.valid_state:
2255
+ return
2256
+
2257
+ try:
2258
+ # Future operations on the workspace stream should be ordered after the
2259
+ # computation.
2260
+ if self.last_compute_event is not None:
2261
+ if self.workspace_stream is not None:
2262
+ self.workspace_stream.wait(self.last_compute_event)
2263
+ self.last_compute_event = None
2264
+
2265
+ self._free_workspace_memory()
2266
+
2267
+ self._free_plan_resources()
2268
+
2269
+ # Free handle if we own it.
2270
+ if self.handle is not None and self.own_handle:
2271
+ cusparse.destroy(self.handle)
2272
+ self.handle, self.own_handle = None, False
2273
+
2274
+ # Set all attributes to None except for logger and valid_state.
2275
+ for attr in list(vars(self)):
2276
+ if attr not in {"logger", "valid_state"}:
2277
+ setattr(self, attr, None)
2278
+
2279
+ except Exception as e:
2280
+ self.logger.critical("Internal error: only part of the Matmul object's resources have been released.")
2281
+ self.logger.critical(str(e))
2282
+ raise e
2283
+ finally:
2284
+ self.valid_state = False
2285
+
2286
+ self.logger.info("The Matmul object's resources have been released.")
2287
+
2288
+
2289
+ @utils.docstring_decorator(SPARSE_MM_DOCUMENTATION, skip_missing=False)
2290
+ def matmul(
2291
+ a,
2292
+ b,
2293
+ /,
2294
+ c=None,
2295
+ *,
2296
+ alpha=None,
2297
+ beta=None,
2298
+ qualifiers=None,
2299
+ prologs=None,
2300
+ epilog=None,
2301
+ semiring=None,
2302
+ compute_capability=None,
2303
+ options=None,
2304
+ execution: ExecutionCUDA | None = None,
2305
+ stream: utils.AnyStream | int | None = None,
2306
+ ):
2307
+ """
2308
+ Perform the specified sparse matrix multiplication computation, which is one of
2309
+ :math:`epilog(\\alpha \\, op_h(a) \\, @ \\, op_h(b) + \\beta \\, c)` or
2310
+ :math:`epilog(prolog_a(op_t(a)) \\, @ \\, prolog_b(op_t(b)) + prolog_c(c))`. The
2311
+ :math:`op_h` and :math:`op_t` operators optionally specify transpose/hermitian or
2312
+ transpose operations respectively via the ``qualifiers`` argument. In addition, the
2313
+ scalar multiplication and addition operators ("semiring") can be customized by the
2314
+ user, if desired.
2315
+
2316
+ .. note::
2317
+ The complex conjugate operation is mutually exclusive with prolog since it can be
2318
+ absorbed into the prolog.
2319
+
2320
+ .. note::
2321
+ Currently only in-place sparse matrix multiplication is supported, so operand ``c``
2322
+ must be provided. This restriction will be removed in a future release.
2323
+
2324
+ This function-form is a wrapper around the stateful :class:`Matmul` object APIs and is
2325
+ meant for *single* use (the user needs to perform just one sparse matrix multiplication,
2326
+ for example), in which case there is no possibility of amortizing preparatory costs.
2327
+
2328
+ Detailed information on what's happening within this function can be obtained by passing
2329
+ in a :class:`logging.Logger` object to :class:`MatmulOptions` or by setting the
2330
+ appropriate options in the root logger object, which is used by default:
2331
+
2332
+ >>> import logging
2333
+ >>> logging.basicConfig(
2334
+ ... level=logging.INFO,
2335
+ ... format="%(asctime)s %(levelname)-8s %(message)s",
2336
+ ... datefmt="%m-%d %H:%M:%S",
2337
+ ... )
2338
+
2339
+ A user can select the desired logging level and, in general, take advantage of all of
2340
+ the functionality offered by the Python `logging` module.
2341
+
2342
+ Args:
2343
+ a: {a}
2344
+
2345
+ b: {b}
2346
+
2347
+ c: {c}
2348
+
2349
+ alpha: {alpha}
2350
+
2351
+ beta: {beta}
2352
+
2353
+ qualifiers: {qualifiers}
2354
+
2355
+ prologs: {prologs}
2356
+
2357
+ epilog: {epilog}
2358
+
2359
+ semiring: {semiring}
2360
+
2361
+ compute_capability: {compute_capability}
2362
+
2363
+ options: {options}
2364
+
2365
+ execution: {execution}
2366
+
2367
+ stream: {stream}
2368
+
2369
+ Returns:
2370
+ {result}
2371
+
2372
+ Semantics:
2373
+ {semantics}
2374
+
2375
+ .. seealso::
2376
+ :class:`Matmul`, :class:`MatmulOptions`, :class:`ExecutionCUDA`
2377
+
2378
+ Examples:
2379
+
2380
+ >>> import torch
2381
+ >>> import nvmath
2382
+
2383
+ Prepare sample data.
2384
+
2385
+ >>> index_type, dtype = torch.int32, torch.float32
2386
+ >>> device_id = 0
2387
+ >>> shape = 2, 2
2388
+
2389
+ Create a torch COO tensor, and view it as UST.
2390
+
2391
+ >>> indices = torch.tensor([[0, 1], [0, 1]], dtype=index_type)
2392
+ >>> values = torch.tensor([2.0, 4.0], dtype=dtype)
2393
+ >>> a = torch.sparse_coo_tensor(indices, values, shape, device=device_id)
2394
+ >>> a = a.coalesce()
2395
+ >>> a = nvmath.sparse.ust.Tensor.from_package(a)
2396
+
2397
+ Dense 'b' and 'c', also viewed as UST objects.
2398
+
2399
+ >>> b = torch.ones(*shape, dtype=dtype, device=device_id)
2400
+ >>> b = nvmath.sparse.ust.Tensor.from_package(b)
2401
+ >>> c = torch.zeros(*shape, dtype=dtype, device=device_id)
2402
+ >>> c = nvmath.sparse.ust.Tensor.from_package(c)
2403
+
2404
+ Solve :math:`c := a @ b + c`.
2405
+
2406
+ >>> r = nvmath.sparse.matmul(a, b, c, beta=1.0)
2407
+
2408
+ The result can also be viewed as a torch tensor.
2409
+
2410
+ >>> r = nvmath.sparse.ust.Tensor.to_package(r)
2411
+
2412
+ .. note:: This function is a convenience wrapper around :class:`Matmul` and is
2413
+ specifically meant for *single* use.
2414
+
2415
+ Further examples can be found in the `nvmath/examples/sparse/generic/matmul
2416
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/sparse/generic/matmul>`_
2417
+ directory.
2418
+ """
2419
+
2420
+ with Matmul(
2421
+ a, b, c=c, alpha=alpha, beta=beta, qualifiers=qualifiers, options=options, execution=execution, stream=stream
2422
+ ) as mm:
2423
+ mm.plan(prologs=prologs, epilog=epilog, semiring=semiring, compute_capability=compute_capability, stream=stream)
2424
+ r = mm.execute(stream=stream)
2425
+
2426
+ return r