nvmath-python 1.0.0__cp314-cp314t-win_amd64.whl

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