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,2094 @@
1
+ # Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ __all__ = [
6
+ "Matmul",
7
+ "matmul",
8
+ ]
9
+
10
+ import dataclasses
11
+ import logging
12
+ import math
13
+ from collections.abc import Generator, MutableSequence, Sequence
14
+ from typing import Any, Final, Literal, TypeAlias
15
+
16
+ import numpy as np
17
+
18
+ from nvmath import memory
19
+ from nvmath._internal import templates
20
+ from nvmath._internal.utils import LoggerLike, get_addresses_of_elements
21
+ from nvmath.bindings import cublas
22
+ from nvmath.internal import formatters, tensor_wrapper, typemaps, utils
23
+ from nvmath.internal._layout import StridedLayout
24
+ from nvmath.internal.tensor_ifc_ndbuffer import NDBufferTensor
25
+ from nvmath.linalg._internal.batch import BatchTraits
26
+ from nvmath.linalg._internal.layout import BLASMatrixTraits, BLASMMTraitsView, InputMMTraits, check_extents, check_strides
27
+ from nvmath.linalg.advanced.matmulmod import SHARED_MM_DOCUMENTATION
28
+ from nvmath.linalg.generic._configuration import (
29
+ CACHED_LAYOUT_CHECKERS,
30
+ GeneralMatrixQualifier,
31
+ MatmulOptions,
32
+ MatrixQualifier,
33
+ matrix_qualifiers_dtype,
34
+ mm_layout_checker_getter,
35
+ select_blas_group_mm_function,
36
+ select_blas_mm_function,
37
+ vector_to_square,
38
+ )
39
+ from nvmath.linalg.generic._dtype import check_dtype
40
+
41
+ AnyTensor: TypeAlias = utils.AnyTensor
42
+ AnyStream: TypeAlias = utils.AnyStream
43
+ SideMode: TypeAlias = cublas.SideMode
44
+ FillMode: TypeAlias = cublas.FillMode
45
+ DiagType: TypeAlias = cublas.DiagType
46
+
47
+
48
+ @dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
49
+ class ExecutionCUDA(templates.ExecutionCUDA):
50
+ """
51
+ A data class for providing GPU execution options.
52
+
53
+ Attributes:
54
+ device_id: CUDA device ordinal (only used if the operand resides on the CPU). The
55
+ default value is 0.
56
+
57
+ .. seealso::
58
+ :class:`ExecutionCPU`, :class:`Matmul`, :func:`matmul`
59
+ """
60
+
61
+ pass
62
+
63
+
64
+ @dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
65
+ class ExecutionCPU(templates.ExecutionCPU):
66
+ """
67
+ A data class for providing CPU execution options.
68
+
69
+ Attributes:
70
+ num_threads: The number of CPU threads used to execute the operation.
71
+ If not specified, defaults to the number of CPU cores available to the
72
+ process.
73
+
74
+ .. seealso::
75
+ :class:`ExecutionCUDA`, :class:`Matmul`, :func:`matmul`,
76
+ :func:`~nvmath.bindings.nvpl.blas.set_num_threads_local`
77
+ """
78
+
79
+ pass
80
+
81
+
82
+ class InvalidMatmulState(Exception):
83
+ pass
84
+
85
+
86
+ class _PrefixedLoggerAdapter(logging.LoggerAdapter):
87
+ def process(self, msg, kwargs):
88
+ return f"[{self.extra['prefix']}] {msg}", kwargs
89
+
90
+
91
+ GENERIC_MM_DOCUMENTATION = SHARED_MM_DOCUMENTATION.copy()
92
+ GENERIC_MM_DOCUMENTATION.update(
93
+ {
94
+ "a": SHARED_MM_DOCUMENTATION["a"].replace("tensor representing", "tensor or a sequence of tensors representing"),
95
+ #
96
+ "b": SHARED_MM_DOCUMENTATION["b"].replace("tensor representing", "tensor or a sequence of tensors representing"),
97
+ #
98
+ "c": SHARED_MM_DOCUMENTATION["c"].replace("tensor representing", "tensor or a sequence of tensors representing"),
99
+ #
100
+ "alpha": SHARED_MM_DOCUMENTATION["alpha"].replace("scale factor", "scale factor or a sequence of scale factors"),
101
+ #
102
+ "beta": SHARED_MM_DOCUMENTATION["beta"].replace("scale factor", "scale factor or a sequence of scale factors"),
103
+ #
104
+ "qualifiers": """\
105
+ If desired, specify the matrix qualifiers as a :class:`numpy.ndarray` of
106
+ :class:`~nvmath.linalg.generic.matrix_qualifiers_dtype` objects of length <= 3 corresponding to the operands `a`, `b`, and
107
+ `c`. By default, :class:`GeneralMatrixQualifier` is assumed for each tensor. See
108
+ :ref:`matrix-tensor-qualifiers` for the motivation behind qualifiers.""".replace("\n", " "),
109
+ #
110
+ "execution": """\
111
+ Specify execution space options for the Matmul as a :class:`ExecutionCUDA` or :class:`ExecutionCPU` object. If not specified,
112
+ the execution space will be selected to match operand's storage (in GPU or host memory), and the corresponding
113
+ :class:`ExecutionCUDA` or :class:`ExecutionCPU` object will be default-constructed.""".replace("\n", " "),
114
+ #
115
+ "options": """\
116
+ Specify options for the matrix multiplication as a :class:`MatmulOptions` object. Alternatively,
117
+ a `dict` containing the parameters for the ``MatmulOptions`` constructor can also be provided.
118
+ If not specified, the value will be set to the default-constructed ``MatmulOptions`` object.""".replace("\n", " "),
119
+ #
120
+ "result": """\
121
+ The result of the specified matrix multiplication, which remains on the same device and belongs to the
122
+ same package as the input operands.""".replace("\n", " "),
123
+ #
124
+ "semantics": """\
125
+ .. _semantics:
126
+
127
+ Inputs may be batched either **implicitly** and/or **explicitly**.
128
+
129
+ * An **implicitly-batched** input is a higher-dimensional :math:`N \\geq 3D` tensor.
130
+
131
+ * An **explicitly-batched** input is a Python sequence of tensors.
132
+
133
+ There may be only one explicitly-batched dimension, but there may be any number of implicitly-batched dimensions.
134
+ Explicitly-batched inputs are first broadcast to equal length along the explicitly-batched dimension,
135
+ then each explicit batch is treated as implicitly-batched inputs.
136
+
137
+ The semantics of the matrix multiplication follows :external:py:data:`numpy.matmul` semantics, with some restrictions.
138
+
139
+ * Broadcasting along implicitly-batched dimensions of `c` is not supported in this API, but may be supported in the
140
+ future. See the advanced API (:func:`nvmath.linalg.advanced.matmul`) for an API that supports broadcasting `c`.
141
+
142
+ In addition, the semantics for the fused matrix addition are described below:
143
+
144
+ * If arguments `a` and `b` are matrices, they are multiplied according to the rules of matrix multiplication.
145
+ * If argument `a` is 1-D, it is promoted to a matrix by prefixing ``1`` to its dimensions. After matrix
146
+ multiplication, the prefixed ``1`` is removed from the result's dimensions.
147
+ * If argument `b` is 1-D, it is promoted to a matrix by appending ``1`` to its dimensions. After matrix
148
+ multiplication, the appended ``1`` is removed from the result's dimensions.
149
+ * The operand for the matrix addition `c` must be the expected shape of the result of the matrix multiplication.
150
+
151
+ """.strip(),
152
+ #
153
+ "reset_operands_unchecked": utils._reset_operand_unchecked_docstring(True, version_added="1.0"),
154
+ }
155
+ )
156
+
157
+
158
+ def _resolve_execution(
159
+ execution: ExecutionCPU | ExecutionCUDA | str | None,
160
+ operands_device_id: int | Literal["cpu"],
161
+ ) -> ExecutionCPU | ExecutionCUDA:
162
+ """Normalize the user-supplied ``execution`` argument to a concrete instance.
163
+
164
+ ``execution`` may be ``None``, the literal strings ``"cuda"``/``"cpu"``, or an
165
+ already-instantiated :class:`ExecutionCPU`/:class:`ExecutionCUDA`. The resolved
166
+ value's ``device_id`` is aligned with ``operands_device_id`` when both sides
167
+ pin a CUDA device.
168
+ """
169
+ match execution, operands_device_id:
170
+ case (None | "cuda", int()):
171
+ return ExecutionCUDA(device_id=operands_device_id)
172
+ case ("cuda", "cpu"):
173
+ return ExecutionCUDA()
174
+ case (None, "cpu") | ("cpu", _):
175
+ return ExecutionCPU()
176
+ case (ExecutionCUDA(), int()):
177
+ return dataclasses.replace(execution, device_id=operands_device_id)
178
+ case (ExecutionCPU(), _):
179
+ return execution
180
+ case (ExecutionCUDA(), "cpu"):
181
+ return execution
182
+ case _:
183
+ raise ValueError("Matmul.execution must be one of ExecutionCUDA, ExecutionCPU, None, 'cuda', or 'cpu'.")
184
+
185
+
186
+ def _resolve_internal_operand_package(execution_name: str, operands_package: str) -> str:
187
+ """Map ``operands_package`` to the package name used internally for buffers/streams.
188
+
189
+ On CUDA execution, ``numpy`` operands are routed through ``cuda``; on CPU execution,
190
+ ``cupy`` operands are routed through ``cupy_host``. All other packages pass through.
191
+ """
192
+ if execution_name == "cuda":
193
+ return operands_package if operands_package != "numpy" else "cuda"
194
+ return operands_package if operands_package != "cupy" else "cupy_host"
195
+
196
+
197
+ def _get_or_create_stream_maybe(
198
+ execution: ExecutionCPU | ExecutionCUDA,
199
+ operands_device_id: int | Literal["cpu"],
200
+ operands_package: str,
201
+ internal_op_package: str,
202
+ stream: utils.AnyStream,
203
+ ) -> tuple[utils.StreamHolder | None, utils.StreamHolder | None]:
204
+ """Return ``(exec_stream_holder, operand_stream_holder)`` for the given spaces.
205
+
206
+ The first holder is used for execution-space work (compute, workspace, output
207
+ buffers). The second is used for input-space data movement. When both are
208
+ non-``None`` and the execution and input spaces coincide, they are the same
209
+ holder.
210
+ """
211
+ if execution.name == "cuda":
212
+ h = utils.get_or_create_stream(execution.device_id, stream, internal_op_package)
213
+ return h, h
214
+ if isinstance(operands_device_id, int):
215
+ return None, utils.get_or_create_stream(operands_device_id, stream, operands_package)
216
+ return None, None
217
+
218
+
219
+ def _realloc_if_needed_and_copy_to_mirror(
220
+ src_holder: utils.TensorHolder,
221
+ dest_holder: utils.TensorHolder,
222
+ dest_device_id: int | Literal["cpu"],
223
+ stream_holder: utils.StreamHolder | None,
224
+ ) -> None:
225
+ """Copy a source operand into its mirror on ``dest_device_id``.
226
+
227
+ If ``dest_holder.tensor`` is still alive, do an in-place copy into it. Otherwise
228
+ (typically after :meth:`release_operands`), allocate a new buffer via
229
+ ``src_holder.to()`` and bind it to ``dest_holder.tensor``.
230
+ """
231
+ if dest_holder.tensor is not None:
232
+ dest_holder.copy_(src_holder, stream_holder=stream_holder)
233
+ return
234
+ dest_holder.tensor = src_holder.to(dest_device_id, stream_holder).tensor
235
+
236
+
237
+ class _ImplicitlyBatchedMatmul:
238
+ """A Matmul object that supports implicitly batched operands.
239
+
240
+ This class has the same public API as :class:`Matmul`, except that the arguments may
241
+ only be implicitly batched.
242
+
243
+ .. important::
244
+
245
+ Explicit batches are always defined as a Python Sequence of the same type and
246
+ implicit batches are always leading dimension of tensors. Otherwise, broadcasting
247
+ behavior would be ambiguous. For example, if some arguments are sequences of tensors
248
+ and other arguments are non-sequence tensors with batch dimensions.
249
+
250
+ .. seealso:: :func:`_zip_broadcast`, :class:`Matmul`, :class:`_ExplicitlyBatchedMatmul`
251
+
252
+ """
253
+
254
+ __slots__ = (
255
+ "_allocator",
256
+ "_batch_traits",
257
+ "_blocking",
258
+ "_call_prologue",
259
+ "_execution_device_id",
260
+ "_function",
261
+ "_input_traits",
262
+ "_internal_op_package",
263
+ "_logger",
264
+ "_memspace_eq_exespace",
265
+ "_mm_traits",
266
+ "_operands_backup",
267
+ "_operands_device_id",
268
+ "_operands_package",
269
+ "_operands",
270
+ "_qualifiers",
271
+ "_result_class",
272
+ "a_dtype_name",
273
+ "a_dtype",
274
+ "alpha",
275
+ "b_dtype_name",
276
+ "b_dtype",
277
+ "beta",
278
+ "c_dtype_name",
279
+ "c_dtype",
280
+ "execution",
281
+ "is_complex",
282
+ "lazy_conjugation",
283
+ "num_operands",
284
+ "operand_extents",
285
+ "operand_strides",
286
+ "options",
287
+ "scale_type_name",
288
+ )
289
+
290
+ options: Final[MatmulOptions]
291
+ _allocator: Final[memory.BaseCUDAMemoryManager | memory.BaseCUDAMemoryManagerAsync | None]
292
+ _blocking: Final[bool]
293
+ _logger: Final[LoggerLike]
294
+
295
+ # Metadata related to execution space
296
+ execution: Final[ExecutionCPU | ExecutionCUDA]
297
+ """A class which describes the execution space parameters."""
298
+ _internal_op_package: Final[str]
299
+ """The package of the operands in the execution space."""
300
+ _operands: MutableSequence[utils.TensorHolder]
301
+ """A copy of the operands in execution space."""
302
+ _result_class: Final[type[utils.TensorHolder]]
303
+ """The type of TensorHolder to use for the execution space result."""
304
+
305
+ # Metadata about the input/output tensors
306
+ _operands_backup: MutableSequence[utils.TensorHolder | None]
307
+ """A reference to original operands in their input space."""
308
+ _operands_device_id: Final[int | Literal["cpu"]]
309
+ """The device_id of the input space."""
310
+ _execution_device_id: Final[int | Literal["cpu"]]
311
+ """The device_id of the execution space (``"cpu"`` for CPU execution)."""
312
+ _memspace_eq_exespace: Final[bool]
313
+ """Cached ``_operands_device_id == _execution_device_id``."""
314
+ _operands_package: Final[str]
315
+ """The package of the operands in the input space."""
316
+
317
+ _call_prologue: Final[str]
318
+ """Stores a message for logging about blocking behavior"""
319
+
320
+ _input_traits: Final[InputMMTraits]
321
+ _batch_traits: Final[tuple[BatchTraits, BatchTraits, BatchTraits, BatchTraits]]
322
+ _qualifiers: Final[MatrixQualifier]
323
+
324
+ def __init__(
325
+ self,
326
+ a: AnyTensor,
327
+ b: AnyTensor,
328
+ /,
329
+ c: AnyTensor | None = None,
330
+ *,
331
+ alpha: float | complex | None = None,
332
+ beta: float | complex | None = None,
333
+ qualifiers: MatrixQualifier | None = None,
334
+ options: MatmulOptions,
335
+ execution: ExecutionCPU | ExecutionCUDA,
336
+ exec_stream_holder: utils.StreamHolder | None,
337
+ operand_stream_holder: utils.StreamHolder | None,
338
+ _allow_mixed_batch_order: bool = False,
339
+ _logger: LoggerLike | None = None,
340
+ ):
341
+ self.options = options
342
+ self._logger = _logger if _logger is not None else self.options.logger
343
+ self.execution = execution
344
+
345
+ assert not (c is None and options.inplace), "Internal Error: Matmul should have rejected inplace=True with c=None."
346
+
347
+ a = tensor_wrapper.wrap_operand(a)
348
+ check_dtype(a.dtype, "A")
349
+ check_extents(a.shape, "A")
350
+ check_strides(a.strides, "A")
351
+
352
+ b = tensor_wrapper.wrap_operand(b)
353
+ check_dtype(b.dtype, "B")
354
+ check_extents(b.shape, "B")
355
+ check_strides(b.strides, "B")
356
+ operands = [a, b]
357
+
358
+ self.num_operands = 2
359
+ if c is not None:
360
+ self.num_operands = 3
361
+ c = tensor_wrapper.wrap_operand(c)
362
+ check_dtype(c.dtype, "C")
363
+ check_extents(c.shape, "C")
364
+ check_strides(c.strides, "C")
365
+ operands.append(c)
366
+
367
+ self._operands_device_id = utils.get_operands_device_id(operands)
368
+ self._execution_device_id = getattr(self.execution, "device_id", "cpu")
369
+ self._memspace_eq_exespace = self._operands_device_id == self._execution_device_id
370
+ self._operands_package = utils.get_operands_package(operands)
371
+ self._internal_op_package = _resolve_internal_operand_package(self.execution.name, self._operands_package)
372
+
373
+ operands_backup: list[utils.TensorHolder | None] = [None] * len(operands)
374
+ for i in range(len(operands)):
375
+ # Copy the operand to execution_space's device if needed.
376
+ operands[i], operands_backup[i] = templates.copy_operand_perhaps(
377
+ None,
378
+ operands[i],
379
+ operand_stream_holder,
380
+ self._execution_device_id,
381
+ self._operands_device_id,
382
+ )
383
+ self._operands = operands
384
+ self._operands_backup = operands_backup
385
+
386
+ # The result's package and device.
387
+ self._result_class = self._operands[0].__class__
388
+
389
+ # Set blocking or non-blocking behavior.
390
+ self._blocking = self.options.blocking != "auto" or self._operands_device_id == "cpu" or self.execution.name == "cpu"
391
+ if self._blocking:
392
+ call_prologue = "This call is blocking and will return only after the operation is complete."
393
+ else:
394
+ call_prologue = (
395
+ "This call is non-blocking and will return immediately after the operation is launched on the device."
396
+ )
397
+ self._call_prologue = call_prologue
398
+
399
+ # Set memory allocator.
400
+ allocator: memory.BaseCUDAMemoryManager | memory.BaseCUDAMemoryManagerAsync | None
401
+ match self.execution:
402
+ case ExecutionCUDA():
403
+ allocator = (
404
+ memory._MEMORY_MANAGER[self._internal_op_package](self.execution.device_id, self._logger)
405
+ if self.options.allocator is None
406
+ else self.options.allocator
407
+ )
408
+ case ExecutionCPU() | _:
409
+ allocator = None # currently, the nvpl/fftw does not support custom workspace allocation
410
+ self._allocator = allocator
411
+
412
+ self._logger.info(f"The data type of operand A is '{a.dtype}', and that of operand B is '{b.dtype}'.")
413
+ if c is not None:
414
+ self._logger.info(f"The data type of operand C is '{c.dtype}'.")
415
+
416
+ if self.options.inplace:
417
+ self._logger.info("The operation will be performed inplace with operand C.")
418
+ assert c is not None # guaranteed by the inplace-with-c assertion above
419
+
420
+ # Inplace writes back into C, so the C layout must be unique.
421
+ # Otherwise distinct logical indices alias the same memory cell,
422
+ # producing overlapping writes / undefined behavior.
423
+ if not StridedLayout(c.shape, c.strides, itemsize=1).is_unique:
424
+ raise ValueError(
425
+ f"The layout for C with shape = {c.shape} and strides = {c.strides} is not "
426
+ f"supported for inplace operations: the index-to-memory mapping is not "
427
+ f"injective, which would cause overlapping writes into the same memory."
428
+ )
429
+
430
+ if c is not None and beta is None:
431
+ raise ValueError("A value for beta must be provided if operand C is provided.")
432
+
433
+ assert self.num_operands == 2 or self.num_operands == 3, "Internal Error."
434
+
435
+ if a.dtype != b.dtype or (c is not None and a.dtype != c.dtype):
436
+ raise ValueError(
437
+ "Unsupported combination of dtypes. "
438
+ f"A ({a.dtype}), B ({b.dtype}), and C ({getattr(c, 'dtype', None)}) must all have the same dtype."
439
+ )
440
+ # Determine the data types for a and b.
441
+ self.a_dtype = typemaps.NAME_TO_DATA_TYPE[a.dtype]
442
+ self.b_dtype = typemaps.NAME_TO_DATA_TYPE[b.dtype]
443
+ self.a_dtype_name = a.dtype
444
+ self.b_dtype_name = b.dtype
445
+
446
+ self.is_complex = "complex" in self.a_dtype_name or "complex" in self.b_dtype_name
447
+
448
+ # Determine the data types for c.
449
+ if c is None:
450
+ self.c_dtype = self.a_dtype
451
+ else:
452
+ self.c_dtype = typemaps.NAME_TO_DATA_TYPE[c.dtype]
453
+ self.c_dtype_name = typemaps.DATA_TYPE_TO_NAME[self.c_dtype]
454
+
455
+ self._logger.info(f"The data type for the result D is '{self.c_dtype_name}'.")
456
+
457
+ self.scale_type_name = self.a_dtype_name
458
+
459
+ # Set alpha and beta.
460
+ self.alpha = np.zeros((1,), dtype=self.scale_type_name)
461
+ try:
462
+ self.alpha[0] = alpha if alpha is not None else 1
463
+ except (ValueError, TypeError) as e:
464
+ raise ValueError(f"The value provided for alpha {alpha} is not convertible to dtype '{self.alpha.dtype}'.") from e
465
+
466
+ self.beta = np.zeros((1,), dtype=self.scale_type_name)
467
+ if beta is not None and c is None:
468
+ self._logger.warning(f"Matmul: The provided beta value {beta} is ignored since operand C is not specified.")
469
+ try:
470
+ self.beta[0] = beta if beta is not None and c is not None else 0
471
+ except (ValueError, TypeError) as e:
472
+ raise ValueError(f"The value provided for beta {beta} is not convertible to dtype '{self.beta.dtype}'.") from e
473
+
474
+ if qualifiers is None:
475
+ new_qualifiers = np.empty(3, dtype=matrix_qualifiers_dtype)
476
+ new_qualifiers[:] = GeneralMatrixQualifier.create()
477
+ else:
478
+ if not ((len(qualifiers) == 3) or (len(qualifiers) == 2 and c is None)):
479
+ raise ValueError("The number of MatrixQualifiers must match the number of operands.")
480
+ new_qualifiers = np.empty(3, dtype=matrix_qualifiers_dtype)
481
+ new_qualifiers[:2] = qualifiers[:2]
482
+ new_qualifiers[2] = GeneralMatrixQualifier.create() if len(qualifiers) < 3 else qualifiers[2]
483
+ self._qualifiers = new_qualifiers
484
+ self._logger.info(
485
+ f"The matrix multiplication qualifiers are:\n"
486
+ f" A = {GeneralMatrixQualifier.to_string(self._qualifiers[0])}\n"
487
+ f" B = {GeneralMatrixQualifier.to_string(self._qualifiers[1])}\n"
488
+ f" C = {GeneralMatrixQualifier.to_string(self._qualifiers[2])}"
489
+ )
490
+
491
+ # Set qualifiers based on torch lazy conjugation flag if not provided.
492
+ self._qualifiers[0]["conjugate"] = self._qualifiers[0]["conjugate"] ^ self._operands[0].is_conjugate
493
+ self._qualifiers[1]["conjugate"] = self._qualifiers[1]["conjugate"] ^ self._operands[1].is_conjugate
494
+ self.lazy_conjugation = (self._operands[0].is_conjugate, self._operands[1].is_conjugate, False)
495
+ if c is not None:
496
+ self._qualifiers[2]["conjugate"] = self._qualifiers[2]["conjugate"] ^ self._operands[2].is_conjugate
497
+ if self._qualifiers[2]["conjugate"]:
498
+ raise ValueError("The conjugate flag is currently not supported for operand C.")
499
+
500
+ # Capture operand extents and strides for consistency check when resetting operands.
501
+ self.operand_extents = tuple(o.shape for o in self._operands)
502
+ self.operand_strides = tuple(o.strides for o in self._operands)
503
+
504
+ # Create operand layouts.
505
+ a_layout = BLASMatrixTraits(
506
+ self.a_dtype,
507
+ *vector_to_square(
508
+ self._operands[0].shape[-2:],
509
+ self._operands[0].strides[-2:],
510
+ self._qualifiers[0],
511
+ ),
512
+ is_conjugate=bool(self._qualifiers[0]["conjugate"]),
513
+ is_transpose=False,
514
+ is_lower=self._qualifiers[0]["uplo"] == FillMode.LOWER,
515
+ )
516
+ self._logger.info("Operand A has traits of %s", a_layout)
517
+ b_layout = BLASMatrixTraits(
518
+ self.b_dtype,
519
+ *vector_to_square(
520
+ self._operands[1].shape[-2:],
521
+ self._operands[1].strides[-2:],
522
+ self._qualifiers[1],
523
+ ),
524
+ is_conjugate=bool(self._qualifiers[1]["conjugate"]),
525
+ is_transpose=False,
526
+ is_lower=self._qualifiers[1]["uplo"] == FillMode.LOWER,
527
+ )
528
+ self._logger.info("Operand B has traits of %s", b_layout)
529
+ c_layout = (
530
+ None
531
+ if c is None
532
+ else BLASMatrixTraits(
533
+ self.c_dtype,
534
+ *vector_to_square(
535
+ self._operands[2].shape[-2:],
536
+ self._operands[2].strides[-2:],
537
+ self._qualifiers[2],
538
+ ),
539
+ is_conjugate=bool(self._qualifiers[2]["conjugate"]),
540
+ is_transpose=False,
541
+ is_lower=self._qualifiers[2]["uplo"] == FillMode.LOWER,
542
+ )
543
+ )
544
+ self._logger.info("Operand C has traits of %s", c_layout)
545
+
546
+ # Get the operation traits.
547
+ self._input_traits = InputMMTraits.from_layouts(a_layout, b_layout, c_layout, self.options.inplace, self._logger)
548
+
549
+ a_batch = BatchTraits.from_full_shape_and_strides(
550
+ self._operands[0].shape,
551
+ self._operands[0].strides,
552
+ num_trailing_dims=2,
553
+ overlap_allowed=True,
554
+ )
555
+ b_batch = BatchTraits.from_full_shape_and_strides(
556
+ self._operands[1].shape,
557
+ self._operands[1].strides,
558
+ num_trailing_dims=2,
559
+ overlap_allowed=True,
560
+ )
561
+ c_batch = (
562
+ BatchTraits(shape=(), strides=())
563
+ if c is None
564
+ else BatchTraits.from_full_shape_and_strides(
565
+ self._operands[2].shape,
566
+ self._operands[2].strides,
567
+ num_trailing_dims=2,
568
+ overlap_allowed=False,
569
+ )
570
+ )
571
+ # BatchTraits overloads the * operator with a batch combination behavior
572
+ abc_batch, axis_order = a_batch.combine(
573
+ b_batch.combine(c_batch, allow_mixed_axis_order=_allow_mixed_batch_order),
574
+ allow_mixed_axis_order=_allow_mixed_batch_order,
575
+ )
576
+ d_batch = (
577
+ c_batch
578
+ if self.options.inplace
579
+ else BatchTraits.from_batch_shape_and_size(
580
+ shape=abc_batch,
581
+ axis_order=axis_order,
582
+ batch_stride=math.prod(self._input_traits.d_layout_traits.shape),
583
+ )
584
+ )
585
+ self._logger.info("Operand A has batch traits of %s.", a_batch)
586
+ self._logger.info("Operand B has batch traits of %s.", b_batch)
587
+ self._logger.info("Operand C has batch traits of %s.", c_batch)
588
+ self._logger.info("Result D has batch traits of %s.", d_batch)
589
+ self._batch_traits = (a_batch, b_batch, c_batch, d_batch)
590
+
591
+ def _get_or_create_stream_maybe(
592
+ self, stream: utils.AnyStream
593
+ ) -> tuple[utils.StreamHolder | None, utils.StreamHolder | None]:
594
+ return _get_or_create_stream_maybe(
595
+ self.execution,
596
+ self._operands_device_id,
597
+ self._operands_package,
598
+ self._internal_op_package,
599
+ stream,
600
+ )
601
+
602
+ def plan(self, *, stream: AnyStream | int | None = None, log_info=False) -> None:
603
+ # NOTE: timing context called from Matmul.plan to avoid redundancy
604
+ mm_layout_checker = mm_layout_checker_getter(self._qualifiers)
605
+
606
+ mm_traits = BLASMMTraitsView.from_input_traits(
607
+ self._input_traits,
608
+ mm_layout_checker,
609
+ self._logger,
610
+ lookup_table_table=CACHED_LAYOUT_CHECKERS,
611
+ )
612
+
613
+ self._mm_traits = mm_traits
614
+
615
+ self._function, function_name = select_blas_mm_function(
616
+ (*self._batch_traits[:2], self._batch_traits[3]),
617
+ mm_traits,
618
+ self._qualifiers,
619
+ self._logger,
620
+ self.execution,
621
+ )
622
+
623
+ if log_info:
624
+ flop_count = 2 * mm_traits.M * mm_traits.N * mm_traits.K
625
+
626
+ self._logger.info(f"The plan found 1 suitable algorithm named {function_name}.")
627
+ self._logger.info(f"The base matrix multiplication FLOP count is {formatters.FLOPSStr(flop_count, 'FLOP')}.")
628
+
629
+ def _check_and_set_operand(
630
+ self,
631
+ new_operand: utils.TensorHolder,
632
+ operand_name: str,
633
+ stream_holder: utils.StreamHolder | None,
634
+ *,
635
+ operand_index: int,
636
+ dtype: str | None = None,
637
+ extents: Sequence[int] | None = None,
638
+ strides: Sequence[int] | None = None,
639
+ ):
640
+ """
641
+ Check to make sure that the provided operand is consistent with the one it's
642
+ updating, and update it.
643
+ """
644
+ # Make sure that the data type and extents match.
645
+ utils.check_attribute_match(dtype, new_operand.dtype, "data type")
646
+ utils.check_attribute_match(extents, new_operand.shape, "extents")
647
+
648
+ # Package must be the same to preserve stream ordering
649
+ if self._operands_package != new_operand.name:
650
+ raise TypeError(
651
+ f"Library package mismatch: The operand {operand_name} must from the same package ({new_operand.name}) "
652
+ f"as the original operand ({self._operands_package})."
653
+ )
654
+
655
+ if self._operands_device_id != new_operand.device_id:
656
+ raise ValueError(
657
+ f"The operand {operand_name} must be on the same device ({new_operand.device_id}) as the original operand "
658
+ f"({self._operands_device_id})."
659
+ )
660
+
661
+ # Conjugate flag of the provided operands must match the original qualifiers
662
+ if self.lazy_conjugation[operand_index] != new_operand.is_conjugate:
663
+ raise ValueError(f"The provided operand {operand_name} has different conjugate flag than the original operand")
664
+
665
+ # Wrappers in self._operands / self._operands_backup are preserved for the
666
+ # lifetime of this object (release_operands clears only their .tensor); rebind
667
+ # in same-space and use the realloc-or-copy helper in cross-space, mirroring
668
+ # the unchecked path so both reset flavors maintain that invariant.
669
+ if self._memspace_eq_exespace:
670
+ self._operands[operand_index].tensor = new_operand.tensor
671
+ else:
672
+ _realloc_if_needed_and_copy_to_mirror(
673
+ new_operand,
674
+ self._operands[operand_index],
675
+ self._execution_device_id,
676
+ stream_holder,
677
+ )
678
+ self._operands_backup[operand_index] = new_operand
679
+
680
+ # After the rebind/copy above, check that the strides of the buffer the matmul
681
+ # will actually read (the user's tensor in same-space, the mirror in cross-space)
682
+ # still match what was planned for at construction.
683
+ # FIXME: a stride mismatch here leaves _operands[operand_index] already mutated;
684
+ # if the caller catches this they can observe a half-applied reset.
685
+ utils.check_attribute_match(strides, self._operands[operand_index].strides, "strides")
686
+
687
+ self._logger.info(f"Operand '{operand_name}' has been reset to the new value.")
688
+
689
+ return
690
+
691
+ def reset_operands(
692
+ self,
693
+ *,
694
+ a=None,
695
+ b=None,
696
+ c=None,
697
+ alpha=None,
698
+ beta=None,
699
+ stream: AnyStream | int | None = None,
700
+ _operands_released=True,
701
+ ):
702
+ if c is not None and self.num_operands == 2:
703
+ raise ValueError(
704
+ "The matrix multiplication problem specification does not include operand C, so it cannot be reset."
705
+ )
706
+
707
+ # If operands have been released, all required operands must be provided
708
+ if _operands_released:
709
+ all_provided = a is not None and b is not None and (self.num_operands != 3 or c is not None)
710
+ if not all_provided:
711
+ raise ValueError(
712
+ "After release_operands(), all required operands must be provided to reset_operands(). "
713
+ f"Required: a, b{', c' if self.num_operands == 3 else ''}"
714
+ )
715
+
716
+ # Update alpha.
717
+ if alpha is not None:
718
+ try:
719
+ self.alpha[0] = alpha
720
+ except (ValueError, TypeError) as e:
721
+ raise ValueError(
722
+ f"The value provided for alpha {alpha} is not convertible to dtype '{self.alpha.dtype}'."
723
+ ) from e
724
+
725
+ # Update beta.
726
+ if beta is not None:
727
+ if self.num_operands == 2:
728
+ self._logger.warning("Matmul: The provided beta value %s is ignored since operand C is not specified.", beta)
729
+ else:
730
+ try:
731
+ self.beta[0] = beta
732
+ except (ValueError, TypeError) as e:
733
+ raise ValueError(
734
+ f"The value provided for beta {beta} is not convertible to dtype '{self.beta.dtype}'."
735
+ ) from e
736
+
737
+ _, operand_stream_holder = self._get_or_create_stream_maybe(stream)
738
+
739
+ # Reset the provided operands.
740
+ if a is not None:
741
+ a = tensor_wrapper.wrap_operand(a)
742
+ index = 0
743
+ self._check_and_set_operand(
744
+ a,
745
+ "A",
746
+ operand_stream_holder,
747
+ operand_index=index,
748
+ dtype=self.a_dtype_name,
749
+ extents=self.operand_extents[index],
750
+ strides=self.operand_strides[index],
751
+ )
752
+
753
+ if b is not None:
754
+ b = tensor_wrapper.wrap_operand(b)
755
+ index = 1
756
+ self._check_and_set_operand(
757
+ b,
758
+ "B",
759
+ operand_stream_holder,
760
+ operand_index=index,
761
+ dtype=self.b_dtype_name,
762
+ extents=self.operand_extents[index],
763
+ strides=self.operand_strides[index],
764
+ )
765
+
766
+ if c is not None: # If we get here, we know that C is one of the operands in the problem specification.
767
+ c = tensor_wrapper.wrap_operand(c)
768
+ index = 2
769
+ self._check_and_set_operand(
770
+ c,
771
+ "C",
772
+ operand_stream_holder,
773
+ operand_index=index,
774
+ dtype=self.c_dtype_name,
775
+ extents=self.operand_extents[index],
776
+ strides=self.operand_strides[index],
777
+ )
778
+
779
+ def reset_operands_unchecked(
780
+ self,
781
+ *,
782
+ a=None,
783
+ b=None,
784
+ c=None,
785
+ alpha=None,
786
+ beta=None,
787
+ stream: AnyStream | int | None = None,
788
+ ):
789
+ if alpha is not None:
790
+ self.alpha[0] = alpha
791
+ if beta is not None:
792
+ self.beta[0] = beta
793
+
794
+ if self._memspace_eq_exespace:
795
+ if a is not None:
796
+ self._operands[0].tensor = a
797
+ if b is not None:
798
+ self._operands[1].tensor = b
799
+ if c is not None and self.num_operands == 3:
800
+ self._operands[2].tensor = c
801
+ return
802
+
803
+ operands = ((0, a), (1, b)) if self.num_operands == 2 else ((0, a), (1, b), (2, c))
804
+ _, operand_stream_holder = self._get_or_create_stream_maybe(stream)
805
+ for idx, value in operands:
806
+ if value is None:
807
+ continue
808
+ wrapped = tensor_wrapper.wrap_operand(value)
809
+ _realloc_if_needed_and_copy_to_mirror(
810
+ wrapped, self._operands[idx], self._execution_device_id, operand_stream_holder
811
+ )
812
+ self._operands_backup[idx] = wrapped
813
+
814
+ def _allocate_result_tensor_perhaps(self, *, stream_holder: utils.StreamHolder | None) -> utils.TensorHolder:
815
+ log_debug = self._logger.isEnabledFor(logging.DEBUG)
816
+ # We must handle all valid combinations of:
817
+ # - c-provided and c-not-provided
818
+ # - results in-place and out-of-place
819
+
820
+ # Create empty tensor for the result.
821
+ if self.num_operands == 2 or not self.options.inplace:
822
+ if log_debug:
823
+ self._logger.debug("Beginning output (empty) tensor creation...")
824
+ self._logger.debug(
825
+ f"The output tensor shape = {self._input_traits.d_layout_traits.shape} with strides = "
826
+ f"{self._input_traits.d_layout_traits.strides} and data type '{self.c_dtype_name}'."
827
+ )
828
+ self._logger.debug(
829
+ f"The output tensor has batch dimensions with shape {self._batch_traits[3].shape} "
830
+ f"and strides {self._batch_traits[3].strides}."
831
+ )
832
+ result = utils.create_empty_tensor(
833
+ self._result_class,
834
+ (*self._batch_traits[3].shape, *self._input_traits.d_layout_traits.shape),
835
+ self.c_dtype_name,
836
+ self._execution_device_id,
837
+ stream_holder,
838
+ # verify_strides=False because we need strides to be exactly what we
839
+ # request; not arbitrary if the strides aren't contiguous and dense.
840
+ # Otherwise, the layout parameters will mismatch what we pass to the matmul
841
+ # implementation.
842
+ verify_strides=False,
843
+ strides=(*self._batch_traits[3].strides, *self._input_traits.d_layout_traits.strides),
844
+ )
845
+ if log_debug:
846
+ self._logger.debug("The output (empty) tensor has been created.")
847
+ else: # num_operands == 3 and self.options.inplace
848
+ result = self._operands[2]
849
+ self._logger.debug("The output tensor is C (in-place execution).")
850
+
851
+ if self.num_operands == 3 and not self.options.inplace:
852
+ result.copy_(self._operands[2], stream_holder)
853
+ self._logger.debug("Operand C copied to result tensor (out-of-place execution).")
854
+
855
+ return result
856
+
857
+ def _copy_result_tensor_perhaps(self, *, result: utils.TensorHolder, stream_holder: utils.StreamHolder | None) -> AnyTensor:
858
+ # Return the result and auxiliary outputs, if present.
859
+ if self._memspace_eq_exespace:
860
+ out = result.tensor
861
+ else:
862
+ if self.options.inplace:
863
+ c = self._operands_backup[2]
864
+ assert c is not None and c.tensor is not None, (
865
+ "Internal Error. "
866
+ "Inplace operation was requested, but the execution space was different from the input space, "
867
+ "and we didn't keep a reference to the input tensor."
868
+ )
869
+ c.copy_(result, stream_holder=stream_holder)
870
+ out = c.tensor
871
+ else:
872
+ out = result.to(self._operands_device_id, stream_holder=stream_holder).tensor
873
+
874
+ return out
875
+
876
+ def execute(self, *, log_info, exec_stream_holder, operand_stream_holder) -> AnyTensor:
877
+ result = self._allocate_result_tensor_perhaps(stream_holder=exec_stream_holder)
878
+
879
+ a, b = self._operands[0], self._operands[1]
880
+ if log_info:
881
+ self._logger.info("Starting matrix multiplication...")
882
+ self._logger.info(f"{self._call_prologue}")
883
+
884
+ if self.execution.name == "cuda":
885
+ assert exec_stream_holder is not None
886
+ with utils.cuda_call_ctx(exec_stream_holder, self._blocking, timing=log_info) as (
887
+ _,
888
+ elapsed,
889
+ ):
890
+ self._function(
891
+ a,
892
+ b,
893
+ result,
894
+ self.alpha,
895
+ self.beta,
896
+ exec_stream_holder,
897
+ )
898
+ else:
899
+ with utils.host_call_ctx(timing=log_info) as elapsed:
900
+ self._function(
901
+ a,
902
+ b,
903
+ result,
904
+ self.alpha,
905
+ self.beta,
906
+ exec_stream_holder,
907
+ )
908
+
909
+ if log_info and elapsed.data is not None:
910
+ self._logger.info(f"The matrix multiplication calculation took {elapsed.data:.3f} ms to complete.")
911
+
912
+ return self._copy_result_tensor_perhaps(result=result, stream_holder=operand_stream_holder)
913
+
914
+ def release_operands(self):
915
+ # Same space: _operands hold direct user references.
916
+ # Cross space: _operands hold internal device mirrors,
917
+ # _operands_backup hold direct user references.
918
+ # We preserve the TensorHolder wrappers and only clear the underlying tensor
919
+ # reference. This lets reset_operands_unchecked subsequently reuse the existing
920
+ # wrappers (and, in cross space, an existing device buffer) when present.
921
+ for op in self._operands:
922
+ op.tensor = None
923
+ if not self._memspace_eq_exespace:
924
+ for op in self._operands_backup:
925
+ op.tensor = None
926
+
927
+ def free(self):
928
+ # No ordering needed: CPU execution is blocking, and GPU execution operates on
929
+ # user-owned operands (use-after-free is the user's responsibility).
930
+ for attr in self.__slots__:
931
+ if attr != "_logger":
932
+ setattr(self, attr, None)
933
+
934
+
935
+ def _effective_batch_len(value: Any) -> int | None:
936
+ """Effective broadcast length: None for None, len() for a non-str Sequence, else 1."""
937
+ if value is None:
938
+ return None
939
+ if isinstance(value, Sequence) and not isinstance(value, str):
940
+ return len(value)
941
+ return 1
942
+
943
+
944
+ def _inplace_singleton_c_with_broadcast(a, b, c, *, alpha, beta, qualifiers) -> bool:
945
+ """Return True iff ``c`` is a singleton but at least one of ``a``/``b``/
946
+ ``qualifiers``/``alpha``/``beta`` would drive ``_zip_broadcast`` to create
947
+ more than one group. In that case the same ``C`` buffer is reused across
948
+ groups; under ``inplace=True`` the per-group updates would alias that buffer
949
+ and overlap, so the constructor must reject the configuration."""
950
+ c_is_singleton = not isinstance(c, Sequence) or len(c) == 1
951
+ if not c_is_singleton:
952
+ return False
953
+ return any(isinstance(x, Sequence) and len(x) > 1 for x in (a, b, qualifiers, alpha, beta))
954
+
955
+
956
+ def _zip_broadcast(*args: Any, **kwargs: Any) -> Generator[tuple[tuple[Any, ...], dict[str, Any]], None, None]:
957
+ """Like zip(), but handles keyword args and broadcasts inputs to the longest length.
958
+
959
+ Raises:
960
+ ValueError: If the inputs are not singleton or the same length as longest input.
961
+
962
+ Returns:
963
+ A generator that yields tuples of (args, kwargs), broadcasted to longest length.
964
+ """
965
+ # Pre-process args such that all args are a sequence and they are all the same length or
966
+ # singleton.
967
+ arg_items: list[Sequence[Any]] = []
968
+ kwarg_items: dict[str, Sequence[Any]] = {}
969
+ lengths: list[int] = []
970
+
971
+ # Process positional arguments
972
+ for arg in args:
973
+ item = arg if isinstance(arg, Sequence) and not isinstance(arg, str) else [arg]
974
+ arg_items.append(item)
975
+ lengths.append(len(item))
976
+
977
+ # Process keyword arguments
978
+ for key, value in kwargs.items():
979
+ item = value if isinstance(value, Sequence) else [value]
980
+ kwarg_items[key] = item
981
+ lengths.append(len(item))
982
+
983
+ # lengths may be empty if no args or kwargs are provided
984
+ if not lengths:
985
+ return
986
+
987
+ # num_batch may be 0 if empty sequences are passed to args and kwargs
988
+ # num_batch may be positive if at least one non-empty args or kwargs are provided
989
+ num_batch = max(lengths)
990
+
991
+ # Check that all lengths are either 1 or num_batch; if num_batch is 0, then we already
992
+ # know all args will have length 0.
993
+ if num_batch != 0:
994
+ illegal_args = []
995
+ for i, item in enumerate(arg_items):
996
+ if len(item) not in (1, num_batch):
997
+ illegal_args.append(i)
998
+
999
+ illegal_kwargs = []
1000
+ for key, item in kwarg_items.items():
1001
+ if len(item) not in (1, num_batch):
1002
+ illegal_kwargs.append(key)
1003
+
1004
+ if illegal_args or illegal_kwargs:
1005
+ error_parts = []
1006
+ if illegal_args:
1007
+ error_parts.append(f"positional args at indices {illegal_args}")
1008
+ if illegal_kwargs:
1009
+ error_parts.append(f"keyword args {illegal_kwargs}")
1010
+ raise ValueError(
1011
+ f"All inputs must have the same length or be a singleton. "
1012
+ f"The following do not satisfy this condition: {' and '.join(error_parts)}."
1013
+ )
1014
+
1015
+ # Yield tuples of (args, kwargs) for each batch
1016
+ for batch_idx in range(num_batch):
1017
+ batch_args = tuple(item[batch_idx] if len(item) > 1 else item[0] for item in arg_items)
1018
+ batch_kwargs = {key: item[batch_idx] if len(item) > 1 else item[0] for key, item in kwarg_items.items()}
1019
+ yield (batch_args, batch_kwargs)
1020
+
1021
+
1022
+ class _ExplicitlyBatchedMatmul:
1023
+ """A Matmul object that supports explicitly batched operands.
1024
+
1025
+ This class has the same public API as :class:`Matmul`, except that the arguments may
1026
+ only be explicitly batched. Internally, this class just delegates to
1027
+ :class:`_ImplicitlyBatchedMatmul` for each batch.
1028
+
1029
+ .. important::
1030
+
1031
+ Explicit batches are always defined as a Python Sequence of the same type and
1032
+ implicit batches are always leading dimension of tensors. Otherwise, broadcasting
1033
+ behavior would be ambiguous. For example, if some arguments are sequences of tensors
1034
+ and other arguments are non-sequence tensors with batch dimensions.
1035
+
1036
+ .. seealso:: :func:`_zip_broadcast`, :class:`Matmul`, :class:`_ImplicitlyBatchedMatmul`
1037
+
1038
+ """
1039
+
1040
+ __slots__ = (
1041
+ "_function",
1042
+ "_logger",
1043
+ "_original_batch_lens",
1044
+ "batches",
1045
+ "d_Aarray",
1046
+ "d_Barray",
1047
+ "d_Carray",
1048
+ "execution",
1049
+ "h_Aarray",
1050
+ "h_Barray",
1051
+ "h_Carray",
1052
+ "last_compute_event",
1053
+ "options",
1054
+ "workspace_stream",
1055
+ )
1056
+
1057
+ batches: Final[Sequence[_ImplicitlyBatchedMatmul]]
1058
+ _logger: Final[LoggerLike]
1059
+ execution: ExecutionCPU | ExecutionCUDA
1060
+
1061
+ def _get_or_create_stream_maybe(
1062
+ self, stream: utils.AnyStream
1063
+ ) -> tuple[utils.StreamHolder | None, utils.StreamHolder | None]:
1064
+ return self.batches[0]._get_or_create_stream_maybe(stream)
1065
+
1066
+ def __init__(
1067
+ self,
1068
+ a: Sequence[AnyTensor] | AnyTensor,
1069
+ b: Sequence[AnyTensor] | AnyTensor,
1070
+ /,
1071
+ c: Sequence[AnyTensor] | AnyTensor | None = None,
1072
+ *,
1073
+ alpha: float | complex | Sequence[float | complex] | None = None,
1074
+ beta: float | complex | Sequence[float | complex] | None = None,
1075
+ qualifiers: Sequence[MatrixQualifier] | MatrixQualifier | None = None,
1076
+ options: MatmulOptions,
1077
+ execution: ExecutionCPU | ExecutionCUDA,
1078
+ exec_stream_holder: utils.StreamHolder | None,
1079
+ operand_stream_holder: utils.StreamHolder | None,
1080
+ ):
1081
+ self.options = options
1082
+ self._logger = self.options.logger
1083
+ self.execution = execution
1084
+
1085
+ assert not (options.inplace and c is None), "Internal Error: Matmul should have rejected inplace=True with c=None."
1086
+ assert not (
1087
+ options.inplace and _inplace_singleton_c_with_broadcast(a, b, c, alpha=alpha, beta=beta, qualifiers=qualifiers)
1088
+ ), "Internal Error: Matmul should have rejected inplace with broadcast c."
1089
+
1090
+ # None entries record "operand not provided" — distinguishable from a length-1
1091
+ # supply, so reset_operands can reject attempts to add an operand that wasn't there
1092
+ # originally.
1093
+ self._original_batch_lens: dict[str, int | None] = {
1094
+ "a": _effective_batch_len(a),
1095
+ "b": _effective_batch_len(b),
1096
+ "c": _effective_batch_len(c),
1097
+ "alpha": _effective_batch_len(alpha),
1098
+ "beta": _effective_batch_len(beta),
1099
+ }
1100
+
1101
+ batches: list[_ImplicitlyBatchedMatmul] = []
1102
+ package: str = ""
1103
+ device_id: str | int = ""
1104
+ dtype: str = ""
1105
+ # Our zip functionality needs to be kwarg aware because some args are keyword only!
1106
+ for i, (batch_args, batch_kwargs) in enumerate(
1107
+ _zip_broadcast(
1108
+ a,
1109
+ b,
1110
+ c,
1111
+ qualifiers=qualifiers,
1112
+ alpha=alpha,
1113
+ beta=beta,
1114
+ )
1115
+ ):
1116
+ batch = _ImplicitlyBatchedMatmul(
1117
+ *batch_args,
1118
+ **batch_kwargs,
1119
+ options=options,
1120
+ execution=execution,
1121
+ exec_stream_holder=exec_stream_holder,
1122
+ operand_stream_holder=operand_stream_holder,
1123
+ _allow_mixed_batch_order=True,
1124
+ _logger=_PrefixedLoggerAdapter(options.logger, {"prefix": f"batch {i}"}),
1125
+ )
1126
+ if package == "":
1127
+ package = batch._operands_package
1128
+ elif package != batch._operands_package:
1129
+ raise TypeError(
1130
+ f"Library package mismatch: All operands must come from the same package ({batch._operands_package}) "
1131
+ f"as the original operand ({package})."
1132
+ )
1133
+ if device_id == "":
1134
+ device_id = batch._operands_device_id
1135
+ elif device_id != batch._operands_device_id:
1136
+ raise TypeError(
1137
+ f"All operands must come from the same device ({batch._operands_device_id}) "
1138
+ f"as the original operand ({device_id})."
1139
+ )
1140
+ if dtype == "":
1141
+ dtype = batch.a_dtype_name
1142
+ elif dtype != batch.a_dtype_name:
1143
+ raise ValueError(
1144
+ f"All batches must have the same dtype. Batch {i} has dtype "
1145
+ f"'{batch.a_dtype_name}', but an earlier batch has dtype '{dtype}'."
1146
+ )
1147
+ batches.append(batch)
1148
+ self.batches = batches
1149
+
1150
+ num_pointers_all_groups = sum([max(1, batch._batch_traits[3].count) for batch in self.batches])
1151
+ self.h_Aarray = tensor_wrapper.wrap_operand(np.empty(num_pointers_all_groups, dtype=np.int64))
1152
+ self.h_Barray = tensor_wrapper.wrap_operand(np.empty(num_pointers_all_groups, dtype=np.int64))
1153
+ self.h_Carray = tensor_wrapper.wrap_operand(np.empty(num_pointers_all_groups, dtype=np.int64))
1154
+
1155
+ if self.execution.name == "cuda":
1156
+ # exec_stream_holder.package may be cupy/torch/etc. (not "cuda") for non-numpy
1157
+ # operands. Safe here because NDBufferTensor.empty only reads .obj.
1158
+ assert exec_stream_holder is not None
1159
+ self.d_Aarray = NDBufferTensor.empty(
1160
+ (num_pointers_all_groups,), device_id=self.execution.device_id, dtype="int64", stream_holder=exec_stream_holder
1161
+ )
1162
+ self.d_Barray = NDBufferTensor.empty(
1163
+ (num_pointers_all_groups,), device_id=self.execution.device_id, dtype="int64", stream_holder=exec_stream_holder
1164
+ )
1165
+ self.d_Carray = NDBufferTensor.empty(
1166
+ (num_pointers_all_groups,), device_id=self.execution.device_id, dtype="int64", stream_holder=exec_stream_holder
1167
+ )
1168
+ self.workspace_stream = exec_stream_holder.obj
1169
+ else:
1170
+ self.workspace_stream = None
1171
+ self.last_compute_event = None
1172
+
1173
+ def plan(self, *, stream: AnyStream | int | None = None, log_info=False) -> None:
1174
+ # NOTE: timing context called from Matmul.plan to avoid redundancy
1175
+ group_batch_traits = []
1176
+ group_mm_traits = []
1177
+ group_qualifiers = []
1178
+ flop_count = 0
1179
+
1180
+ for batch in self.batches:
1181
+ mm_layout_checker = mm_layout_checker_getter(batch._qualifiers)
1182
+
1183
+ mm_traits = BLASMMTraitsView.from_input_traits(
1184
+ batch._input_traits,
1185
+ mm_layout_checker,
1186
+ batch._logger,
1187
+ lookup_table_table=CACHED_LAYOUT_CHECKERS,
1188
+ )
1189
+
1190
+ batch._mm_traits = mm_traits
1191
+
1192
+ group_batch_traits.append(batch._batch_traits[3])
1193
+ group_mm_traits.append(batch._mm_traits)
1194
+ group_qualifiers.append(batch._qualifiers)
1195
+
1196
+ if log_info:
1197
+ flop_count += 2 * mm_traits.M * mm_traits.N * mm_traits.K
1198
+
1199
+ self._function, function_name = select_blas_group_mm_function(
1200
+ group_batch_traits,
1201
+ group_mm_traits,
1202
+ group_qualifiers,
1203
+ self._logger,
1204
+ self.execution,
1205
+ )
1206
+
1207
+ if log_info:
1208
+ self._logger.info(f"The plan found 1 suitable algorithm named {function_name}.")
1209
+ self._logger.info(f"The base matrix multiplication FLOP count is {formatters.FLOPSStr(flop_count, 'FLOP')}.")
1210
+
1211
+ def reset_operands(
1212
+ self,
1213
+ *,
1214
+ a=None,
1215
+ b=None,
1216
+ c=None,
1217
+ alpha=None,
1218
+ beta=None,
1219
+ stream: AnyStream | int | None = None,
1220
+ _operands_released=True,
1221
+ ):
1222
+ # An operand value of None means "skip"; otherwise its effective length must equal
1223
+ # what was supplied at construction time, since changing the broadcast type of an
1224
+ # operand may be unsafe.
1225
+ unprovided: list[str] = []
1226
+ mismatches: list[tuple[str, int, int]] = []
1227
+ for name, value in (("a", a), ("b", b), ("c", c), ("alpha", alpha), ("beta", beta)):
1228
+ if value is None:
1229
+ continue
1230
+ new_len = _effective_batch_len(value)
1231
+ orig_len = self._original_batch_lens[name]
1232
+ if orig_len is None:
1233
+ unprovided.append(name)
1234
+ elif new_len != orig_len:
1235
+ assert new_len is not None # value is not None implies new_len is int (mypy)
1236
+ mismatches.append((name, orig_len, new_len))
1237
+ if unprovided:
1238
+ details = ", ".join(unprovided)
1239
+ raise ValueError(
1240
+ f"reset_operands cannot set operand(s) {details} because they were not "
1241
+ "provided to the constructor; reset_operands may only update operands "
1242
+ "that were originally supplied."
1243
+ )
1244
+ if mismatches:
1245
+ details = ", ".join(
1246
+ f"{name} was originally length {orig} but reset_operands received length {new}"
1247
+ for name, orig, new in mismatches
1248
+ )
1249
+ raise ValueError(
1250
+ "reset_operands received operands whose explicit-batch length does not "
1251
+ f"match the original construction: {details}. Each operand's effective "
1252
+ "length (len(seq) for a sequence, otherwise 1) must equal the length "
1253
+ "supplied at construction time; broadcasting cannot be introduced or "
1254
+ "removed by reset_operands."
1255
+ )
1256
+
1257
+ # NOTE: self.batches is included in the zip, so operands are broadcast correctly
1258
+ # when reset.
1259
+ for batch_args, batch_kwargs in _zip_broadcast(
1260
+ self.batches,
1261
+ a=a,
1262
+ b=b,
1263
+ c=c,
1264
+ alpha=alpha,
1265
+ beta=beta,
1266
+ ):
1267
+ batch_args[0].reset_operands(
1268
+ *batch_args[1:],
1269
+ **batch_kwargs,
1270
+ stream=stream,
1271
+ _operands_released=_operands_released,
1272
+ )
1273
+
1274
+ def reset_operands_unchecked(
1275
+ self,
1276
+ *,
1277
+ a=None,
1278
+ b=None,
1279
+ c=None,
1280
+ alpha=None,
1281
+ beta=None,
1282
+ stream: AnyStream | int | None = None,
1283
+ ):
1284
+ for batch_args, batch_kwargs in _zip_broadcast(
1285
+ self.batches,
1286
+ a=a,
1287
+ b=b,
1288
+ c=c,
1289
+ alpha=alpha,
1290
+ beta=beta,
1291
+ ):
1292
+ batch_args[0].reset_operands_unchecked(
1293
+ *batch_args[1:],
1294
+ **batch_kwargs,
1295
+ stream=stream,
1296
+ )
1297
+
1298
+ def execute(self, *, log_info, exec_stream_holder, operand_stream_holder) -> Sequence[AnyTensor]:
1299
+ # Get the data type to determine which function to call
1300
+ dtype = self.batches[0].a_dtype_name
1301
+
1302
+ results: list[utils.TensorHolder] = []
1303
+
1304
+ lo, hi = (0, 0)
1305
+
1306
+ for batch in self.batches:
1307
+ # Allocate result tensors and collect their pointers
1308
+ result = batch._allocate_result_tensor_perhaps(stream_holder=exec_stream_holder)
1309
+ results.append(result)
1310
+
1311
+ # Collect all matrix pointers from each batch
1312
+ num_pointers_this_batch = max(1, batch._batch_traits[3].count)
1313
+ hi += num_pointers_this_batch
1314
+ A_pointers = get_addresses_of_elements(
1315
+ batch._batch_traits[0].shape,
1316
+ batch._batch_traits[0].strides,
1317
+ batch._operands[0].data_ptr,
1318
+ itemsize=batch._operands[0].itemsize,
1319
+ )
1320
+ B_pointers = get_addresses_of_elements(
1321
+ batch._batch_traits[1].shape,
1322
+ batch._batch_traits[1].strides,
1323
+ batch._operands[1].data_ptr,
1324
+ itemsize=batch._operands[1].itemsize,
1325
+ )
1326
+ C_pointers = get_addresses_of_elements(
1327
+ batch._batch_traits[3].shape,
1328
+ batch._batch_traits[3].strides,
1329
+ result.data_ptr,
1330
+ itemsize=result.itemsize,
1331
+ )
1332
+
1333
+ # Handle broadcast operands within an implicit batch
1334
+ if len(A_pointers) < num_pointers_this_batch:
1335
+ A_pointers = np.tile(A_pointers, num_pointers_this_batch)
1336
+ if len(B_pointers) < num_pointers_this_batch:
1337
+ B_pointers = np.tile(B_pointers, num_pointers_this_batch)
1338
+
1339
+ # Swap A and B pointers if the matrices are swapped because of layout
1340
+ # compatibility
1341
+ if batch._mm_traits.is_swapped_AB:
1342
+ A_pointers, B_pointers = B_pointers, A_pointers
1343
+
1344
+ self.h_Aarray.tensor[lo:hi] = A_pointers
1345
+ self.h_Barray.tensor[lo:hi] = B_pointers
1346
+ self.h_Carray.tensor[lo:hi] = C_pointers
1347
+
1348
+ lo = hi
1349
+
1350
+ # Collect alpha and beta values
1351
+ alpha_array = np.array([batch.alpha[0] for batch in self.batches], dtype=dtype)
1352
+ beta_array = np.array([batch.beta[0] for batch in self.batches], dtype=dtype)
1353
+
1354
+ if log_info:
1355
+ self._logger.info("Starting matrix multiplication...")
1356
+ self._logger.info(f"{self.batches[0]._call_prologue}")
1357
+
1358
+ # Call the appropriate grouped batched gemm function based on dtype
1359
+ if self.execution.name == "cpu":
1360
+ with utils.host_call_ctx(timing=log_info) as elapsed:
1361
+ self._function(
1362
+ self.h_Aarray,
1363
+ self.h_Barray,
1364
+ self.h_Carray,
1365
+ alpha_array,
1366
+ beta_array,
1367
+ exec_stream_holder,
1368
+ )
1369
+ else:
1370
+ assert exec_stream_holder is not None
1371
+
1372
+ # Copy pointer arrays to device using the appropriate wrapper
1373
+ self.d_Aarray.copy_(self.h_Aarray, exec_stream_holder)
1374
+ self.d_Barray.copy_(self.h_Barray, exec_stream_holder)
1375
+ self.d_Carray.copy_(self.h_Carray, exec_stream_holder)
1376
+
1377
+ with utils.cuda_call_ctx(exec_stream_holder, self.batches[0]._blocking, timing=log_info) as (
1378
+ self.last_compute_event,
1379
+ elapsed,
1380
+ ):
1381
+ self._function(
1382
+ self.d_Aarray,
1383
+ self.d_Barray,
1384
+ self.d_Carray,
1385
+ alpha_array,
1386
+ beta_array,
1387
+ exec_stream_holder,
1388
+ )
1389
+
1390
+ if log_info and elapsed.data is not None:
1391
+ self._logger.info(f"The matrix multiplication calculation took {elapsed.data:.3f} ms to complete.")
1392
+
1393
+ # Return results (copy if needed)
1394
+ return [
1395
+ batch._copy_result_tensor_perhaps(result=result, stream_holder=operand_stream_holder)
1396
+ for batch, result in zip(self.batches, results, strict=True)
1397
+ ]
1398
+
1399
+ def release_operands(self):
1400
+ for b in self.batches:
1401
+ b.release_operands()
1402
+
1403
+ def free(self):
1404
+ # Ensure ordering with respect to the last computation
1405
+ # to avoid race conditions when releasing internal resources.
1406
+ if self.last_compute_event is not None:
1407
+ if self.workspace_stream is not None:
1408
+ self.workspace_stream.wait(self.last_compute_event)
1409
+ self.last_compute_event = None
1410
+
1411
+ for batch in self.batches:
1412
+ batch.free()
1413
+
1414
+ for attr in self.__slots__:
1415
+ if attr != "_logger":
1416
+ setattr(self, attr, None)
1417
+
1418
+
1419
+ @utils.docstring_decorator(GENERIC_MM_DOCUMENTATION, skip_missing=False)
1420
+ class Matmul:
1421
+ """
1422
+ Create a stateful object encapsulating the specified matrix multiplication computation
1423
+ :math:`\\alpha a @ b + \\beta c` and the required resources to perform the operation. A
1424
+ stateful object can be used to amortize the cost of preparation (planning in the case of
1425
+ matrix multiplication) across multiple executions (also see the :ref:`Stateful APIs
1426
+ <host api types>` section).
1427
+
1428
+ The function-form API :func:`matmul` is a convenient alternative to using stateful
1429
+ objects for *single* use (the user needs to perform just one matrix multiplication, for
1430
+ example), in which case there is no possibility of amortizing preparatory costs. The
1431
+ function-form APIs are just convenience wrappers around the stateful object APIs.
1432
+
1433
+ Using the stateful object typically involves the following steps:
1434
+
1435
+ 1. **Problem Specification**: Initialize the object with a defined operation and
1436
+ options.
1437
+ 2. **Preparation**: Use :meth:`plan` to determine the best algorithmic implementation
1438
+ for this specific matrix multiplication operation.
1439
+ 3. **Execution**: Perform the matrix multiplication computation with :meth:`execute`.
1440
+
1441
+ Detailed information on what's happening in the various phases described above can be
1442
+ obtained by passing in a :class:`logging.Logger` object to :class:`MatmulOptions` or by
1443
+ setting the appropriate options in the root logger object, which is used by default:
1444
+
1445
+ >>> import logging
1446
+ >>> logging.basicConfig(
1447
+ ... level=logging.INFO,
1448
+ ... format="%(asctime)s %(levelname)-8s %(message)s",
1449
+ ... datefmt="%m-%d %H:%M:%S",
1450
+ ... )
1451
+
1452
+ A user can select the desired logging level and, in general, take advantage of all of
1453
+ the functionality offered by the Python `logging` module.
1454
+
1455
+ Args:
1456
+ a: {a}
1457
+
1458
+ b: {b}
1459
+
1460
+ c: {c}
1461
+
1462
+ alpha: {alpha}
1463
+
1464
+ beta: {beta}
1465
+
1466
+ qualifiers: {qualifiers}
1467
+
1468
+ options: {options}
1469
+
1470
+ execution: {execution}
1471
+
1472
+ stream: {stream}
1473
+
1474
+ Semantics:
1475
+ {semantics}
1476
+
1477
+ Thread Safety:
1478
+ {thread_safety}
1479
+
1480
+ .. seealso::
1481
+ :meth:`reset_operands`, :meth:`reset_operands_unchecked`, :meth:`execute`
1482
+
1483
+ Examples:
1484
+
1485
+ >>> import numpy as np
1486
+ >>> import nvmath
1487
+
1488
+ Create two 2-D float64 ndarrays on the CPU:
1489
+
1490
+ >>> M, N, K = 1024, 1024, 1024
1491
+ >>> a = np.random.rand(M, K)
1492
+ >>> b = np.random.rand(K, N)
1493
+
1494
+ We will define a matrix multiplication operation using the generic matrix
1495
+ multiplication interface.
1496
+
1497
+ Create a Matmul object encapsulating the problem specification above:
1498
+
1499
+ >>> mm = nvmath.linalg.Matmul(a, b)
1500
+
1501
+ Options can be provided above to control the behavior of the operation using the
1502
+ `options` argument (see :class:`MatmulOptions`).
1503
+
1504
+ Next, plan the operation. The operands' layouts, qualifiers, and dtypes will be
1505
+ considered to select an appropriate matrix multiplication:
1506
+
1507
+ >>> mm.plan()
1508
+
1509
+ Now execute the matrix multiplication, and obtain the result `r1` as a NumPy
1510
+ ndarray.
1511
+
1512
+ >>> r1 = mm.execute()
1513
+
1514
+ Note that all :class:`Matmul` methods execute on the current stream by default.
1515
+ Alternatively, the `stream` argument can be used to run a method on a specified
1516
+ stream.
1517
+
1518
+ Let's now look at the same problem with CuPy ndarrays on the GPU.
1519
+
1520
+ Create a 2-D CuPy ndarray on the GPU:
1521
+
1522
+ >>> import cupy as cp
1523
+ >>> a = cp.random.rand(M, K)
1524
+ >>> b = cp.random.rand(K, N)
1525
+
1526
+ Create an Matmul object encapsulating the problem specification described earlier
1527
+ and use it as a context manager.
1528
+
1529
+ >>> with nvmath.linalg.Matmul(a, b) as mm:
1530
+ ... # Plan the operation.
1531
+ ... mm.plan()
1532
+ ...
1533
+ ... # Execute the operation to get the first result.
1534
+ ... r1 = mm.execute()
1535
+ ...
1536
+ ... # Update operands 'a' and 'b' in-place (see reset_operands() for an
1537
+ ... # alternative).
1538
+ ... a[:] = cp.random.rand(M, K)
1539
+ ... b[:] = cp.random.rand(K, N)
1540
+ ...
1541
+ ... # Execute the operation to get the new result.
1542
+ ... r2 = mm.execute()
1543
+
1544
+
1545
+ All the resources used by the object are released at the end of the block.
1546
+
1547
+ Further examples can be found in the `examples/linalg/generic/matmul
1548
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/linalg/generic/matmul>`_
1549
+ directory.
1550
+ """
1551
+
1552
+ __slots__ = (
1553
+ "_delegate",
1554
+ "_has_plan",
1555
+ "_logger",
1556
+ "_operands_released",
1557
+ "options",
1558
+ "valid_state",
1559
+ )
1560
+
1561
+ _delegate: Final[_ImplicitlyBatchedMatmul | _ExplicitlyBatchedMatmul]
1562
+
1563
+ def __init__(
1564
+ self,
1565
+ a: AnyTensor | Sequence[AnyTensor],
1566
+ b: AnyTensor | Sequence[AnyTensor],
1567
+ /,
1568
+ c: AnyTensor | Sequence[AnyTensor] | None = None,
1569
+ *,
1570
+ alpha: float | complex | Sequence[float | complex] | None = None,
1571
+ beta: float | complex | Sequence[float | complex] | None = None,
1572
+ qualifiers: MatrixQualifier | Sequence[MatrixQualifier] | None = None,
1573
+ options: MatmulOptions | dict[str, Any] | None = None,
1574
+ execution: ExecutionCPU | ExecutionCUDA | str | None = None,
1575
+ stream: AnyStream | int | None = None,
1576
+ ):
1577
+ if isinstance(options, Sequence):
1578
+ raise TypeError("options must be a single value.")
1579
+ if isinstance(execution, Sequence) and not isinstance(execution, str):
1580
+ raise TypeError("execution must be a single value.")
1581
+ if isinstance(stream, Sequence):
1582
+ raise TypeError("stream must be a single value.")
1583
+
1584
+ options = utils.check_or_create_options(MatmulOptions, options, "Matrix multiplication options")
1585
+ assert options is not None
1586
+ self.options = options
1587
+ self._logger = self.options.logger
1588
+ self._logger.info("= SPECIFICATION PHASE =")
1589
+
1590
+ if (
1591
+ isinstance(a, Sequence)
1592
+ and isinstance(b, Sequence)
1593
+ and (c is None or isinstance(c, Sequence))
1594
+ and (qualifiers is None or isinstance(qualifiers, Sequence))
1595
+ ):
1596
+ self._logger.info("The matrix multiplication is explicitly batched.")
1597
+ delegate_class: type = _ExplicitlyBatchedMatmul
1598
+ elif (
1599
+ not isinstance(a, Sequence)
1600
+ and not isinstance(b, Sequence)
1601
+ and (c is None or not isinstance(c, Sequence))
1602
+ and (qualifiers is None or not isinstance(qualifiers, Sequence))
1603
+ ):
1604
+ self._logger.info("The matrix multiplication is implicitly batched.")
1605
+ delegate_class = _ImplicitlyBatchedMatmul
1606
+ else:
1607
+ raise ValueError(
1608
+ "All arguments a, b, c, and qualifiers must be either "
1609
+ "implicitly batched (tensors) or explicitly batched (sequences of tensors). "
1610
+ f"a is a {type(a)}, "
1611
+ f"b is a {type(b)}, "
1612
+ f"c is a {type(c)}, and "
1613
+ f"qualifiers is a {type(qualifiers)}."
1614
+ )
1615
+
1616
+ empty = [
1617
+ name
1618
+ for name, value in (("a", a), ("b", b), ("c", c), ("qualifiers", qualifiers))
1619
+ if isinstance(value, Sequence) and len(value) == 0
1620
+ ]
1621
+ if empty:
1622
+ raise ValueError(
1623
+ "At least one batch must be provided for explicitly batched matrix multiplication. "
1624
+ f"The following argument(s) are empty: {', '.join(empty)}."
1625
+ )
1626
+
1627
+ nested = [
1628
+ name
1629
+ for name, value in (("a", a), ("b", b), ("c", c), ("qualifiers", qualifiers))
1630
+ if isinstance(value, Sequence) and isinstance(value[0], Sequence)
1631
+ ]
1632
+ if nested:
1633
+ raise ValueError(
1634
+ "Explicitly batched operands cannot be nested (sequence of sequences). "
1635
+ f"The following argument(s) are nested: {', '.join(nested)}."
1636
+ )
1637
+
1638
+ if options.inplace:
1639
+ if c is None:
1640
+ raise ValueError("Operation cannot be inplace if operand C is not provided.")
1641
+ if delegate_class is _ExplicitlyBatchedMatmul and _inplace_singleton_c_with_broadcast(
1642
+ a, b, c, alpha=alpha, beta=beta, qualifiers=qualifiers
1643
+ ):
1644
+ raise ValueError("Operation cannot be inplace if operand C is broadcast.")
1645
+
1646
+ representatives = []
1647
+ for operand in (a, b, c):
1648
+ if operand is None:
1649
+ continue
1650
+ representative = operand[0] if isinstance(operand, Sequence) else operand
1651
+ representatives.append(tensor_wrapper.wrap_operand(representative))
1652
+ operands_device_id = utils.get_operands_device_id(representatives)
1653
+
1654
+ execution = _resolve_execution(execution, operands_device_id)
1655
+
1656
+ self._logger.info(
1657
+ f"The input tensors are located on device {operands_device_id}, and the execution space "
1658
+ f"is {execution.name}, with device {getattr(execution, 'device_id', 'cpu')}."
1659
+ )
1660
+
1661
+ operands_package = utils.get_operands_package(representatives)
1662
+ internal_op_package = _resolve_internal_operand_package(execution.name, operands_package)
1663
+ exec_stream_holder, operand_stream_holder = _get_or_create_stream_maybe(
1664
+ execution,
1665
+ operands_device_id,
1666
+ operands_package,
1667
+ internal_op_package,
1668
+ stream,
1669
+ )
1670
+ holder = exec_stream_holder or operand_stream_holder
1671
+ self._logger.info(f"The specified stream for the Matmul ctor is {holder.obj if holder else None}.")
1672
+
1673
+ self._delegate = delegate_class(
1674
+ a,
1675
+ b,
1676
+ c=c,
1677
+ alpha=alpha,
1678
+ beta=beta,
1679
+ qualifiers=qualifiers,
1680
+ options=options,
1681
+ execution=execution,
1682
+ exec_stream_holder=exec_stream_holder,
1683
+ operand_stream_holder=operand_stream_holder,
1684
+ )
1685
+ self._has_plan = False
1686
+ # Track whether user-provided operands have been released
1687
+ self._operands_released = False
1688
+ self.valid_state = True
1689
+ self._logger.info("The Matmul operation has been created.")
1690
+
1691
+ def _check_valid_matmul(self, *args, **kwargs):
1692
+ """
1693
+ Check if the Matmul object is alive and well.
1694
+ """
1695
+ if not self.valid_state:
1696
+ raise InvalidMatmulState("The Matmul object cannot be used after resources are free'd")
1697
+
1698
+ def _check_planned(self, *args, **kwargs):
1699
+ """
1700
+ Check if plan() has been called on this Matmul object.
1701
+ """
1702
+ what = kwargs["what"]
1703
+ if not self._has_plan:
1704
+ raise RuntimeError(f"{what} cannot be performed before plan() has been called.")
1705
+
1706
+ def _check_valid_operands(self, *args, **kwargs):
1707
+ """
1708
+ Check if operands are available for operations.
1709
+ """
1710
+ if self._operands_released:
1711
+ raise ValueError(
1712
+ "Operands have been released. Call reset_operands() to provide new operands before using this method."
1713
+ )
1714
+
1715
+ @utils.precondition(_check_valid_matmul)
1716
+ @utils.precondition(_check_planned, "Execution")
1717
+ @utils.precondition(_check_valid_operands)
1718
+ def execute(self, *, stream: AnyStream | int | None = None) -> AnyTensor | Sequence[AnyTensor]:
1719
+ """
1720
+ Execute a prepared (planned) matrix multiplication.
1721
+
1722
+ Args:
1723
+ stream: {stream}
1724
+
1725
+ Returns:
1726
+ {result}
1727
+ """
1728
+ exec_stream_holder, operand_stream_holder = self._delegate._get_or_create_stream_maybe(stream)
1729
+
1730
+ if log_info := self._logger.isEnabledFor(logging.INFO):
1731
+ self._logger.info("= EXECUTION PHASE =")
1732
+ self._logger.info(
1733
+ "The specified stream for execute() is "
1734
+ f"{getattr(exec_stream_holder or operand_stream_holder, 'obj', 'no stream')}."
1735
+ )
1736
+
1737
+ return self._delegate.execute(
1738
+ log_info=log_info,
1739
+ exec_stream_holder=exec_stream_holder,
1740
+ operand_stream_holder=operand_stream_holder,
1741
+ )
1742
+
1743
+ def execute_unchecked(self, *, stream: AnyStream | int | None = None) -> AnyTensor | Sequence[AnyTensor]:
1744
+ """
1745
+ .. experimental:: method
1746
+
1747
+ .. versionadded:: 1.0
1748
+
1749
+ This method is a performance-optimized alternative to :meth:`execute` that
1750
+ eliminates validation and logging overhead, making it ideal for performance-critical
1751
+ loops where operand compatibility is guaranteed by the caller.
1752
+
1753
+ Args:
1754
+ stream: {stream}
1755
+
1756
+ Returns:
1757
+ {result}
1758
+
1759
+ .. seealso::
1760
+ :meth:`execute`: Safe, validated method for executing the matrix multiplication.
1761
+ """
1762
+ exec_stream_holder, operand_stream_holder = self._delegate._get_or_create_stream_maybe(stream)
1763
+
1764
+ return self._delegate.execute(
1765
+ log_info=False, exec_stream_holder=exec_stream_holder, operand_stream_holder=operand_stream_holder
1766
+ )
1767
+
1768
+ @utils.precondition(_check_valid_matmul)
1769
+ def plan(self, *, stream: AnyStream | int | None = None) -> None:
1770
+ """
1771
+ Plan the matrix multiplication operation.
1772
+
1773
+ Unlike :py:meth:`nvmath.linalg.advanced.Matmul.plan`, this method takes no
1774
+ tuning parameters. Its primary function is to find the correct matrix multiplication
1775
+ implementation based on the operands and options provided to the constructor.
1776
+
1777
+ Args:
1778
+ stream: {stream}
1779
+
1780
+ Returns:
1781
+ None
1782
+ """
1783
+ if log_info := self._logger.isEnabledFor(logging.INFO):
1784
+ self._logger.info("= PLANNING PHASE =")
1785
+ if self._has_plan:
1786
+ self._logger.info("Matmul is already planned. Replanning is skipped.")
1787
+ else:
1788
+ self._logger.info("Starting matrix multiplication planning...")
1789
+
1790
+ if self._has_plan:
1791
+ return
1792
+
1793
+ with utils.host_call_ctx(timing=log_info) as elapsed:
1794
+ self._delegate.plan(stream=stream, log_info=log_info)
1795
+
1796
+ if log_info and elapsed.data is not None:
1797
+ self._logger.info(f"The matrix multiplication planning phase took {elapsed.data:.3f} ms to complete.")
1798
+
1799
+ self._has_plan = True
1800
+
1801
+ @utils.precondition(_check_valid_matmul)
1802
+ def reset_operands(
1803
+ self,
1804
+ *,
1805
+ a=None,
1806
+ b=None,
1807
+ c=None,
1808
+ alpha=None,
1809
+ beta=None,
1810
+ stream: AnyStream | int | None = None,
1811
+ ):
1812
+ """
1813
+ Reset one or more of the operands held by this :class:`Matmul` instance.
1814
+
1815
+ .. versionchanged:: 0.9
1816
+ All parameters are now keyword-only.
1817
+
1818
+ Args:
1819
+ a: {a}
1820
+
1821
+ b: {b}
1822
+
1823
+ c: {c}
1824
+
1825
+ alpha: {alpha}
1826
+
1827
+ beta: {beta}
1828
+
1829
+ stream: {stream}
1830
+
1831
+ Semantics:
1832
+ - Only the operands explicitly passed are updated. At least one operand
1833
+ is required (all of them after :meth:`release_operands`), otherwise
1834
+ a :class:`ValueError` is raised.
1835
+
1836
+ - This method will perform various checks on the new operands to make sure:
1837
+
1838
+ - The explicit batch counts, shapes, strides, datatypes match
1839
+ those of the old ones.
1840
+ - The packages that the operands belong to match those of the old ones.
1841
+ - If input tensors are on GPU, the device must match.
1842
+
1843
+ Examples:
1844
+
1845
+ >>> import cupy as cp
1846
+ >>> import nvmath
1847
+
1848
+ Create two 3-D float64 ndarrays on the GPU:
1849
+
1850
+ >>> M, N, K = 128, 128, 256
1851
+ >>> a = cp.random.rand(M, K)
1852
+ >>> b = cp.random.rand(K, N)
1853
+
1854
+ Create an matrix multiplication object as a context manager
1855
+
1856
+ >>> with nvmath.linalg.Matmul(a, b) as mm:
1857
+ ... # Plan the operation.
1858
+ ... mm.plan()
1859
+ ...
1860
+ ... # Execute the MM to get the first result.
1861
+ ... r1 = mm.execute()
1862
+ ...
1863
+ ... # Reset the operands to new CuPy ndarrays.
1864
+ ... a_new = cp.random.rand(M, K)
1865
+ ... b_new = cp.random.rand(K, N)
1866
+ ... mm.reset_operands(a=a_new, b=b_new)
1867
+ ...
1868
+ ... # Execute to get the new result corresponding to the updated operands.
1869
+ ... r2 = mm.execute()
1870
+
1871
+ Note that if only a subset of operands are reset, the operands that are not
1872
+ reset hold their original values.
1873
+
1874
+ With :meth:`reset_operands`, minimal overhead is achieved as problem
1875
+ specification and planning are only performed once.
1876
+
1877
+ For the particular example above, a slower alternative to calling
1878
+ :meth:`reset_operands` would be to modify the operands in-place (e.g.,
1879
+ ``a[:] = a_new`` and ``b[:] = b_new``), but this approach is less efficient
1880
+ as it involves copying data rather than just updating pointers.
1881
+
1882
+ For more details, please refer to `inplace update example
1883
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/linalg/generic/matmul/example05_stateful_inplace.py>`_.
1884
+ """
1885
+ # When operands have not been released and all arguments are None, there is
1886
+ # nothing to update, so reject the call. In the released state we fall through to
1887
+ # the delegate, which enforces post-release completeness.
1888
+ if not self._operands_released and all(arg is None for arg in (a, b, c, alpha, beta)):
1889
+ raise ValueError("reset_operands() requires at least one operand to be provided.")
1890
+
1891
+ self._delegate.reset_operands(
1892
+ a=a,
1893
+ b=b,
1894
+ c=c,
1895
+ alpha=alpha,
1896
+ beta=beta,
1897
+ stream=stream,
1898
+ # _operands_released is forwarded to the delegate so it can enforce
1899
+ # the post-release requirement that reset_operands() must receive every
1900
+ # operand so the operation has a fully valid state to execute on.
1901
+ _operands_released=self._operands_released,
1902
+ )
1903
+ # Clear the released flag since we now have valid operands, this is
1904
+ # guaranteed since the delegate above raised if the post-release
1905
+ # contract was violated.
1906
+ self._operands_released = False
1907
+
1908
+ def reset_operands_unchecked(
1909
+ self,
1910
+ *,
1911
+ a=None,
1912
+ b=None,
1913
+ c=None,
1914
+ alpha=None,
1915
+ beta=None,
1916
+ stream: AnyStream | int | None = None,
1917
+ ):
1918
+ """
1919
+ {reset_operands_unchecked}
1920
+ """
1921
+ # The unchecked path waives all validation, so forwarding _operands_released
1922
+ # is not needed because the parameter would go unused anyway.
1923
+ # The caller is responsible for providing a complete and consistent operand set.
1924
+ self._delegate.reset_operands_unchecked(a=a, b=b, c=c, alpha=alpha, beta=beta, stream=stream)
1925
+ self._operands_released = False
1926
+
1927
+ @utils.precondition(_check_valid_matmul)
1928
+ def release_operands(self):
1929
+ """
1930
+ {release_operands}
1931
+ """
1932
+ if self._operands_released:
1933
+ self._logger.info("Operands have already been released; nothing to do.")
1934
+ return
1935
+ self._delegate.release_operands()
1936
+ self._operands_released = True
1937
+ self._logger.info("User-provided operands have been released.")
1938
+
1939
+ def free(self):
1940
+ """Free Matmul resources.
1941
+
1942
+ It is recommended that the :class:`Matmul` object be used within a context,
1943
+ but if it is not possible then this method must be called explicitly to ensure
1944
+ that the Matmul object doesn't hold unnecessary references to operands.
1945
+ """
1946
+ if not self.valid_state:
1947
+ return
1948
+ try:
1949
+ self._delegate.free()
1950
+ except Exception as e:
1951
+ self._logger.critical("Internal error: only part of the Matmul object's resources have been released.")
1952
+ self._logger.critical(str(e))
1953
+ raise e
1954
+ finally:
1955
+ self.valid_state = False
1956
+
1957
+ self._logger.info("The Matmul object's resources have been released.")
1958
+
1959
+ def __enter__(self):
1960
+ return self
1961
+
1962
+ def __exit__(self, *args, **kwargs):
1963
+ self.free()
1964
+
1965
+
1966
+ @utils.docstring_decorator(GENERIC_MM_DOCUMENTATION, skip_missing=False)
1967
+ def matmul(
1968
+ a: AnyTensor | Sequence[AnyTensor],
1969
+ b: AnyTensor | Sequence[AnyTensor],
1970
+ /,
1971
+ c: AnyTensor | Sequence[AnyTensor] | None = None,
1972
+ *,
1973
+ alpha: float | complex | Sequence[float | complex] | None = None,
1974
+ beta: float | complex | Sequence[float | complex] | None = None,
1975
+ qualifiers: MatrixQualifier | Sequence[MatrixQualifier] | None = None,
1976
+ options: MatmulOptions | dict[str, Any] | None = None,
1977
+ execution: ExecutionCPU | ExecutionCUDA | str | None = None,
1978
+ stream: AnyStream | int | None = None,
1979
+ ) -> AnyTensor | Sequence[AnyTensor]:
1980
+ """
1981
+ Perform the specified matrix multiplication computation :math:`\\alpha a @ b + \\beta
1982
+ c`. This function-form is a wrapper around the stateful :class:`Matmul` object APIs and
1983
+ is meant for *single* use (the user needs to perform just one matrix multiplication, for
1984
+ example), in which case there is no possibility of amortizing preparatory costs.
1985
+
1986
+ Detailed information on what's happening within this function can be obtained by passing
1987
+ in a :class:`logging.Logger` object to :class:`MatmulOptions` or by setting the
1988
+ appropriate options in the root logger object, which is used by default:
1989
+
1990
+ >>> import logging
1991
+ >>> logging.basicConfig(
1992
+ ... level=logging.INFO,
1993
+ ... format="%(asctime)s %(levelname)-8s %(message)s",
1994
+ ... datefmt="%m-%d %H:%M:%S",
1995
+ ... )
1996
+
1997
+ A user can select the desired logging level and, in general, take advantage of all of
1998
+ the functionality offered by the Python `logging` module.
1999
+
2000
+ Args:
2001
+ a: {a}
2002
+
2003
+ b: {b}
2004
+
2005
+ c: {c}
2006
+
2007
+ alpha: {alpha}
2008
+
2009
+ beta: {beta}
2010
+
2011
+ qualifiers: {qualifiers}
2012
+
2013
+ options: {options}
2014
+
2015
+ execution: {execution}
2016
+
2017
+ stream: {stream}
2018
+
2019
+ Returns:
2020
+ {result}
2021
+
2022
+ Semantics:
2023
+ {semantics}
2024
+
2025
+ .. seealso::
2026
+ :class:`Matmul`, :class:`MatmulOptions`, :class:`matrix_qualifiers_dtype`,
2027
+ :class:`MatrixQualifier`
2028
+
2029
+ Examples:
2030
+
2031
+ >>> import cupy as cp
2032
+ >>> import nvmath
2033
+
2034
+ Create three float32 ndarrays on the GPU:
2035
+
2036
+ >>> M, N, K = 128, 64, 256
2037
+ >>> a = cp.random.rand(M, K, dtype=cp.float32)
2038
+ >>> b = cp.random.rand(K, N, dtype=cp.float32)
2039
+ >>> c = cp.random.rand(M, N, dtype=cp.float32)
2040
+
2041
+ Perform the operation :math:`\\alpha a @ b + \\beta c` using :func:`matmul`. The
2042
+ result `r` is also a CuPy float32 ndarray:
2043
+
2044
+ >>> r = nvmath.linalg.matmul(a, b, c, alpha=1.23, beta=0.74)
2045
+
2046
+ The package current stream is used by default, but a stream can be explicitly
2047
+ provided to the Matmul operation. This can be done if the operands are computed on a
2048
+ different stream, for example:
2049
+
2050
+ >>> s = cp.cuda.Stream()
2051
+ >>> with s:
2052
+ ... a = cp.random.rand(M, K)
2053
+ ... b = cp.random.rand(K, N)
2054
+ >>> r = nvmath.linalg.matmul(a, b, stream=s)
2055
+
2056
+ The operation above runs on stream `s` and is ordered with respect to the input
2057
+ computation.
2058
+
2059
+ Create NumPy ndarrays on the CPU.
2060
+
2061
+ >>> import numpy as np
2062
+ >>> a = np.random.rand(M, K)
2063
+ >>> b = np.random.rand(K, N)
2064
+
2065
+ Provide the NumPy ndarrays to :func:`matmul`, with the result also being a NumPy
2066
+ ndarray:
2067
+
2068
+ >>> r = nvmath.linalg.matmul(a, b)
2069
+
2070
+ Notes:
2071
+ - This function is a convenience wrapper around :class:`Matmul` and is
2072
+ specifically meant for *single* use.
2073
+
2074
+ Further examples can be found in the `examples/linalg/generic/matmul
2075
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/linalg/generic/matmul>`_
2076
+ directory.
2077
+ """
2078
+
2079
+ with Matmul(
2080
+ a,
2081
+ b,
2082
+ c=c,
2083
+ alpha=alpha,
2084
+ beta=beta,
2085
+ qualifiers=qualifiers,
2086
+ options=options,
2087
+ execution=execution,
2088
+ stream=stream,
2089
+ ) as mm:
2090
+ mm.plan(stream=stream)
2091
+
2092
+ r = mm.execute(stream=stream)
2093
+
2094
+ return r