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,1301 @@
1
+ # Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+
6
+ __all__ = ("DirectSolver", "direct_solver", "InvalidDirectSolverState", "DirectSolverOptions")
7
+
8
+
9
+ import itertools
10
+ import logging
11
+ from collections.abc import Callable, Sequence
12
+ from typing import Any, TypeAlias
13
+
14
+ import numpy as np
15
+
16
+ from nvmath import memory
17
+ from nvmath._internal.workspace import Workspace, get_pinned_memory_resource
18
+ from nvmath.bindings import cublas, cusolverDn
19
+ from nvmath.internal import tensor_wrapper, typemaps, utils
20
+ from nvmath.internal.tensor_ifc_numpy import NumpyTensor
21
+ from nvmath.linalg._internal import solver_utils
22
+ from nvmath.linalg._internal.solver_utils import SolverBackend
23
+ from nvmath.linalg._internal.utils import get_handle
24
+ from nvmath.linalg.generic import ExecutionCUDA
25
+ from nvmath.linalg.generic._configuration.solver_configuration import DirectSolverOptions
26
+
27
+ AnyTensor: TypeAlias = tensor_wrapper.AnyTensor
28
+
29
+
30
+ # Shared doc fragments for the dense direct linear solver public API.
31
+ GENERIC_DIRECT_SOLVER_DOCUMENTATION: dict = utils.COMMON_SHARED_DOC_MAP.copy()
32
+ GENERIC_DIRECT_SOLVER_DOCUMENTATION.update(
33
+ {
34
+ "a": r"""\
35
+ The dense operand (or sequence of operands) representing the left-hand side (LHS) of the system
36
+ of equations: a square matrix ``a`` (or batch of square ``(n, n)`` matrices) for ``a @ x = b``.
37
+ The LHS may be a (sequence of) :class:`numpy.ndarray`, :class:`cupy.ndarray`, and
38
+ :class:`torch.Tensor`. For *batched* input, ``a`` can also be a single tensor of shape
39
+ ``(*batch_shape, n, n)`` (implicit batching) or a :class:`list` or :class:`tuple` of matrices of
40
+ the *same* shape. Refer to the **Semantics** section for details.\
41
+ """.replace("\n", " "),
42
+ "b": r"""\
43
+ The ndarray/tensor or (sequence of ndarray/tensors) representing the dense right-hand side (RHS) of
44
+ the system of equations. The RHS operand may be a (sequence of) :class:`numpy.ndarray`,
45
+ :class:`cupy.ndarray`, and :class:`torch.Tensor`. Unbatched shapes are ``(n,)`` or ``(n, nrhs)``;
46
+ with batching parallel to ``a``, use a :class:`list` / :class:`tuple` of 1D/2D arrays (explicit) or a
47
+ single tensor ``(*batch_shape, n, nrhs)`` (implicit). The package and device must match ``a``. Refer
48
+ to the **Semantics** section for details.\
49
+ """.replace("\n", " "),
50
+ "options": r"""\
51
+ Specify options for the direct linear solver as a :class:`DirectSolverOptions` object. Alternatively, a
52
+ `dict` containing the parameters for the ``DirectSolverOptions`` constructor can also be
53
+ provided. If not specified, the value will be set to the default-constructed ``DirectSolverOptions``
54
+ object.\
55
+ """.replace("\n", " "),
56
+ "execution": r"""\
57
+ Specify execution space options for the direct solver as an :class:`ExecutionCUDA` object. Alternatively, a
58
+ string (``"cuda"``) or a ``dict`` with a ``"name"`` of ``"cuda"`` and optional
59
+ ``device_id`` relevant to the given execution space. The default execution space is ``"cuda"`` and the
60
+ corresponding :class:`ExecutionCUDA` will be default-constructed on device 0. CPU operands are executed
61
+ on the selected CUDA device as needed.\
62
+ """.replace("\n", " "),
63
+ "result": r"""\
64
+ The result of the specified direct linear solve, ``x``, which has the same batching structure
65
+ as the RHS ``b`` (a single array/tensor, or a sequence of ndarray/tensors, each the same shape as
66
+ the corresponding tensor in ``b``), a dtype matching ``a`` and ``b``, and device and package
67
+ placement consistent with the chosen execution and operands.\
68
+ """.replace("\n", " "),
69
+ "semantics": r"""\
70
+ The dense direct linear solver solves :math:`a @ x = b` for ``x`` given the left-hand side
71
+ (LHS) ``a`` and the right-hand side (RHS) ``b`` (square dense :math:`a`, dense ``b``), using
72
+ an LU factorization and triangular solves on the target device.
73
+
74
+ * **Non-batched:** ``a`` is a square ``(n, n)`` matrix; ``b`` is a vector
75
+ ``(n,)`` or a matrix ``(n, nrhs)`` (one or more right-hand sides). The
76
+ solve runs on the selected CUDA device using **cuSOLVER**.
77
+
78
+ * **Batched** (explicit or implicit): the solve uses **cuBLAS** batched
79
+ APIs, so in one call every batch entry shares the *same* system
80
+ size :math:`n` (all ``(n, n)`` ``a``). For ``b``, explicit batching
81
+ uses per-item shapes ``(n,)`` or ``(n, m)``; implicit batching
82
+ uses ``(*batch_shape, n, nrhs)``. If ``a`` and ``b`` are implicitly batched, they
83
+ must share the same leading ``batch_shape`` where applicable. The LHS
84
+ and RHS batching forms may differ (for example, a sequence of ``a``
85
+ with an implicitly batched ``b``, or the reverse) as long as the number
86
+ of batch entries is the same for ``a`` and ``b``.
87
+
88
+ .. seealso:: :class:`nvmath.sparse.advanced.DirectSolver` for
89
+ :math:`a` as a **sparse** CSR matrix (a different feature set and
90
+ backend).
91
+ """.strip(),
92
+ "direct_solver_note": r""".. note::
93
+
94
+ - Currently supported dtypes are: ``float32``, ``float64``, ``complex64``,
95
+ ``complex128`` for both ``a`` and ``b``.
96
+ - If users provide ``options.handle``, it is used only for non-batched
97
+ execution (cuSOLVER path). In batched execution (cuBLAS path), the
98
+ provided handle is ignored and an internal cached cuBLAS handle is used.
99
+ - When ``inplace_a=True`` or ``inplace_b=True`` for batched execution,
100
+ users are responsible for ensuring that the LHS and RHS batch entries do
101
+ not overlap in memory.
102
+ """,
103
+ "reset_operands_inplace_note": r""".. note::
104
+
105
+ When ``inplace_a=True`` or ``inplace_b=True`` and the input operands
106
+ reside on the GPU, replacement operands must have the same layout as the
107
+ original operands. CPU operands are copied through internal GPU buffers, so
108
+ this layout restriction does not apply to CPU inputs.""",
109
+ }
110
+ )
111
+ GENERIC_DIRECT_SOLVER_DOCUMENTATION["release_operands"] = utils._release_operand_docstring(
112
+ True, version_added=None, execution_methods=("factorize", "solve")
113
+ )
114
+
115
+
116
+ class InvalidDirectSolverState(Exception):
117
+ """
118
+ Raised when a :class:`DirectSolver` instance (or a method on it) is used after
119
+ :meth:`DirectSolver.free` has run and resources are invalid.
120
+ """
121
+
122
+
123
+ @utils.docstring_decorator(GENERIC_DIRECT_SOLVER_DOCUMENTATION, skip_missing=False)
124
+ class DirectSolver:
125
+ r"""
126
+ Create a stateful object that encapsulates the specified dense direct linear solver
127
+ computations and required resources. This object ensures the validity of resources during
128
+ use and releases them when they are no longer needed to prevent misuse.
129
+
130
+ This object encompasses all functionality of the function-form API
131
+ :func:`~nvmath.linalg.direct_solver`, which is a
132
+ convenience wrapper around it. The stateful object also allows amortization of preparatory
133
+ costs (for example :meth:`plan` and an LU :meth:`factorize`) when the same size and layout
134
+ are reused, including after :meth:`reset_operands` to update ``a``/``b``.
135
+
136
+ Using the stateful object typically involves the following steps:
137
+
138
+ 1. **Problem specification**: Initialize the object with ``a``, ``b``, and optional
139
+ ``options`` / ``execution`` / ``stream``.
140
+ 2. **Preparation**: :meth:`plan` in preparation for factorization (see **Semantics**).
141
+ 3. **Execution**: :meth:`factorize`, then :meth:`solve` to obtain ``x`` (or :meth:`solve`
142
+ again with the same ``a`` and an updated ``b`` after :meth:`reset_operands` when the
143
+ matrix or RHS values change without recreating the solver).
144
+ 4. **Resource management**: Release resources with :meth:`free` or by using a ``with`` block.
145
+
146
+ More detail on what each phase is doing (planning, factorization, and solve) can be
147
+ seen in the logs by passing a :class:`logging.Logger` in :class:`DirectSolverOptions`,
148
+ or by configuring the root logger that :class:`DirectSolverOptions` uses by default, for
149
+ example:
150
+
151
+ >>> import logging
152
+ >>> logging.basicConfig(
153
+ ... level=logging.INFO,
154
+ ... format="%(asctime)s %(levelname)-8s %(message)s",
155
+ ... datefmt="%m-%d %H:%M:%S",
156
+ ... )
157
+
158
+ Args:
159
+ a: {a}
160
+ b: {b}
161
+ options: {options}
162
+ execution: {execution}
163
+ stream: {stream}
164
+
165
+ Semantics:
166
+ {semantics}
167
+
168
+ {direct_solver_note}
169
+
170
+ .. seealso:: :func:`~nvmath.linalg.direct_solver`, :class:`DirectSolverOptions`, :class:`ExecutionCUDA`, and
171
+ :class:`nvmath.sparse.advanced.DirectSolver` (sparse ``a``).
172
+
173
+ Examples:
174
+
175
+ Non-batched :class:`numpy.ndarray` operands: build a well-conditioned
176
+ :math:`n \times n` system and solve with :meth:`plan`, :meth:`factorize`, and
177
+ :meth:`solve` inside a context manager so that :meth:`free` is called
178
+ automatically:
179
+
180
+ >>> import numpy as np
181
+ >>> import nvmath.linalg as la
182
+ >>>
183
+ >>> n = 4
184
+ >>> rng = np.random.default_rng(0)
185
+ >>> a = rng.standard_normal((n, n), dtype=np.float64)
186
+ >>> a = a + n * np.eye(n, dtype=np.float64)
187
+ >>> b = np.ones(n, dtype=np.float64)
188
+ >>>
189
+ >>> with la.DirectSolver(a, b) as solver:
190
+ ... solver.plan()
191
+ ... solver.factorize()
192
+ ... x = solver.solve()
193
+ >>> x.shape
194
+ (4,)
195
+
196
+ The same call pattern applies to :class:`cupy.ndarray` and
197
+ :class:`torch.Tensor` on the default CUDA device when the operands
198
+ already live on the GPU, and to batching; see **Semantics** and the
199
+ example-tree link below. Further worked examples (implicit batching,
200
+ :meth:`reset_operands`, streams) are in
201
+ `generic direct solver examples
202
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/linalg/generic/direct_solver>`_
203
+ on GitHub.
204
+
205
+ """ # noqa: W505
206
+
207
+ def __init__(
208
+ self,
209
+ a: AnyTensor,
210
+ b: AnyTensor,
211
+ options: DirectSolverOptions | dict[str, Any] | None = None,
212
+ execution: ExecutionCUDA | str | dict[str, Any] | None = None,
213
+ stream: utils.AnyStream | int | None = None,
214
+ ):
215
+ # Process options.
216
+ self.options: DirectSolverOptions = utils.check_or_create_options(
217
+ DirectSolverOptions, options, "direct linear solver options"
218
+ ) # type: ignore[assignment]
219
+ self.inplace_a = self.options.inplace_a
220
+ self.inplace_b = self.options.inplace_b
221
+
222
+ # Process execution options. The default execution space is CUDA.
223
+ self.execution_options = utils.check_or_create_one_of_options(
224
+ (ExecutionCUDA,), execution, "execution options", default_name="cuda"
225
+ )
226
+
227
+ self.logger = self.options.logger if self.options.logger is not None else logging.getLogger()
228
+ self.logger.info("= SPECIFICATION PHASE =")
229
+
230
+ # Wrap operands, check dtypes/devices, and validate LHS/RHS batching consistency.
231
+ (
232
+ self.lhs_backup,
233
+ self.lhs_batch_info,
234
+ self.rhs_backup,
235
+ self.rhs_batch_info,
236
+ self.n,
237
+ self.nrhs,
238
+ self.package,
239
+ self.input_device_id,
240
+ self.dtype_name,
241
+ lhs_layout,
242
+ rhs_layout,
243
+ ) = solver_utils.parse_solver_operands(
244
+ a,
245
+ b,
246
+ check_inplace_a_layout=self.inplace_a,
247
+ check_inplace_b_layout=self.inplace_b,
248
+ )
249
+ # LHS/RHS batching consistency is already validated in parse_solver_operands.
250
+ # Use LHS metadata for batched flag and batch_count.
251
+ self.batched = self.lhs_batch_info.explicitly_batched or self.lhs_batch_info.implicitly_batched
252
+ self.batch_count = self.lhs_batch_info.batch_count
253
+ self.cuda_dtype = typemaps.NAME_TO_DATA_TYPE[self.dtype_name]
254
+
255
+ # Backend:
256
+ # non-batched → cuSOLVER:
257
+ # - lhs/a: (n, n); rhs/b: (n,) or (n, nrhs)
258
+ # batched → cuBLAS *Batched APIs
259
+ # - Explicit batching:
260
+ # - lhs/a: list/tuple of (n, n);
261
+ # rhs/b: list/tuple of (n,) or (n, nrhs)
262
+ # - Implicit batching:
263
+ # - lhs/a: (*batch_shape, n, n);
264
+ # rhs/b: (*batch_shape, n, nrhs)
265
+ if not self.batched:
266
+ self.backend = SolverBackend.CUSOLVER
267
+ self.logger.info("For non-batched input, execution will be performed using cusolver library.")
268
+ else:
269
+ self.backend = SolverBackend.CUBLAS
270
+ self.logger.info("For batched input, execution will be performed using cublas library.")
271
+
272
+ # Determine the stream package for the execution space.
273
+ self.stream_package = "cuda" if self.package == "numpy" else self.package
274
+
275
+ self.memory_space = solver_utils.get_single_or_sequence_attr(self.lhs_backup, "device")
276
+ if self.memory_space == "cuda":
277
+ # When the operands are on device,
278
+ # we respect the device id of the operands over execution options.
279
+ self.execution_device = solver_utils.get_single_or_sequence_attr(self.lhs_backup, "device_id")
280
+ self.copy_across_memspace = False
281
+ else:
282
+ # When the operands are on CPU,
283
+ # we use the device id from execution options.
284
+ self.execution_device = self.execution_options.device_id
285
+ self.copy_across_memspace = True
286
+ self.input_operand_class = solver_utils.get_single_or_sequence_attr(self.lhs_backup, "__class__")
287
+ if self.copy_across_memspace and self.package == "numpy":
288
+ self.internal_operand_class = self.input_operand_class.device_tensor_class
289
+ else:
290
+ self.internal_operand_class = self.input_operand_class
291
+
292
+ # Get the wrapped input shapes:
293
+ # - For explicit batching, this represents the per-batch shapes.
294
+ # - For implicit batching or non-batched, this represents the full tensor shape.
295
+ self.lhs_shape = solver_utils.get_single_or_sequence_attr(self.lhs_backup, "shape") # type: ignore[assignment]
296
+ self.rhs_shape = solver_utils.get_single_or_sequence_attr(self.rhs_backup, "shape") # type: ignore[assignment]
297
+
298
+ if self.logger.isEnabledFor(logging.INFO):
299
+ if self.batched:
300
+ self.logger.info("Batched problem specification:")
301
+ self.logger.info(f" dtype: {self.dtype_name}")
302
+ self.logger.info(f" lhs shape(s): {self.lhs_shape!r}")
303
+ self.logger.info(f" rhs shape(s): {self.rhs_shape!r}")
304
+ self.logger.info(f" lhs batching: {self.lhs_batch_info}")
305
+ self.logger.info(f" rhs batching: {self.rhs_batch_info}")
306
+ else:
307
+ self.logger.info("Non-batched problem specification:")
308
+ self.logger.info(f" dtype: {self.dtype_name}")
309
+ self.logger.info(f" lhs shape(s): {self.lhs_shape!r}")
310
+ self.logger.info(f" rhs shape(s): {self.rhs_shape!r}")
311
+
312
+ # lhs_backup/rhs_backup wrap the user operands and keep them available for
313
+ # reset/retry flows when their inplace option is False.
314
+ # lhs/rhs are internal execution buffers otherwise.
315
+ stream_holder = utils.get_or_create_stream(self.execution_device, stream, self.stream_package)
316
+
317
+ # Keep track of the lhs/rhs allocation streams to ensure
318
+ # proper ordering in free() and release_operands().
319
+ self.input_lhs_layout = lhs_layout
320
+ self.input_rhs_layout = rhs_layout
321
+
322
+ if self.inplace_a and not self.copy_across_memspace:
323
+ if lhs_layout is None:
324
+ raise RuntimeError("Internal error: inplace_a LHS layout was not computed.")
325
+ self.lhs = self.lhs_backup
326
+ self.lhs_backup = None
327
+ self.lhs_mirror_stream = None
328
+ self.lhs_layout = lhs_layout
329
+ else:
330
+ self.lhs = self._create_empty_lhs(stream_holder)
331
+ solver_utils.copy_operands(self.lhs, self.lhs_backup, stream_holder)
332
+ self.lhs_layout = solver_utils.LHSLayout("F", self.n)
333
+ self.logger.debug("lhs/a has been copied to the execution space for the linear solver...")
334
+ # GETRF always factorizes the column-major view of lhs. For C-order input
335
+ # that view is A.T, so GETRS uses op=T to solve against the original A.
336
+ self.getrs_operation = cublas.Operation.N if self.lhs_layout.order == "F" else cublas.Operation.T
337
+ # Whether the execution LHS buffer contains the same values as the source LHS.
338
+ # For device-resident inplace_a, these are the same allocation, so this remains True
339
+ # and freshness is user-managed.
340
+ self.lhs_synced = True
341
+
342
+ if self.inplace_b and not self.copy_across_memspace:
343
+ if rhs_layout is None:
344
+ raise RuntimeError("Internal error: inplace_b RHS layout was not computed.")
345
+ self.rhs = self.rhs_backup
346
+ self.rhs_backup = None
347
+ self.rhs_mirror_stream = None
348
+ self.rhs_layout = rhs_layout
349
+ else:
350
+ self.rhs = self._create_empty_rhs(stream_holder)
351
+ solver_utils.copy_operands(self.rhs, self.rhs_backup, stream_holder)
352
+ self.rhs_layout = solver_utils.get_rhs_layout(self.rhs, self.nrhs, check_inplace_compatible_strides=False)
353
+ # Whether the execution RHS buffer contains the same values as the source RHS.
354
+ # For device-resident inplace_b, these are the same allocation, so this remains True
355
+ # and freshness is user-managed.
356
+ self.rhs_synced = True
357
+ self.logger.debug("rhs/b has been copied to the execution space for the linear solver...")
358
+
359
+ # Solver library resources (non-batched → cuSOLVER; batched → cuBLAS batched).
360
+ # Typical buffer shapes (batch_count is 1 for non-batched):
361
+ #
362
+ # Resource | cuSOLVER | cuBLAS
363
+ # ---------------+--------------------------+----------------------------------
364
+ # handle | yes if not provided | yes (cached) if not provided
365
+ # params | yes | no
366
+ # getrf_info | (1,) int32 | (batch_count,) int32
367
+ # getrs_info | (1,) int32 | no
368
+ # pivot buffer | (n,) int64 | (batch_count, n) int32
369
+ # lhs_pointers | no | (batch_count,) uint64
370
+ # rhs_pointers | no | (batch_count,) uint64
371
+ #
372
+
373
+ # getrf_info_host.tensor must be an ndarray where we can inspect the info values.
374
+ self.getrf_info_host = utils.create_empty_tensor(NumpyTensor, (self.batch_count,), "int32", "cpu", None, False)
375
+ self.getrf_info_device = self.getrf_info_host.to(self.execution_device, stream_holder)
376
+ self.logger.debug("Allocating info in the execution space for the linear solver factorization...")
377
+
378
+ if self.backend == SolverBackend.CUSOLVER:
379
+ if self.options.handle is None:
380
+ with utils.device_ctx(self.execution_device):
381
+ self.handle = cusolverDn.create()
382
+ self.own_handle = True
383
+ self.logger.info("A cusolverDn handle has been created.")
384
+ else:
385
+ self.handle = self.options.handle
386
+ self.own_handle = False
387
+ self.params = cusolverDn.create_params()
388
+ self.logger.info("A cusolverDn params object has been created.")
389
+
390
+ self.pivot = self._create_internal_operand((self.n,), "int64", stream_holder)
391
+ self.lhs_pointers = None
392
+ self.rhs_pointers = None
393
+ else:
394
+ # When execution is dispatched to cuBLAS, the provided handle is ignored
395
+ # and a cuBLAS handle will be created.
396
+ if self.options.handle is not None:
397
+ self.logger.info("Execution will be performed with cuBLAS, the provided handle is ignored.")
398
+
399
+ # get the cached cublas handle
400
+ self.handle = get_handle(self.execution_device, binding="cublas")
401
+ self.logger.info("A cublas handle has been created.")
402
+
403
+ # cublas does not require params
404
+ self.params = None
405
+ self.own_handle = False
406
+
407
+ self.pivot = self._create_internal_operand((self.batch_count, self.n), "int32", stream_holder)
408
+
409
+ # Per-batch device pointers for cuBLAS *Batched: explicit batching gathers
410
+ # data_ptr from each wrapped tensor; implicit batching uses batch_indices on
411
+ # one strided tensor.
412
+ if self.lhs_batch_info.implicitly_batched:
413
+ self.lhs_batch_indices = tuple(itertools.product(*list(map(range, self.lhs_batch_info.batch_shape)))) # type: ignore[assignment]
414
+ else:
415
+ self.lhs_batch_indices = None # type: ignore[assignment]
416
+ self.lhs_pointers = solver_utils.get_operands_ptr_array(self.lhs, self.lhs_batch_indices).to(
417
+ device_id=self.execution_device, stream_holder=stream_holder
418
+ )
419
+ if self.rhs_batch_info.implicitly_batched:
420
+ self.rhs_batch_indices = tuple(itertools.product(*list(map(range, self.rhs_batch_info.batch_shape)))) # type: ignore[assignment]
421
+ else:
422
+ self.rhs_batch_indices = None # type: ignore[assignment]
423
+ self.rhs_pointers = solver_utils.get_operands_ptr_array(self.rhs, self.rhs_batch_indices).to(
424
+ device_id=self.execution_device, stream_holder=stream_holder
425
+ )
426
+
427
+ # Stream used when allocating handles/pointers above; free() may wait on it.
428
+ self.resource_alloc_stream = stream_holder.obj
429
+
430
+ self.solver_planned = False
431
+ self.solver_factorized = False
432
+ # Last GPU event from factorize/solve; free() waits so teardown cannot race kernels.
433
+ self.last_compute_event = None
434
+
435
+ self._operands_released = False
436
+
437
+ # DirectSolver always synchronizes so factorization info can be inspected safely.
438
+ # TODO: enable asynchronous execution
439
+ self.blocking = True
440
+ self.call_prologue = "This call is blocking and will return only after the operation is complete."
441
+
442
+ # Set memory allocator.
443
+ self.allocator = (
444
+ self.options.allocator
445
+ if self.options.allocator is not None
446
+ else memory._MEMORY_MANAGER[self.stream_package](self.execution_device, self.logger)
447
+ )
448
+
449
+ # resolution deferred until plan() is called
450
+ self._getrf_func: Callable | None = None
451
+ self._getrs_func: Callable | None = None
452
+
453
+ # Workspace lifecycle is managed by `Workspace` (allocate-on-demand,
454
+ # reuse, exception cleanup, stream-ordered free).
455
+ self.device_workspace = Workspace(
456
+ self.allocator,
457
+ self.logger,
458
+ label="device workspace",
459
+ device_id=self.execution_device,
460
+ )
461
+ # cuSOLVER getrf may use host workspace during factorization. Use pinned
462
+ # host memory so Workspace can order its lifetime on the compute stream.
463
+ # Prefer PinnedMemoryResource when runtime support is available; otherwise
464
+ # fall back to synchronous LegacyPinnedMemoryResource.
465
+ host_mr = get_pinned_memory_resource(self.execution_device)
466
+ self.host_workspace = Workspace(host_mr, device_id=self.execution_device, logger=self.logger, label="host workspace")
467
+
468
+ self.valid_state = True
469
+ self.logger.info("The DirectSolver operation has been created.")
470
+
471
+ def _check_valid_solver(self, *args, **kwargs):
472
+ """
473
+ Check if the DirectSolver object is alive and well.
474
+ """
475
+ if not self.valid_state:
476
+ raise InvalidDirectSolverState("The DirectSolver object cannot be used after resources are free'd")
477
+
478
+ def _check_planned(self, *args, **kwargs):
479
+ what = kwargs["what"]
480
+ if not self.solver_planned:
481
+ raise RuntimeError(f"{what} cannot be performed before plan() has been called.")
482
+
483
+ def _check_factorized(self, *args, **kwargs):
484
+ what = kwargs["what"]
485
+ if not self.solver_factorized:
486
+ raise RuntimeError(f"{what} cannot be performed before factorize() has been called.")
487
+
488
+ def _check_valid_operands(self, *args, **kwargs):
489
+ """
490
+ Check if the operands are available for the operation.
491
+ """
492
+ what = kwargs["what"]
493
+ if self._operands_released:
494
+ raise RuntimeError(
495
+ f"{what} cannot be performed after the operands have been released. "
496
+ f"Use reset_operands() to provide new operands before performing the {what.lower()}."
497
+ )
498
+
499
+ def _check_lhs_layout(self, lhs_layout):
500
+ expected_layout = self.input_lhs_layout if self.copy_across_memspace and self.inplace_a else self.lhs_layout
501
+ if lhs_layout != expected_layout:
502
+ raise ValueError(
503
+ f"The layout of 'a' ({lhs_layout}) must match the original layout ({expected_layout}) when inplace_a is True."
504
+ )
505
+
506
+ def _check_rhs_layout(self, rhs_layout):
507
+ expected_layout = self.input_rhs_layout if self.copy_across_memspace and self.inplace_b else self.rhs_layout
508
+ if rhs_layout != expected_layout:
509
+ raise ValueError(
510
+ f"The layout of 'b' ({rhs_layout}) must match the original layout ({expected_layout}) when inplace_b is True."
511
+ )
512
+
513
+ @utils.precondition(_check_valid_solver)
514
+ def release_operands(self):
515
+ """
516
+ {release_operands}
517
+ """
518
+ if self._operands_released:
519
+ self.logger.info("Operands have already been released; nothing to do.")
520
+ return
521
+
522
+ if self.lhs_mirror_stream is not None:
523
+ self.lhs_mirror_stream.wait(self.last_compute_event)
524
+ self.lhs_mirror_stream = None
525
+ if self.rhs_mirror_stream is not None:
526
+ self.rhs_mirror_stream.wait(self.last_compute_event)
527
+ self.rhs_mirror_stream = None
528
+
529
+ def release_wrapped_reference(wrapped_ops):
530
+ if isinstance(wrapped_ops, Sequence):
531
+ for op in wrapped_ops:
532
+ op.tensor = None
533
+ else:
534
+ wrapped_ops.tensor = None
535
+
536
+ # When possible, we keep the TensorHolder wrappers alive and only release the
537
+ # internal tensor reference so that reset_operands_unchecked can reuse them
538
+ # without re-wrapping.
539
+ # When inplace_a is True on device operands, the user operand is self.lhs
540
+ # directly. CPU inplace keeps the user operand in self.lhs_backup.
541
+ if self.inplace_a and not self.copy_across_memspace:
542
+ release_wrapped_reference(self.lhs)
543
+ else:
544
+ release_wrapped_reference(self.lhs_backup)
545
+ self.lhs = None
546
+
547
+ # When inplace_b is True on device operands, the user operand is self.rhs
548
+ # directly. CPU inplace keeps the user operand in self.rhs_backup.
549
+ if self.inplace_b and not self.copy_across_memspace:
550
+ release_wrapped_reference(self.rhs)
551
+ elif self.rhs_backup is not None:
552
+ release_wrapped_reference(self.rhs_backup)
553
+ self.rhs = None
554
+
555
+ self._operands_released = True
556
+ self.logger.info("User-provided operands have been released.")
557
+
558
+ @utils.precondition(_check_valid_solver)
559
+ def reset_operands(self, *, a=None, b=None, stream: utils.AnyStream | int | None = None):
560
+ """
561
+ Replace ``a`` and/or ``b`` with operands that match the *same* problem shape,
562
+ batching, device, package, and dtype as the original.
563
+
564
+ Args:
565
+ a: New left-hand side, or ``None`` to keep the previous left-hand side.
566
+ b: New right-hand side, or ``None`` to keep the previous right-hand side.
567
+ stream: {stream}
568
+
569
+ Semantics:
570
+ - Only the operands explicitly passed are updated. At least one operand
571
+ is required (all of them after :meth:`release_operands`), otherwise
572
+ a :class:`ValueError` is raised.
573
+
574
+ - Supplying a new ``a`` invalidates the current factorization; call
575
+ :meth:`factorize` again before the next :meth:`solve`.
576
+
577
+ {reset_operands_inplace_note}
578
+ """
579
+ # if the operands have been released, both 'a' and 'b' must be provided
580
+ if self._operands_released:
581
+ if a is None or b is None:
582
+ raise ValueError("After release_operands(), both 'a' and 'b' must be provided to reset_operands().")
583
+ elif a is None and b is None:
584
+ # All arguments are None: there is nothing to update, so reject the call.
585
+ raise ValueError("reset_operands() requires at least one operand to be provided.")
586
+
587
+ stream_holder = utils.get_or_create_stream(self.execution_device, stream, self.stream_package)
588
+
589
+ def _check_operands_space(operand, operand_name):
590
+ package = solver_utils.get_single_or_sequence_attr(operand, "name")
591
+ device_id = solver_utils.get_single_or_sequence_attr(operand, "device_id")
592
+ dtype_name = solver_utils.get_single_or_sequence_attr(operand, "dtype")
593
+ if package != self.package:
594
+ raise ValueError(f"The package for {operand_name} ({package}) must match the original one ({self.package}).")
595
+ if device_id != self.input_device_id:
596
+ raise ValueError(
597
+ f"The device id for {operand_name} ({device_id}) must match the original one ({self.input_device_id})."
598
+ )
599
+ if dtype_name != self.dtype_name:
600
+ raise ValueError(
601
+ f"The dtype for {operand_name} ({dtype_name}) must match the original one ({self.dtype_name})."
602
+ )
603
+
604
+ if a is not None:
605
+ lhs, lhs_batch_info, n, lhs_layout = solver_utils.wrap_check_solver_lhs(a, check_inplace_a_layout=self.inplace_a)
606
+ if lhs_batch_info != self.lhs_batch_info:
607
+ raise ValueError(
608
+ f"The batching metadata for 'a' ({lhs_batch_info!r}) must match the original ({self.lhs_batch_info!r})."
609
+ )
610
+ if n != self.n:
611
+ raise ValueError(f"The number of columns for 'a' ({n}) must match the original one ({self.n}).")
612
+ _check_operands_space(lhs, "a")
613
+ if self.inplace_a and not self.copy_across_memspace:
614
+ self._check_lhs_layout(lhs_layout)
615
+ self.lhs = lhs
616
+ else:
617
+ self.lhs_backup = lhs
618
+ if self.lhs is None:
619
+ self.lhs = self._create_empty_lhs(stream_holder)
620
+ solver_utils.copy_operands(self.lhs, self.lhs_backup, stream_holder)
621
+ if self.backend == SolverBackend.CUBLAS:
622
+ solver_utils.update_operands_ptr_array(
623
+ self.lhs_pointers, # type: ignore[arg-type]
624
+ self.lhs,
625
+ stream_holder,
626
+ self.lhs_batch_indices,
627
+ )
628
+ self.lhs_synced = True
629
+ self.solver_factorized = False
630
+ self.logger.info("User-provided lhs/a has been reset.")
631
+ if b is not None:
632
+ rhs, rhs_batch_info, n, nrhs, rhs_layout = solver_utils.wrap_check_solver_rhs(
633
+ b, check_inplace_b_layout=self.inplace_b
634
+ )
635
+ if rhs_batch_info != self.rhs_batch_info:
636
+ raise ValueError(
637
+ f"The batching metadata for 'b' ({rhs_batch_info!r}) must match the original ({self.rhs_batch_info!r})."
638
+ )
639
+ if n != self.n:
640
+ raise ValueError(f"The number of columns for 'b' ({n}) must match the original one ({self.n}).")
641
+ if nrhs != self.nrhs:
642
+ raise ValueError(f"The number of right-hand sides for 'b' ({nrhs}) must match the original one ({self.nrhs}).")
643
+ _check_operands_space(rhs, "b")
644
+ if self.inplace_b and not self.copy_across_memspace:
645
+ self._check_rhs_layout(rhs_layout)
646
+ self.rhs = rhs
647
+ else:
648
+ self.rhs_backup = rhs
649
+ if self.rhs is None:
650
+ self.rhs = self._create_empty_rhs(stream_holder)
651
+ solver_utils.copy_operands(self.rhs, self.rhs_backup, stream_holder)
652
+ if self.backend == SolverBackend.CUBLAS:
653
+ solver_utils.update_operands_ptr_array(
654
+ self.rhs_pointers, # type: ignore[arg-type]
655
+ self.rhs,
656
+ stream_holder,
657
+ self.rhs_batch_indices,
658
+ )
659
+ self.rhs_synced = True
660
+ self.logger.info("User-provided rhs/b has been reset.")
661
+
662
+ self._operands_released = False
663
+
664
+ @utils.precondition(_check_valid_solver)
665
+ def reset_operands_unchecked(self, *, a=None, b=None, stream: utils.AnyStream | int | None = None):
666
+ """
667
+ .. experimental:: method
668
+
669
+ This method is a performance-optimized alternative to :meth:`reset_operands` that
670
+ eliminates validation and logging overhead, making it ideal for
671
+ performance-critical loops where ``a``/``b`` compatibility is guaranteed by the
672
+ caller.
673
+
674
+ This method accepts the same parameters as :meth:`reset_operands`.
675
+
676
+ Args:
677
+ a: New left-hand side, or ``None`` to keep the previous one. The caller must
678
+ guarantee the same invariants :meth:`reset_operands` would enforce; this
679
+ method does not validate.
680
+ b: New right-hand side, or ``None`` to keep the previous one. Same invariants
681
+ as ``a`` where applicable.
682
+ stream: {stream}
683
+
684
+ Returns:
685
+ None
686
+
687
+ Semantics:
688
+ The semantics are the same as in :meth:`reset_operands`, except that this
689
+ method does not perform any validation
690
+ (e.g. batching metadata, ``n``, ``n_rhs``, device, and dtype) or logging.
691
+
692
+ When to Use:
693
+ - Performance-critical loops with repeated :meth:`solve` or operand updates
694
+ when compatibility is already guaranteed
695
+
696
+ - After verifying correctness with :meth:`reset_operands` during development
697
+
698
+ - When ``a``/``b`` compatibility is guaranteed by construction or invariant
699
+
700
+ {reset_operands_inplace_note}
701
+
702
+ .. seealso::
703
+ :meth:`reset_operands`, :meth:`release_operands`
704
+ """
705
+ # If operands have been released, all required operands must be provided
706
+ if self._operands_released and (a is None or b is None):
707
+ raise ValueError("After release_operands(), both 'a' and 'b' must be provided to reset_operands_unchecked().")
708
+
709
+ stream_holder = utils.get_or_create_stream(self.execution_device, stream, self.stream_package)
710
+ if a is not None:
711
+ if self.inplace_a and not self.copy_across_memspace:
712
+ # inplace_a requires operands to be in the execution space,
713
+ # therefore we can directly update self.lhs with the user-provided operands.
714
+ if self.lhs_batch_info.explicitly_batched:
715
+ for lhs_wrapper, lhs_raw in zip(self.lhs, a, strict=True):
716
+ lhs_wrapper.tensor = lhs_raw
717
+ else:
718
+ self.lhs.tensor = a
719
+ else:
720
+ if self.lhs is None:
721
+ self.lhs = self._create_empty_lhs(stream_holder)
722
+ # update the internal tensor references held by the wrappers
723
+ # for the user-provided operands.
724
+ if self.lhs_batch_info.explicitly_batched:
725
+ for lhs_wrapper, lhs_raw in zip(self.lhs_backup, a, strict=True):
726
+ lhs_wrapper.tensor = lhs_raw
727
+ else:
728
+ self.lhs_backup.tensor = a
729
+ solver_utils.copy_operands(self.lhs, self.lhs_backup, stream_holder)
730
+ if self.backend == SolverBackend.CUBLAS:
731
+ solver_utils.update_operands_ptr_array(
732
+ self.lhs_pointers, # type: ignore[arg-type]
733
+ self.lhs,
734
+ stream_holder,
735
+ self.lhs_batch_indices,
736
+ )
737
+ self.lhs_synced = True
738
+ self.solver_factorized = False
739
+ if b is not None:
740
+ if self.inplace_b and not self.copy_across_memspace:
741
+ # inplace_b requires operands to be in the execution space,
742
+ # therefore we can directly update self.rhs with the user-provided operands.
743
+ if self.rhs_batch_info.explicitly_batched:
744
+ for rhs_wrapper, rhs_raw in zip(self.rhs, b, strict=True):
745
+ rhs_wrapper.tensor = rhs_raw
746
+ else:
747
+ self.rhs.tensor = b
748
+ else:
749
+ if self.rhs is None:
750
+ self.rhs = self._create_empty_rhs(stream_holder)
751
+ # update the internal tensor references held by the wrappers
752
+ # for the user-provided operands.
753
+ if self.rhs_batch_info.explicitly_batched:
754
+ for rhs_wrapper, rhs_raw in zip(self.rhs_backup, b, strict=True):
755
+ rhs_wrapper.tensor = rhs_raw
756
+ else:
757
+ self.rhs_backup.tensor = b
758
+ solver_utils.copy_operands(self.rhs, self.rhs_backup, stream_holder)
759
+ if self.backend == SolverBackend.CUBLAS:
760
+ solver_utils.update_operands_ptr_array(
761
+ self.rhs_pointers, # type: ignore[arg-type]
762
+ self.rhs,
763
+ stream_holder,
764
+ self.rhs_batch_indices,
765
+ )
766
+ self.rhs_synced = True
767
+ self._operands_released = False
768
+
769
+ @utils.precondition(_check_valid_solver)
770
+ @utils.precondition(_check_valid_operands, "Planning")
771
+ def plan(self, stream: utils.AnyStream | int | None = None):
772
+ """
773
+ Plan the dense direct solve. This is the **preparation** step before numerical
774
+ factorization (any sizing or setup required for the current operands on the
775
+ selected device). After planning, the object is ready for :meth:`factorize`.
776
+
777
+ In the class-based workflow, call :meth:`plan` once before calling
778
+ :meth:`factorize` and :meth:`solve`.
779
+
780
+ Args:
781
+ stream: {stream}
782
+
783
+ .. seealso::
784
+ :meth:`factorize`, :meth:`solve`.
785
+ """
786
+ self.logger.info("= PLANNING PHASE =")
787
+ if self.solver_planned:
788
+ self.logger.info("Skipping planning since it has already been performed in a previous call.")
789
+ return
790
+
791
+ if self.backend == SolverBackend.CUBLAS:
792
+ # cublas does not require workspace memory for cublasXgetrf/getrsBatched
793
+ self.logger.info("For batched input, workspace memory allocation is not required for cublasXgetrf/getrsBatched.")
794
+ self.device_workspace.set_size(0)
795
+ self.host_workspace.set_size(0)
796
+ # resolve the function pointers for the batched getrf/getrs functions
797
+ match self.dtype_name:
798
+ case "float32":
799
+ self._getrf_func = cublas.sgetrf_batched
800
+ self._getrs_func = cublas.sgetrs_batched
801
+ case "float64":
802
+ self._getrf_func = cublas.dgetrf_batched
803
+ self._getrs_func = cublas.dgetrs_batched
804
+ case "complex64":
805
+ self._getrf_func = cublas.cgetrf_batched
806
+ self._getrs_func = cublas.cgetrs_batched
807
+ case "complex128":
808
+ self._getrf_func = cublas.zgetrf_batched
809
+ self._getrs_func = cublas.zgetrs_batched
810
+ case _:
811
+ raise NotImplementedError(f"The dtype {self.dtype_name} is not supported yet.")
812
+ else:
813
+ # resolve the function pointers for the Non-batched getrf/getrs functions
814
+ self._getrf_func = cusolverDn.xgetrf
815
+ self._getrs_func = cusolverDn.xgetrs
816
+ stream_holder = utils.get_or_create_stream(self.execution_device, stream, self.stream_package)
817
+ with utils.cuda_call_ctx(stream_holder, self.blocking) as (self.last_compute_event, elapsed):
818
+ cusolverDn.set_stream(self.handle, stream_holder.ptr)
819
+ workspace_size_device, workspace_size_host = cusolverDn.xgetrf_buffer_size(
820
+ self.handle,
821
+ self.params,
822
+ self.n,
823
+ self.n,
824
+ self.cuda_dtype,
825
+ self.lhs.data_ptr,
826
+ self.lhs_layout.lda,
827
+ self.cuda_dtype,
828
+ ) # type: ignore[assignment]
829
+ self.device_workspace.set_size(workspace_size_device)
830
+ self.host_workspace.set_size(workspace_size_host)
831
+ if elapsed.data is not None:
832
+ self.logger.info(f"Planning took {elapsed.data:.3f} ms to complete.")
833
+ else:
834
+ self.logger.info("Planning has been completed.")
835
+ self.solver_planned = True
836
+
837
+ def _create_internal_operand(self, shape, dtype, stream_holder, strides=None):
838
+ return utils.create_empty_tensor(
839
+ self.internal_operand_class,
840
+ shape,
841
+ dtype,
842
+ self.execution_device,
843
+ stream_holder,
844
+ verify_strides=False,
845
+ strides=strides,
846
+ )
847
+
848
+ def _create_empty_lhs(self, stream_holder: utils.StreamHolder):
849
+ # TODO: currently lhs is created with F order, matching the generally
850
+ # expected input for cusolver/cublas. In principle we could optimize it such that:
851
+ # 1. If input is in C order, lhs also gets created in C order and
852
+ # makes use of cublas.Operation.T to perform the equivalent operation.
853
+ # 2. If input is not in C order, lhs also gets created in F order and
854
+ # makes use of cublas.Operation.N to perform the equivalent operation.
855
+ # 3. This would help speedup the d2d copy process when the input is in C order.
856
+ if not self.batched:
857
+ lhs = self._create_internal_operand(self.lhs_shape, self.dtype_name, stream_holder, (1, self.n))
858
+ elif self.lhs_batch_info.explicitly_batched:
859
+ lhs = [
860
+ self._create_internal_operand(self.lhs_shape, self.dtype_name, stream_holder, (1, self.n))
861
+ for _ in range(self.batch_count)
862
+ ]
863
+ else:
864
+ assert self.lhs_batch_info.implicitly_batched, "Internal Error."
865
+ # For execution, store lhs as shape (m, m, *batch_shape) in F order.
866
+ opt_shape = (self.n, self.n) + self.lhs_shape[:-2]
867
+ opt_strides = solver_utils.compute_f_strides(opt_shape)
868
+ # revert the the original view to get the correct strides
869
+ shape = self.lhs_shape
870
+ strides = opt_strides[2:] + opt_strides[:2]
871
+ lhs = self._create_internal_operand(shape, self.dtype_name, stream_holder, strides)
872
+ # Update the allocation stream for proper ordering in free() and release_operands()
873
+ self.lhs_mirror_stream = stream_holder.obj
874
+ self.logger.debug("Allocating lhs/a in the execution space for the linear solver...")
875
+ return lhs
876
+
877
+ def _create_empty_rhs(self, stream_holder: utils.StreamHolder):
878
+ if not self.batched:
879
+ rhs = self._create_internal_operand(
880
+ self.rhs_shape, self.dtype_name, stream_holder, solver_utils.compute_f_strides(self.rhs_shape)
881
+ )
882
+ elif self.rhs_batch_info.explicitly_batched:
883
+ strides = solver_utils.compute_f_strides(self.rhs_shape)
884
+ rhs = [
885
+ self._create_internal_operand(self.rhs_shape, self.dtype_name, stream_holder, strides)
886
+ for _ in range(self.batch_count)
887
+ ]
888
+ else:
889
+ assert self.rhs_batch_info.implicitly_batched, "Internal Error."
890
+ b_dim = len(self.rhs_shape)
891
+ batch_dim = len(self.rhs_batch_info.batch_shape)
892
+
893
+ if b_dim == batch_dim + 1:
894
+ # m, nrhs
895
+ opt_shape = (self.n,) + self.rhs_batch_info.batch_shape
896
+ opt_strides = solver_utils.compute_f_strides(opt_shape)
897
+ strides = opt_strides[1:] + opt_strides[:1]
898
+ else:
899
+ assert b_dim == batch_dim + 2, "Internal Error."
900
+ opt_shape = (self.n, self.nrhs) + self.rhs_batch_info.batch_shape
901
+ opt_strides = solver_utils.compute_f_strides(opt_shape)
902
+ strides = opt_strides[2:] + opt_strides[:2]
903
+ rhs = self._create_internal_operand(self.rhs_shape, self.dtype_name, stream_holder, strides)
904
+ # Update the allocation stream for proper ordering in free() and release_operands()
905
+ self.rhs_mirror_stream = stream_holder.obj
906
+ self.logger.debug("Allocating rhs/b in the execution space for the linear solver...")
907
+ return rhs
908
+
909
+ @utils.precondition(_check_valid_solver)
910
+ @utils.precondition(_check_planned, "Factorization")
911
+ @utils.precondition(_check_valid_operands, "Factorization")
912
+ def factorize(self, *, stream: utils.AnyStream | int | None = None):
913
+ """
914
+ Factorize the system of equations. This performs a numerical **LU** decomposition
915
+ of the left-hand side ``a`` for the current problem. Call :meth:`plan` first.
916
+
917
+ Each call factorizes the LHS ``a`` currently held by the solver. To
918
+ change the LHS values between calls, call :meth:`reset_operands` (or
919
+ :meth:`reset_operands_unchecked`) with the new ``a``. Alternatively,
920
+ when ``inplace_a=True`` and ``a`` is accessible from the execution
921
+ space, users may directly modify ``a`` in place, for example with
922
+ ``a[:] = a_new``. In that case, users are responsible for ensuring
923
+ that ``a`` contains the intended LHS values before each call, since
924
+ :meth:`factorize` overwrites that operand with its LU factors.
925
+
926
+ Args:
927
+ stream: {stream}
928
+
929
+ .. seealso::
930
+ :meth:`plan`, :meth:`solve`, :meth:`reset_operands`.
931
+ """
932
+
933
+ self.logger.info("= FACTORIZATION PHASE =")
934
+ self.logger.info(self.call_prologue)
935
+ self.solver_factorized = False
936
+
937
+ stream_holder = utils.get_or_create_stream(self.execution_device, stream, self.stream_package)
938
+
939
+ if not self.lhs_synced:
940
+ solver_utils.copy_operands(self.lhs, self.lhs_backup, stream_holder)
941
+ self.lhs_synced = True
942
+
943
+ elapsed = None
944
+ getrf = self._getrf_func
945
+ assert getrf is not None, "Internal error"
946
+
947
+ if self.backend == SolverBackend.CUBLAS:
948
+ with utils.cuda_call_ctx(stream_holder, self.blocking) as (self.last_compute_event, elapsed):
949
+ cublas.set_stream(self.handle, stream_holder.ptr)
950
+ getrf(
951
+ self.handle,
952
+ self.n,
953
+ self.lhs_pointers.data_ptr, # type: ignore[union-attr]
954
+ self.lhs_layout.lda,
955
+ self.pivot.data_ptr,
956
+ self.getrf_info_device.data_ptr,
957
+ self.batch_count,
958
+ )
959
+ else:
960
+ with (
961
+ self.device_workspace.allocate_perhaps(
962
+ stream_holder, get_last_event=lambda: self.last_compute_event
963
+ ) as device_ws,
964
+ self.host_workspace.allocate_perhaps(stream_holder, get_last_event=lambda: self.last_compute_event) as host_ws,
965
+ utils.cuda_call_ctx(stream_holder, self.blocking) as (self.last_compute_event, elapsed),
966
+ ):
967
+ cusolverDn.set_stream(self.handle, stream_holder.ptr)
968
+ getrf(
969
+ self.handle,
970
+ self.params,
971
+ self.n,
972
+ self.n,
973
+ self.cuda_dtype,
974
+ self.lhs.data_ptr,
975
+ self.lhs_layout.lda,
976
+ self.pivot.data_ptr,
977
+ self.cuda_dtype, # compute type
978
+ device_ws.raw_ptr,
979
+ device_ws.size,
980
+ host_ws.raw_ptr,
981
+ host_ws.size,
982
+ self.getrf_info_device.data_ptr,
983
+ )
984
+ # When inplace_a=True and the LHS operand lives on the execution space,
985
+ # lhs_synced always remains True. Otherwise, GETRF overwrites the internal
986
+ # execution buffer with LU factors, so it no longer matches the source LHS.
987
+ self.lhs_synced = self.inplace_a and not self.copy_across_memspace
988
+
989
+ if elapsed.data is not None:
990
+ self.logger.info(f"The factorization phase took {elapsed.data:.3f} ms to complete.")
991
+
992
+ self.getrf_info_host.copy_(self.getrf_info_device, stream_holder)
993
+ if self.backend == SolverBackend.CUSOLVER:
994
+ info = self.getrf_info_host.tensor.item()
995
+ if info != 0:
996
+ if info > 0:
997
+ raise RuntimeError(f"Factorization failed with U[{info}, {info}] being 0.")
998
+ else:
999
+ # The (-info)-th parameter to cusolverDnXgetrf is invalid.
1000
+ raise AssertionError(
1001
+ f"Internal error: cusolverDnXgetrf reported an invalid argument at position {-info} (info={info})."
1002
+ )
1003
+ else:
1004
+ info = self.getrf_info_host.tensor
1005
+ if not np.all(info == 0):
1006
+ failed_batch_indices = np.where(info > 0)[0]
1007
+ if len(failed_batch_indices) > 0:
1008
+ raise RuntimeError(f"Factorization failed for the following batch indices: {failed_batch_indices}.")
1009
+ # The (-info)-th parameter to cublasXgetrfBatched is invalid.
1010
+ info = info.item(0)
1011
+ raise AssertionError(
1012
+ f"Internal error: cublasXgetrfBatched reported an invalid argument at position {-info} (info={info})."
1013
+ )
1014
+
1015
+ if self.inplace_a and self.copy_across_memspace:
1016
+ solver_utils.copy_operands(self.lhs_backup, self.lhs, stream_holder)
1017
+ self.logger.info("Factorization has been completed.")
1018
+ self.solver_factorized = True
1019
+ return
1020
+
1021
+ @utils.precondition(_check_valid_solver)
1022
+ @utils.precondition(_check_valid_operands, "Solver Execution")
1023
+ @utils.precondition(_check_planned, "Solver Execution")
1024
+ @utils.precondition(_check_factorized, "Solver Execution")
1025
+ def solve(self, *, stream: utils.AnyStream | int | None = None):
1026
+ """
1027
+ Solve the factorized system of equations, producing the solution ``x`` for
1028
+ :math:`a @ x = b` using the factor from the most recent :meth:`factorize` and
1029
+ the current right-hand side ``b``.
1030
+
1031
+ Each call uses the RHS ``b`` currently held by the solver. To change
1032
+ the RHS values between calls, call :meth:`reset_operands` (or
1033
+ :meth:`reset_operands_unchecked`) with the new ``b``. Alternatively,
1034
+ when ``inplace_b=True`` and ``b`` is accessible from the execution
1035
+ space, users may directly modify ``b`` in place, for example with
1036
+ ``b[:] = b_new``. In that case, users are responsible for ensuring
1037
+ that ``b`` contains the intended RHS values before each call, since
1038
+ :meth:`solve` overwrites that operand with the solution.
1039
+
1040
+ Args:
1041
+ stream: {stream}
1042
+
1043
+ Returns:
1044
+ {result}
1045
+
1046
+ .. seealso::
1047
+ :meth:`plan`, :meth:`factorize`, :meth:`reset_operands`.
1048
+ """
1049
+
1050
+ self.logger.info("= SOLVER EXECUTION PHASE =")
1051
+ self.logger.info(self.call_prologue)
1052
+
1053
+ stream_holder = utils.get_or_create_stream(self.execution_device, stream, self.stream_package)
1054
+
1055
+ if self.rhs is None:
1056
+ if self.inplace_b:
1057
+ raise RuntimeError("Internal error: inplace_b=True requires an active RHS operand.")
1058
+ self.rhs = self._create_empty_rhs(stream_holder)
1059
+ if self.backend == SolverBackend.CUBLAS:
1060
+ solver_utils.update_operands_ptr_array(
1061
+ self.rhs_pointers, # type: ignore[arg-type]
1062
+ self.rhs,
1063
+ stream_holder,
1064
+ self.rhs_batch_indices,
1065
+ )
1066
+ if not self.rhs_synced:
1067
+ # The execution RHS was overwritten by a previous solve. Refresh it
1068
+ # from the source RHS before the next solve. reset_operands(..., b=...)
1069
+ # also repopulates rhs and sets rhs_synced True.
1070
+ #
1071
+ # Example: same rhs, new lhs — only the lhs must be updated:
1072
+ #
1073
+ # with DirectSolver(a0, b) as solver:
1074
+ # solver.plan()
1075
+ # solver.factorize()
1076
+ # x0 = solver.solve()
1077
+ # solver.reset_operands(a=a1)
1078
+ # solver.factorize()
1079
+ # x1 = solver.solve()
1080
+ solver_utils.copy_operands(self.rhs, self.rhs_backup, stream_holder)
1081
+ self.rhs_synced = True
1082
+
1083
+ getrs = self._getrs_func
1084
+ assert getrs is not None, "Internal error"
1085
+
1086
+ with utils.cuda_call_ctx(stream_holder, self.blocking) as (self.last_compute_event, elapsed):
1087
+ if self.backend == SolverBackend.CUSOLVER:
1088
+ # From cusolverDn, regardless of whether b is a vector or a matrix,
1089
+ # b is viewed as an F ordered matrix with shape (m, nrhs) with ldb = m
1090
+ # When b is a vector, nrhs = 1
1091
+ ldb = self.rhs_layout.ldb
1092
+ cusolverDn.set_stream(self.handle, stream_holder.ptr)
1093
+ # here getrf_info_device is reused for getrs_info
1094
+ # we can skip the check for getrs_info as it only checks for invalid args
1095
+ getrs(
1096
+ self.handle,
1097
+ self.params,
1098
+ self.getrs_operation,
1099
+ self.n,
1100
+ self.nrhs,
1101
+ self.cuda_dtype,
1102
+ self.lhs.data_ptr,
1103
+ self.lhs_layout.lda,
1104
+ self.pivot.data_ptr,
1105
+ self.cuda_dtype,
1106
+ self.rhs.data_ptr,
1107
+ ldb,
1108
+ self.getrf_info_device.data_ptr,
1109
+ )
1110
+ self.getrf_info_host.copy_(self.getrf_info_device, stream_holder)
1111
+ info = self.getrf_info_host.tensor.item()
1112
+ assert info == 0, (
1113
+ f"Internal error: cusolverDnXgetrs reported an invalid argument at position {-info} (info={info})."
1114
+ )
1115
+ else:
1116
+ assert self.backend == SolverBackend.CUBLAS, "Internal Error."
1117
+ ldb = self.rhs_layout.ldb
1118
+ info = np.empty(1, dtype=np.int32)
1119
+ cublas.set_stream(self.handle, stream_holder.ptr)
1120
+ getrs(
1121
+ self.handle,
1122
+ self.getrs_operation,
1123
+ self.n,
1124
+ self.nrhs,
1125
+ self.lhs_pointers.data_ptr, # type: ignore[union-attr]
1126
+ self.lhs_layout.lda,
1127
+ self.pivot.data_ptr,
1128
+ self.rhs_pointers.data_ptr, # type: ignore[union-attr]
1129
+ ldb,
1130
+ info.ctypes.data,
1131
+ self.batch_count,
1132
+ )
1133
+ assert np.all(info == 0), f"Internal error: cublasXgetrsBatched reported an invalid argument (info={info})."
1134
+
1135
+ if elapsed.data is not None:
1136
+ self.logger.info(f"The solver execution took {elapsed.data:.3f} ms to complete.")
1137
+
1138
+ self.logger.info("Solver execution has been completed.")
1139
+
1140
+ # GETRS overwrites rhs with the solution. For device-resident inplace_b,
1141
+ # rhs is also the source allocation, so execution/source still match and
1142
+ # users are responsible for restoring intended RHS values. Otherwise,
1143
+ # the execution RHS no longer matches source until refreshed.
1144
+ self.rhs_synced = self.inplace_b and not self.copy_across_memspace
1145
+
1146
+ if self.inplace_b:
1147
+ if self.copy_across_memspace:
1148
+ solver_utils.copy_operands(self.rhs_backup, self.rhs, stream_holder)
1149
+ rhs_return = self.rhs_backup
1150
+ else:
1151
+ rhs_return = self.rhs
1152
+ if isinstance(rhs_return, Sequence):
1153
+ return [o.tensor for o in rhs_return]
1154
+ return rhs_return.tensor
1155
+ elif self.copy_across_memspace:
1156
+ self.logger.debug("Copying the result to the input device id...")
1157
+ # return the result on the input device id
1158
+ if isinstance(self.rhs, Sequence):
1159
+ return [o.to(self.input_device_id, stream_holder=stream_holder).tensor for o in self.rhs]
1160
+ else:
1161
+ return self.rhs.to(self.input_device_id, stream_holder=stream_holder).tensor
1162
+ else:
1163
+ # return the ownership of the result to the caller
1164
+ if isinstance(self.rhs, Sequence):
1165
+ out = [o.tensor for o in self.rhs]
1166
+ else:
1167
+ out = self.rhs.tensor
1168
+ self.rhs = None
1169
+ return out
1170
+
1171
+ def free(self):
1172
+ """Free :class:`DirectSolver` resources.
1173
+
1174
+ It is recommended that the object be used as a context manager; if that is
1175
+ not possible, call this method explicitly when the object is no longer needed.
1176
+ """
1177
+ if not self.valid_state:
1178
+ return
1179
+
1180
+ try:
1181
+ self.device_workspace.release(self.last_compute_event)
1182
+ self.host_workspace.release(self.last_compute_event)
1183
+ if self.last_compute_event is not None:
1184
+ streams = {
1185
+ self.lhs_mirror_stream,
1186
+ self.rhs_mirror_stream,
1187
+ self.resource_alloc_stream,
1188
+ }
1189
+
1190
+ for stream in streams:
1191
+ if stream is not None:
1192
+ stream.wait(self.last_compute_event)
1193
+
1194
+ self.lhs_mirror_stream = None
1195
+ self.rhs_mirror_stream = None
1196
+ self.resource_alloc_stream = None
1197
+ self.last_compute_event = None
1198
+
1199
+ # Free cusolverDn params if it exists.
1200
+ if self.params is not None:
1201
+ cusolverDn.destroy_params(self.params)
1202
+ self.params = None
1203
+
1204
+ # Free handle if we own it.
1205
+ if self.handle is not None and self.own_handle:
1206
+ cusolverDn.destroy(self.handle)
1207
+ self.handle, self.own_handle = None, False
1208
+
1209
+ # Set all attributes to None except for logger and valid_state
1210
+ _keep = {"logger", "valid_state"}
1211
+ for attr in list(vars(self)):
1212
+ if attr not in _keep:
1213
+ setattr(self, attr, None)
1214
+
1215
+ except Exception as e:
1216
+ self.logger.critical("Internal error: only part of the DirectSolver object's resources have been released.")
1217
+ self.logger.critical(str(e))
1218
+ raise e
1219
+ finally:
1220
+ self.valid_state = False
1221
+
1222
+ self.logger.info("The DirectSolver object's resources have been released.")
1223
+
1224
+ def __enter__(self):
1225
+ """
1226
+ Return ``self`` for a ``with`` block; the matching :meth:`__exit__` calls
1227
+ :meth:`free` so resources are always released.
1228
+ """
1229
+ return self
1230
+
1231
+ def __exit__(self, exc_type, exc_value, traceback):
1232
+ """
1233
+ Call :meth:`free` (context manager ``__exit__`` protocol).
1234
+ """
1235
+ self.free()
1236
+
1237
+
1238
+ @utils.docstring_decorator(GENERIC_DIRECT_SOLVER_DOCUMENTATION, skip_missing=False)
1239
+ def direct_solver(
1240
+ a: AnyTensor,
1241
+ b: AnyTensor,
1242
+ *,
1243
+ options: DirectSolverOptions | dict[str, Any] | None = None,
1244
+ execution: ExecutionCUDA | str | dict[str, Any] | None = None,
1245
+ stream: utils.AnyStream | int | None = None,
1246
+ ):
1247
+ r"""
1248
+ Solve :math:`a @ x = b` for :math:`x`. This function-form API is a wrapper around the
1249
+ stateful :class:`DirectSolver` object APIs and is meant for *single* use (for example,
1250
+ when the user needs to perform just one dense direct linear solve), in which case
1251
+ there is no possibility of amortizing preparatory costs (such as :meth:`DirectSolver.plan`
1252
+ and :meth:`DirectSolver.factorize` before :meth:`DirectSolver.solve`).
1253
+
1254
+ Use :class:`DirectSolver` when reusing a factorization or performing many solves for the
1255
+ same problem size and layout, or when using :class:`DirectSolverOptions` in a long-lived
1256
+ way (for example a logger or a custom handle).
1257
+
1258
+ Args:
1259
+ a: {a}
1260
+ b: {b}
1261
+ options: {options}
1262
+ execution: {execution}
1263
+ stream: {stream}
1264
+
1265
+ Returns:
1266
+ {result}
1267
+
1268
+ Semantics:
1269
+ {semantics}
1270
+
1271
+ {direct_solver_note}
1272
+
1273
+ Examples:
1274
+
1275
+ A single :func:`~nvmath.linalg.direct_solver` call (equivalent to :meth:`DirectSolver.plan` followed
1276
+ by :meth:`DirectSolver.factorize` and :meth:`DirectSolver.solve` on a stateful
1277
+ object) with NumPy CPU operands, which the implementation executes on the
1278
+ default CUDA device:
1279
+
1280
+ >>> import numpy as np
1281
+ >>> import nvmath.linalg as la
1282
+ >>>
1283
+ >>> n = 4
1284
+ >>> rng = np.random.default_rng(0)
1285
+ >>> a = rng.standard_normal((n, n), dtype=np.float64) + n * np.eye(n, dtype=np.float64)
1286
+ >>> b = np.ones(n, dtype=np.float64)
1287
+ >>>
1288
+ >>> x = la.direct_solver(a, b)
1289
+ >>> x.shape
1290
+ (4,)
1291
+
1292
+ For ``options``, ``stream``, and batching, see the **Semantics** section. More
1293
+ examples (batching, :meth:`~DirectSolver.reset_operands`, streams) are in
1294
+ `generic direct solver examples
1295
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/linalg/generic/direct_solver>`_
1296
+ on GitHub.
1297
+ """ # noqa: W505
1298
+ with DirectSolver(a, b, options=options, execution=execution, stream=stream) as solver:
1299
+ solver.plan(stream=stream)
1300
+ solver.factorize(stream=stream)
1301
+ return solver.solve(stream=stream)