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,1368 @@
1
+ # Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from __future__ import annotations
6
+
7
+ __all__ = ["DirectSolver", "InvalidDirectSolverState", "direct_solver"]
8
+
9
+ import logging
10
+ from collections.abc import Sequence
11
+ from typing import Any
12
+
13
+ from nvmath._internal.workspace import Workspace
14
+ from nvmath.bindings import (
15
+ cublas, # type: ignore[attr-defined]
16
+ cusolverMp, # type: ignore[attr-defined]
17
+ )
18
+ from nvmath.distributed._internal import tensor_wrapper
19
+ from nvmath.distributed.distribution import Distribution
20
+ from nvmath.internal import utils
21
+ from nvmath.internal.tensor_ifc import AnyTensor, TensorHolder
22
+ from nvmath.linalg._internal.solver_utils import compute_f_strides
23
+
24
+ from ._configuration import DirectSolverOptions
25
+ from ._factorization import _FactorizationState
26
+ from ._initialization import _resolve_runtime_context, _ResolvedProblemSpec, _RuntimeContext, initialize_direct_solver
27
+
28
+ # cuSOLVERMp workflow -> code map
29
+ # (https://docs.nvidia.com/cuda/cusolvermp/getting_started/index.html#workflow)
30
+ #
31
+ # 1. NCCL initialization
32
+ # _resolve_runtime_context() (_initialization.py) -- verifies the NCCL
33
+ # comm (the comm itself is created externally by
34
+ # nvmath.distributed.initialize); called from __init__.
35
+ # 2. cusolverMpCreate (library handle)
36
+ # initialize_direct_solver() (_initialization.py) -- via get_handle()
37
+ # (cached) or a user-supplied options.handle.
38
+ # 3. cusolverMpCreateDeviceGrid
39
+ # _create_device_grids() (_initialization.py).
40
+ # 4. cusolverMpCreateMatrixDesc
41
+ # _create_descriptors() (_initialization.py).
42
+ # 5. query host/device buffer sizes
43
+ # method: plan() -- getrf_buffer_size / getrs_buffer_size.
44
+ # 6. allocate host/device workspace
45
+ # method: factorize() / solve() -- via Workspace.allocate_perhaps.
46
+ # The Workspace objects are created during construction via
47
+ # _create_factorization_state_and_workspaces() (_initialization.py)
48
+ # and sized in plan().
49
+ # 7. execute the routine
50
+ # method: factorize() (cusolverMpGetrf) / solve() (cusolverMpGetrs).
51
+ # 8. synchronize the local stream
52
+ # method: factorize() (sync_and_check_factorize_info) / solve()
53
+ # (stream sync before returning the result).
54
+ # 9. deallocate host/device workspace
55
+ # method: factorize() / solve() (Workspace context-manager exit) and
56
+ # free() -> _release_internal_resources().
57
+ # teardown: cusolverMp{DestroyMatrixDesc, DestroyGrid, Destroy}
58
+ # method: free() -> _release_internal_resources() (the handle
59
+ # is owned by the handle cache, so the instance never destroys it).
60
+
61
+
62
+ # Shared doc fragments for the distributed dense direct linear solver public API.
63
+ GENERIC_DIRECT_SOLVER_DOCUMENTATION: dict = utils.COMMON_SHARED_DOC_MAP.copy()
64
+ GENERIC_DIRECT_SOLVER_DOCUMENTATION.update(
65
+ {
66
+ "a": """\
67
+ A distributed tensor representing the left-hand side operand :math:`a` of the linear system
68
+ :math:`a @ x = b` (see `Semantics`_). The currently
69
+ supported types are :class:`numpy.ndarray`, :class:`cupy.ndarray`, and
70
+ :class:`torch.Tensor`.""".replace("\n", " "),
71
+ #
72
+ "b": """\
73
+ A distributed tensor representing the right-hand side :math:`b` of the linear system
74
+ :math:`a @ x = b` (see `Semantics`_). The currently
75
+ supported types are :class:`numpy.ndarray`, :class:`cupy.ndarray`, and
76
+ :class:`torch.Tensor`.""".replace("\n", " "),
77
+ #
78
+ "distributions": """\
79
+ A sequence ``[distribution_for_a, distribution_for_b]`` specifying how :math:`a` and
80
+ :math:`b` are partitioned across processes. See `Semantics`_ and `Requirements`_ for the
81
+ supported distribution types and the constraints they must satisfy (in particular, the
82
+ row block size of :math:`b` must match that of :math:`a`).""".replace("\n", " "),
83
+ #
84
+ "options": """\
85
+ Specify options for the direct solver as a :class:`DirectSolverOptions` object.
86
+ Alternatively, a `dict` containing the parameters for the ``DirectSolverOptions``
87
+ constructor can also be provided. If not specified, the value will be set to the
88
+ default-constructed ``DirectSolverOptions`` object.""".replace("\n", " "),
89
+ #
90
+ "result": """\
91
+ A distributed tensor representing the solution :math:`x` of the linear system
92
+ :math:`a @ x = b`
93
+ (see `Semantics`_). It has the same distribution,
94
+ local shape, device, and package as the right-hand side ``b``. When the
95
+ :attr:`~nvmath.distributed.linalg.DirectSolverOptions.inplace_b` attribute of
96
+ :class:`DirectSolverOptions` is ``True`` (the default), :math:`x` is written into
97
+ ``b`` in place and the returned object is ``b`` itself.""".replace("\n", " "),
98
+ #
99
+ "semantics": """\
100
+ .. _Semantics:
101
+
102
+ The distributed direct solver solves the dense linear system :math:`a @ x = b` for
103
+ :math:`x`, where :math:`a` is a square ``(n, n)`` matrix and :math:`b` has shape
104
+ ``(n, nrhs)`` or ``(n,)``, where ``nrhs`` is the number of RHS vectors.
105
+ It performs an LU factorization with partial
106
+ pivoting (:meth:`DirectSolver.factorize`) followed by triangular solves
107
+ (:meth:`DirectSolver.solve`).
108
+ The computation always runs on the GPU; CPU operands are copied to the device
109
+ and back automatically.
110
+
111
+ On each process, ``a`` and ``b`` hold only the portions of the global
112
+ matrices :math:`a` and :math:`b` that are assigned to that process. The
113
+ corresponding entries of ``distributions`` describe how :math:`a` and
114
+ :math:`b` are partitioned across processes.
115
+
116
+ The solution :math:`x` has the same local shape, memory space, and package
117
+ as ``b`` and follows ``b``'s distribution. By default,
118
+ :meth:`DirectSolver.factorize` overwrites ``a`` with its LU factors and
119
+ :meth:`DirectSolver.solve` overwrites ``b`` with :math:`x` and returns
120
+ ``b``; this in-place behavior is controlled by the
121
+ :attr:`~nvmath.distributed.linalg.DirectSolverOptions.inplace_a` and
122
+ :attr:`~nvmath.distributed.linalg.DirectSolverOptions.inplace_b` attributes of
123
+ :class:`DirectSolverOptions` (both ``True`` by default). If :math:`a` is singular,
124
+ :meth:`DirectSolver.factorize` raises.
125
+ """.strip(),
126
+ #
127
+ "requirements": """\
128
+ .. _Requirements:
129
+
130
+ **Operands.**
131
+
132
+ - The global number of columns of ``a`` must match the global number of rows of
133
+ ``b`` (both equal :math:`n`, the order of the square system).
134
+ - ``a`` and ``b`` must belong to the same package and have the same dtype, one of
135
+ ``float32``, ``float64``, ``complex64``, or ``complex128``.
136
+ - ``a`` and ``b`` must reside in the same memory space on each process, and
137
+ GPU operands must be on the device bound to the distributed runtime on
138
+ that process.
139
+ - The local arrays must be column-major; a padded leading dimension
140
+ (a column-major slice of a larger buffer) is allowed.
141
+
142
+ **Distributions.**
143
+
144
+ ``distributions`` must contain exactly two entries,
145
+ ``[distribution_for_a, distribution_for_b]``, respectively describing how ``a``
146
+ and ``b`` are partitioned across processes.
147
+
148
+ The simplest, but not most performant, choice is to give
149
+ both ``a`` and ``b`` the same row-wise
150
+ :class:`~nvmath.distributed.distribution.Slab` distribution (``Slab.X``),
151
+ so that each process owns an equal, contiguous block of rows of the
152
+ global operands. The only requirement here is that the global number of
153
+ rows be divisible by the number of processes (a ``Slab`` with unequal
154
+ partition sizes is not supported, because it cannot be expressed as a
155
+ block-cyclic distribution); ``a`` and ``b`` are then partitioned
156
+ consistently and all the other requirements below are satisfied
157
+ automatically, so none of them need to be considered.
158
+
159
+ In general, each distribution entry may be a
160
+ :class:`~nvmath.distributed.distribution.Slab` (with uniform partition sizes),
161
+ :class:`~nvmath.distributed.distribution.BlockNonCyclic`, or
162
+ :class:`~nvmath.distributed.distribution.BlockCyclic` distribution,
163
+ and ``a`` and ``b`` may use different distributions.
164
+ This flexibility comes with stricter requirements:
165
+
166
+ - The process grid must place its first process at ``(0, 0)`` and
167
+ use a ``COL_MAJOR`` or ``ROW_MAJOR`` layout.
168
+ - The row block size of ``b`` must equal that of ``a``, while column block sizes
169
+ are independent.
170
+ - A 1-D ``b`` requires ``b``'s distribution to be 1-D (row-only): a 2-D distribution
171
+ is rejected, because the single column cannot be split across process columns.
172
+ - For a 2-D ``b``, ``b``'s distribution may be either 1-D (row-only) or
173
+ 2-D, partitioning both rows and columns.
174
+ """.strip(),
175
+ #
176
+ # Re-generate these two with ``version_added=None``: the ``COMMON_SHARED_DOC_MAP``
177
+ # defaults stamp ".. versionadded:: 0.9.0", which does not apply to the (newer)
178
+ # distributed direct solver.
179
+ "release_operands": utils._release_operand_docstring(True, execution_methods=("factorize", "solve")),
180
+ "reset_operands_unchecked": utils._reset_operand_unchecked_docstring(True),
181
+ }
182
+ )
183
+
184
+
185
+ class InvalidDirectSolverState(Exception):
186
+ """Raised when a public method is called after :meth:`DirectSolver.free`."""
187
+
188
+
189
+ @utils.docstring_decorator(GENERIC_DIRECT_SOLVER_DOCUMENTATION, skip_missing=False)
190
+ class DirectSolver:
191
+ """
192
+ Create a stateful object that encapsulates the specified distributed direct solver
193
+ computation for the dense linear system :math:`a @ x = b` and the required resources to
194
+ perform it.
195
+
196
+ A stateful object can be used to amortize the cost of preparation (planning and
197
+ factorization) across multiple solves with different right-hand sides, or with updated
198
+ operands of the same problem specification (see :meth:`reset_operands`).
199
+ The function-form API :func:`direct_solver` is a convenient alternative for single
200
+ use (the user needs to perform just one solve, for example), in which case there
201
+ is no possibility of amortizing preparatory costs.
202
+
203
+ Using the stateful object typically involves the following steps:
204
+
205
+ 1. **Problem Specification**: Initialize the object with the operands, their
206
+ distributions, and options.
207
+ 2. **Preparation**: Use :meth:`plan` to query and size the solver workspaces for this
208
+ specific problem.
209
+ 3. **Execution**: :meth:`factorize` the matrix and :meth:`solve` the system.
210
+ 4. **Resource Management**: Ensure all resources are released either by explicitly
211
+ calling :meth:`free` or by managing the stateful object within a context manager.
212
+
213
+ Detailed information on what is happening in the various phases described above can be
214
+ obtained by passing in a :class:`logging.Logger` object to :class:`DirectSolverOptions`
215
+ or by setting the appropriate options in the root logger object, which is used by
216
+ default:
217
+
218
+ >>> import logging
219
+ >>> logging.basicConfig(
220
+ ... level=logging.INFO,
221
+ ... format="%(asctime)s %(levelname)-8s %(message)s",
222
+ ... datefmt="%m-%d %H:%M:%S",
223
+ ... )
224
+
225
+ Args:
226
+ a: {a}
227
+
228
+ b: {b}
229
+
230
+ distributions: {distributions}
231
+
232
+ options: {options}
233
+
234
+ stream: {stream}
235
+
236
+ Semantics:
237
+ {semantics}
238
+
239
+ Requirements:
240
+ {requirements}
241
+
242
+ .. seealso::
243
+ :meth:`plan`, :meth:`factorize`, :meth:`solve`, :meth:`reset_operands`,
244
+ :meth:`reset_operands_unchecked`, :meth:`release_operands`, :meth:`free`,
245
+ :class:`DirectSolverOptions`, :func:`direct_solver`.
246
+
247
+ Examples:
248
+
249
+ >>> import numpy as np
250
+ >>> import nvmath.distributed
251
+ >>> from nvmath.distributed.distribution import Slab
252
+ >>> from nvmath.distributed.linalg import DirectSolver
253
+
254
+ Get the process group used to initialize nvmath.distributed (for information on
255
+ initializing ``nvmath.distributed``, you can refer to the documentation or to the
256
+ direct solver examples in `nvmath/examples/distributed/linalg/generic/direct_solver
257
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/linalg/
258
+ generic/direct_solver>`_):
259
+
260
+ >>> process_group = nvmath.distributed.get_context().process_group
261
+
262
+ Get my process rank and the total number of processes:
263
+
264
+ >>> rank = process_group.rank
265
+ >>> nranks = process_group.nranks
266
+
267
+ We will solve the dense square system :math:`a @ x = b`, where :math:`a` has shape
268
+ ``(n, n)`` and :math:`b` has shape ``(n, nrhs)``. Both operands use a
269
+ :class:`~nvmath.distributed.distribution.Slab` distribution, so each process owns a
270
+ contiguous block of rows of the global matrices.
271
+
272
+ .. note::
273
+ The :class:`~nvmath.distributed.distribution.Slab` distribution is used here
274
+ for convenience of exposition and is not necessarily the most performant
275
+ choice. For :math:`a`, a
276
+ :class:`~nvmath.distributed.distribution.BlockCyclic` distribution is
277
+ generally preferred, as it improves load balancing across processes.
278
+
279
+ Create the local row slabs on the CPU (cuSOLVERMp requires column-major):
280
+
281
+ >>> n, nrhs = 256, 8
282
+ >>> local_n = n // nranks # assume n is divisible by the process count
283
+ >>> rng = np.random.default_rng(rank)
284
+ >>> a = rng.random((local_n, n)).astype(np.float64, order="F")
285
+ >>> b = rng.random((local_n, nrhs)).astype(np.float64, order="F")
286
+
287
+ Make the global :math:`a` diagonally dominant so the system is well-conditioned.
288
+ The diagonal entry ``a[i, i]`` of this rank's row band is at local position
289
+ ``[k, rank * local_n + k]``:
290
+
291
+ >>> idx = np.arange(local_n)
292
+ >>> a[idx, rank * local_n + idx] += n
293
+
294
+ Create a DirectSolver object encapsulating the problem specification above:
295
+
296
+ >>> distributions = [Slab.X, Slab.X]
297
+ >>> solver = DirectSolver(a, b, distributions=distributions)
298
+
299
+ Options can be provided above to control the behavior of the operation using the
300
+ `options` argument (see :class:`DirectSolverOptions`).
301
+
302
+ Next, plan the operation:
303
+
304
+ >>> solver.plan()
305
+
306
+ Factorize the matrix, then solve the system.
307
+ By default, the solution :math:`x` overwrites ``b`` and is returned:
308
+
309
+ >>> solver.factorize()
310
+ >>> x = solver.solve()
311
+
312
+ Finally, free the object's resources. To avoid having to explicitly make this call,
313
+ it's recommended to use the DirectSolver object as a context manager as shown below,
314
+ if possible.
315
+
316
+ >>> solver.free()
317
+
318
+ A benefit of the stateful API is that an expensive factorization can
319
+ be reused across multiple right-hand sides: factorize once, then solve
320
+ repeatedly, resetting only ``b`` between solves.
321
+
322
+ Further examples can be found in the
323
+ `nvmath/examples/distributed/linalg/generic/direct_solver
324
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/linalg/
325
+ generic/direct_solver>`_
326
+ directory.
327
+ """
328
+
329
+ __slots__ = (
330
+ # Immutable, cross-rank-consistent derived spec
331
+ # see _ResolvedProblemSpec in _initialization.py
332
+ "_spec",
333
+ # Runtime / distributed context.
334
+ "_runtime",
335
+ # Lifecycle flags.
336
+ "_valid_state",
337
+ "_solver_planned",
338
+ "_solver_factorized",
339
+ "_operands_released",
340
+ # cuSOLVERMp resources (flat here for now, but can
341
+ # be refactored into a separate object in the future).
342
+ "_cusolvermp_handle",
343
+ "_cusolvermp_version",
344
+ "_cusolvermp_grid_a",
345
+ "_cusolvermp_grid_b",
346
+ "_desc_a",
347
+ "_desc_b",
348
+ "_factorization_state",
349
+ "_device_workspace",
350
+ "_host_workspace",
351
+ "_last_compute_event",
352
+ "_local_ld_a",
353
+ "_local_ld_b",
354
+ # Operands' state (flat here for now, but can
355
+ # be refactored into a separate object in the future).
356
+ "_lhs_compute",
357
+ "_rhs_compute",
358
+ "_lhs_user",
359
+ "_rhs_user",
360
+ "_lhs_synced",
361
+ "_rhs_synced",
362
+ # Misc.
363
+ "_options",
364
+ "_logger",
365
+ )
366
+
367
+ # Type annotations for attributes assigned by the construction free
368
+ # functions in ``_initialization.py``. mypy can't infer their types from a
369
+ # call it can't see into, so we declare them here. (Attributes assigned
370
+ # in-class by ``_init_defaults`` / ``__init__`` are inferred and omitted.)
371
+ # The derived problem facts now live behind ``_spec``; only the runtime,
372
+ # resource, and operand-state attributes set by free functions remain.
373
+ _spec: _ResolvedProblemSpec
374
+ _runtime: _RuntimeContext
375
+ # Active operand buffers handed to getrf/getrs as ``data_ptr``, bound in
376
+ # ``_resolve_active_operand_buffers`` from the wrapped user operands held in
377
+ # ``_lhs_user`` / ``_rhs_user``: a device mirror when a mirror is needed, or
378
+ # the user's GPU tensor itself on the cuda + inplace path. They don't exist
379
+ # before that step. So ``_lhs_compute`` always refers to a real compute-space
380
+ # buffer -- never a transient CPU operand.
381
+ _lhs_compute: TensorHolder
382
+ _rhs_compute: TensorHolder
383
+ _local_ld_a: int
384
+ _local_ld_b: int
385
+
386
+ def _init_defaults(self) -> None:
387
+ """
388
+ Pre-initialize every slot that ``_release_internal_resources`` guards
389
+ with ``if X is not None``.
390
+
391
+ ``__init__``'s rollback path calls ``self._release_internal_resources``
392
+ on any partial-construction failure, which then walks the resource
393
+ slots below. The ``None`` defaults here are what makes that walk safe:
394
+ without them the rollback itself would raise ``AttributeError`` due
395
+ to ``__slots__``.
396
+
397
+ Lifecycle flags also live here so the rollback path can read them
398
+ (``_valid_state`` is set to ``True`` only at the end of ``__init__``,
399
+ which is what makes ``free()`` idempotent across double-frees while
400
+ still letting the rollback bypass the guard via the private helper).
401
+
402
+ Note: this is placed *before* __init__ because mypy sees it first;
403
+ subsequent assignments in ``__init__`` / workflow methods narrow each
404
+ union to a concrete value.
405
+ """
406
+ # Lifecycle flags. _valid_state stays False until __init__ completes.
407
+ self._valid_state: bool = False
408
+ self._solver_planned: bool = False
409
+ self._solver_factorized: bool = False
410
+ self._operands_released: bool = False
411
+
412
+ self._cusolvermp_handle: int | None = None
413
+ # cuSOLVERMp library version set during __init__
414
+ self._cusolvermp_version: int | None = None
415
+ self._cusolvermp_grid_a: int | None = None
416
+ self._cusolvermp_grid_b: int | None = None
417
+ self._desc_a: int | None = None
418
+ self._desc_b: int | None = None
419
+
420
+ # Persistent device state (pivot vector + per-call info flags).
421
+ self._factorization_state: _FactorizationState | None = None
422
+
423
+ self._device_workspace: Workspace | None = None
424
+ self._host_workspace: Workspace | None = None
425
+
426
+ self._last_compute_event = None
427
+
428
+ # User-side wrappers. ``_collective_problem_spec`` binds the wrapped user
429
+ # operands here during construction; ``_resolve_active_operand_buffers``
430
+ # then either keeps them (mirror case, where ``_lhs_compute`` /
431
+ # ``_rhs_compute`` point at a mirror and the user wrapper is held here for
432
+ # copy-back / ``release_operands``) or, for an operand that needs no
433
+ # mirror (cuda + inplace), moves the wrapper into ``_lhs_compute`` /
434
+ # ``_rhs_compute`` and resets the slot to None. So in steady state these
435
+ # are non-None iff ``_spec.need_lhs_mirror`` / ``_spec.need_rhs_mirror``.
436
+ # The None default also keeps the partial-init rollback safe.
437
+ self._lhs_user: TensorHolder | None = None
438
+ self._rhs_user: TensorHolder | None = None
439
+
440
+ # Whether the rhs compute mirror currently holds a faithful copy of the
441
+ # user's b (i.e. no refresh needed before getrs). True after the initial
442
+ # staging copy and after reset_operands(b=...); set False once getrs
443
+ # overwrites the mirror with x. Mirrors the non-distributed solver's
444
+ # ``rhs_synced`` and lets solve() skip the H2D/D2D copy when the mirror
445
+ # is already in sync (e.g. the first solve after construction/reset).
446
+ self._rhs_synced: bool = True
447
+
448
+ # Whether the lhs compute mirror currently holds a faithful copy of the
449
+ # user's A (i.e. no refresh needed before getrf). True after the initial
450
+ # staging copy and after reset_operands(a=...); set False once getrf
451
+ # overwrites the mirror with the LU factors. Symmetric with
452
+ # ``_rhs_synced``: lets factorize() skip the H2D/D2D copy when the mirror
453
+ # is already in sync, and makes a repeated factorize() re-read the
454
+ # pristine A rather than factorizing the previous LU.
455
+ self._lhs_synced: bool = True
456
+
457
+ def __init__(
458
+ self,
459
+ a: AnyTensor,
460
+ b: AnyTensor,
461
+ /,
462
+ *,
463
+ distributions: Sequence[Distribution],
464
+ options: DirectSolverOptions | dict[str, Any] | None = None,
465
+ stream: utils.AnyStream | int | None = None,
466
+ ) -> None:
467
+ # __init__ covers cuSOLVERMp workflow steps 1-4 (runtime/handle/grids/
468
+ # descriptors) and creates the workspaces without allocating their
469
+ # memory. The step implementations live in _initialization.py; see the
470
+ # "cuSOLVERMp workflow -> code map" comment at module scope for how
471
+ # every step maps onto this class and that module.
472
+
473
+ # Default-initialize every slot so the rollback cleanup can work safely.
474
+ self._init_defaults()
475
+
476
+ self._options: DirectSolverOptions = utils.check_or_create_options( # type: ignore[assignment]
477
+ DirectSolverOptions, options, "Distributed direct solver options"
478
+ )
479
+ self._logger = self._options.logger if self._options.logger is not None else logging.getLogger()
480
+ self._logger.info("= SPECIFICATION PHASE =")
481
+
482
+ # ``options.blocking`` is accepted, but it is currently ignored:
483
+ # factorize()/solve() always block.
484
+ # This is the correct behavior anyway when blocking is True; when blocking
485
+ # is "auto" (the default), always blocking is still a valid choice and
486
+ # leaves room for forward compatibility once we implement non-blocking
487
+ # execution for GPU operands.
488
+ # TODO: use options.blocking and enable asynchronous execution.
489
+
490
+ # Resolve the runtime context (verify the NCCL communicator and capture
491
+ # rank/device). Kept OUTSIDE the try below because the rollback reads
492
+ # _runtime.device_id.
493
+ self._runtime = _resolve_runtime_context()
494
+
495
+ try:
496
+ # Run the cuSOLVERMp construction workflow.
497
+ initialize_direct_solver(self, a, b, distributions=distributions, stream=stream)
498
+ except BaseException:
499
+ # Construction failed partway through; release whatever was already
500
+ # built. We call _release_internal_resources() directly rather than
501
+ # free() because _valid_state is still False, so free() would
502
+ # short-circuit to a no-op. _release_internal_resources() tolerates
503
+ # partial init: each resource is guarded by an is-not-None check.
504
+ try:
505
+ self._release_internal_resources()
506
+ except Exception:
507
+ self._logger.exception("DirectSolver: error during partial-init rollback")
508
+ raise
509
+
510
+ self._valid_state = True
511
+ self._logger.info("The DirectSolver instance has been created.")
512
+
513
+ def _create_compute_mirror(self, src: TensorHolder, stream_holder) -> TensorHolder:
514
+ # Allocate a fresh execution-space mirror for ``src`` and copy its
515
+ # data in. Two implementations behind a single signature:
516
+
517
+ # * Cross-space (cpu operand, cuda execution): ``TensorHolder.to``
518
+ # handles the allocation and the H2D copy in one call.
519
+ # * Same-space (cuda operand, cuda execution): we need a fresh
520
+ # same-device buffer with the same shape/dtype. Cannot reuse
521
+ # ``.to(self._runtime.device_id)`` here because
522
+ # ``CudaTensor.to(same_device)`` is a no-op that returns ``self``
523
+ # (see ``nvmath/internal/tensor_ifc_numpy.py``) -- it would
524
+ # alias the user's tensor instead of cloning it. Allocate via
525
+ # ``TensorHolder.empty`` and then ``copy_`` D2D.
526
+
527
+ if self._spec.operands_memory_space == self._spec.execution_space:
528
+ # Lay the mirror out densely in column-major order (LLD == local
529
+ # rows) rather than mirroring ``src.strides``. The source may be a
530
+ # slice of a larger buffer with a *padded* leading dimension
531
+ # (strides[1] > shape[0]); such strides are not dense, and
532
+ # ``TensorHolder.empty`` sizes its allocation for a dense layout
533
+ # only -- handing it padded strides yields an undersized buffer
534
+ # that cuSOLVERMp then overruns. cuSOLVERMp accepts any LLD >= the
535
+ # local row count, so a tight mirror is both valid and sufficient;
536
+ # ``copy_`` is a strided copy, so it reads the padded source
537
+ # correctly regardless.
538
+ dst = src.__class__.empty(
539
+ shape=src.shape,
540
+ device_id=self._runtime.device_id,
541
+ dtype=src.dtype,
542
+ strides=compute_f_strides(tuple(src.shape)),
543
+ stream_holder=stream_holder,
544
+ )
545
+ dst.copy_(src, stream_holder=stream_holder)
546
+ return dst
547
+ return src.to(self._runtime.device_id, stream_holder)
548
+
549
+ def _check_valid_solver(self, *args, **kwargs):
550
+ if not self._valid_state:
551
+ raise InvalidDirectSolverState("The DirectSolver object cannot be used after resources are freed")
552
+
553
+ def _check_planned(self, *args, **kwargs):
554
+ what = kwargs["what"]
555
+ if not self._solver_planned:
556
+ raise RuntimeError(f"{what} cannot be performed before plan() has been called.")
557
+
558
+ def _check_factorized(self, *args, **kwargs):
559
+ what = kwargs["what"]
560
+ if not self._solver_factorized:
561
+ raise RuntimeError(f"{what} cannot be performed before factorize() has been called.")
562
+
563
+ def _check_valid_operands(self, *args, **kwargs):
564
+ what = kwargs["what"]
565
+ if self._operands_released:
566
+ raise RuntimeError(
567
+ f"{what} cannot be performed after the operands have been released. "
568
+ f"Use reset_operands() to provide new operands before performing the {what.lower()}."
569
+ )
570
+
571
+ @utils.precondition(_check_valid_solver)
572
+ @utils.precondition(_check_valid_operands, "Planning")
573
+ def plan(self, *, stream: utils.AnyStream | int | None = None) -> None:
574
+ """
575
+ Plan the distributed direct solver. This queries cuSOLVERMp for the host and device
576
+ workspace sizes needed by the factorization and the solve, and sizes the solver's
577
+ internal workspaces accordingly. Planning is idempotent, so calling it again after a
578
+ successful plan is a no-op.
579
+
580
+ Args:
581
+ stream: {stream}
582
+
583
+ .. seealso::
584
+ :meth:`factorize`, :meth:`solve`.
585
+ """
586
+ # plan() is idempotent: re-entry after a successful plan is a
587
+ # logged no-op so callers can stack plan() inside a setup helper
588
+ # without tracking whether it has run before.
589
+ if self._solver_planned:
590
+ self._logger.info("Skipping planning since it has already been performed in a previous call.")
591
+ return
592
+
593
+ self._logger.info("= PLANNING PHASE =")
594
+
595
+ stream_holder = utils.get_or_create_stream(self._runtime.device_id, stream, self._spec.stream_package)
596
+ self._logger.info(f"The specified stream for DirectSolver.plan is {stream_holder.obj}.")
597
+
598
+ # Device pointers to this rank's local tile of A, b, and the pivot
599
+ a_data_ptr = self._lhs_compute.data_ptr
600
+ b_data_ptr = self._rhs_compute.data_ptr
601
+ ipiv_ptr = self._factorization_state.ipiv_device_ptr # type: ignore[union-attr]
602
+
603
+ with utils.device_ctx(self._runtime.device_id):
604
+ # *_buffer_size queries launch no kernels, so the stream is unused
605
+ # here; set for consistency with factorize()/solve().
606
+ cusolverMp.set_stream(self._cusolvermp_handle, stream_holder.ptr)
607
+
608
+ # https://docs.nvidia.com/cuda/cusolvermp/usage/functions.html#cusolvermpgetrf-buffersize
609
+ getrf_device_bytes, getrf_host_bytes = cusolverMp.getrf_buffer_size(
610
+ self._cusolvermp_handle,
611
+ self._spec.global_m,
612
+ self._spec.global_n,
613
+ a_data_ptr,
614
+ self._spec.ia,
615
+ self._spec.ja,
616
+ self._desc_a,
617
+ ipiv_ptr,
618
+ self._spec.solver_cuda_dtype,
619
+ )
620
+
621
+ # trans=N: cuSOLVERMp's getrs only supports non-transposed solves
622
+ # against the LU factorization produced by getrf (in-place on A).
623
+ # https://docs.nvidia.com/cuda/cusolvermp/usage/functions.html#cusolvermpgetrs-buffersize
624
+ getrs_device_bytes, getrs_host_bytes = cusolverMp.getrs_buffer_size(
625
+ self._cusolvermp_handle,
626
+ cublas.Operation.N,
627
+ self._spec.global_m,
628
+ self._spec.nrhs,
629
+ a_data_ptr,
630
+ self._spec.ia,
631
+ self._spec.ja,
632
+ self._desc_a,
633
+ ipiv_ptr,
634
+ b_data_ptr,
635
+ self._spec.ib,
636
+ self._spec.jb,
637
+ self._desc_b,
638
+ self._spec.solver_cuda_dtype,
639
+ )
640
+
641
+ device_bytes = max(getrf_device_bytes, getrs_device_bytes)
642
+ host_bytes = max(getrf_host_bytes, getrs_host_bytes)
643
+ assert self._device_workspace is not None and self._host_workspace is not None, (
644
+ "Workspace objects must be created before plan()."
645
+ )
646
+ self._device_workspace.set_size(device_bytes)
647
+ self._host_workspace.set_size(host_bytes)
648
+
649
+ self._logger.info(
650
+ "Planned cuSOLVERMp workspaces: device=%d B, host=%d B "
651
+ "(getrf needed device=%d/host=%d, getrs needed device=%d/host=%d).",
652
+ device_bytes,
653
+ host_bytes,
654
+ getrf_device_bytes,
655
+ getrf_host_bytes,
656
+ getrs_device_bytes,
657
+ getrs_host_bytes,
658
+ )
659
+ self._solver_planned = True
660
+
661
+ @utils.precondition(_check_valid_solver)
662
+ @utils.precondition(_check_valid_operands, "Factorization")
663
+ @utils.precondition(_check_planned, "Factorization")
664
+ def factorize(self, *, stream: utils.AnyStream | int | None = None) -> None:
665
+ """
666
+ Factorize the matrix :math:`a` (LU factorization with partial pivoting).
667
+
668
+ Each call factorizes the left-hand side ``a`` currently held by the solver.
669
+ To change the left-hand side values between calls, call :meth:`reset_operands`
670
+ (or :meth:`reset_operands_unchecked`) with the new ``a``. Alternatively, when
671
+ ``inplace_a=True`` and ``a`` is accessible from the execution space, users
672
+ may directly modify ``a`` in place, for example with ``a[:] = a_new``.
673
+ In that case, users are responsible for ensuring that ``a`` contains the intended
674
+ left-hand side values before each call, since :meth:`factorize` overwrites
675
+ that operand with its LU factors.
676
+
677
+ Args:
678
+ stream: {stream}
679
+
680
+ .. seealso::
681
+ :meth:`plan`, :meth:`solve`, :meth:`reset_operands`.
682
+ """
683
+
684
+ self._logger.info("= FACTORIZATION PHASE =")
685
+
686
+ stream_holder = utils.get_or_create_stream(self._runtime.device_id, stream, self._spec.stream_package)
687
+ self._logger.info(f"The specified stream for DirectSolver.factorize is {stream_holder.obj}.")
688
+
689
+ # Refresh the LHS compute mirror from the user's current A when it is no
690
+ # longer in sync: getrf is destructive on the mirror, so once a prior
691
+ # factorize has overwritten it with the LU we must re-read A before
692
+ # factorizing again (this is also what makes an in-place edit to a mirrored
693
+ # A visible to the next factorize() without an intervening reset_operands,
694
+ # and keeps factorize(); factorize() re-factorizing the original A rather
695
+ # than the previous LU). ``_lhs_synced`` lets us skip the copy when the
696
+ # mirror is already current (e.g. the first factorize after
697
+ # construction/reset). When there is no lhs mirror (gpu + inplace_a=True)
698
+ # the user's A IS the live buffer and needs no refresh; need_lhs_mirror is
699
+ # False there (and _lhs_user is None), so this block is skipped.
700
+ if self._spec.need_lhs_mirror and not self._lhs_synced:
701
+ assert self._lhs_user is not None, "lhs mirror expected but no user handle is held"
702
+ if self._lhs_compute.tensor is None:
703
+ self._lhs_compute = self._create_compute_mirror(self._lhs_user, stream_holder)
704
+ else:
705
+ self._lhs_compute.copy_(self._lhs_user, stream_holder=stream_holder)
706
+ self._lhs_synced = True
707
+
708
+ # narrow for mypy
709
+ factorization_state = self._factorization_state
710
+ device_workspace, host_workspace = self._device_workspace, self._host_workspace
711
+ assert factorization_state is not None
712
+ assert device_workspace is not None and host_workspace is not None
713
+
714
+ # ``options.blocking`` is intentionally ignored here: getrf is
715
+ # launched non-blocking and captures _last_compute_event for
716
+ # stream ordering, then sync_and_check_factorize_info() below does
717
+ # an unconditional blocking sync. factorize() therefore always
718
+ # blocks regardless of options.blocking (see __init__ for context).
719
+ # The non-blocking launch + event capture is kept for forward
720
+ # compatibility: once we support non-blocking execution, honoring
721
+ # options.blocking here becomes almost a matter of skipping that final sync.
722
+ with (
723
+ device_workspace.allocate_perhaps(
724
+ stream_holder,
725
+ get_last_event=lambda: self._last_compute_event,
726
+ ) as device_wsp,
727
+ host_workspace.allocate_perhaps(
728
+ stream_holder,
729
+ get_last_event=lambda: self._last_compute_event,
730
+ ) as host_wsp,
731
+ utils.cuda_call_ctx(stream_holder, blocking=False, timing=False) as (
732
+ self._last_compute_event,
733
+ _,
734
+ ),
735
+ ):
736
+ cusolverMp.set_stream(self._cusolvermp_handle, stream_holder.ptr)
737
+
738
+ cusolverMp.getrf(
739
+ self._cusolvermp_handle,
740
+ self._spec.global_m,
741
+ self._spec.global_n,
742
+ self._lhs_compute.data_ptr,
743
+ self._spec.ia,
744
+ self._spec.ja,
745
+ self._desc_a,
746
+ factorization_state.ipiv_device_ptr,
747
+ self._spec.solver_cuda_dtype,
748
+ device_wsp.raw_ptr,
749
+ device_wsp.size,
750
+ host_wsp.raw_ptr,
751
+ host_wsp.size,
752
+ factorization_state.info_getrf_device_ptr,
753
+ )
754
+
755
+ # getrf overwrote the compute buffer with the LU factors. Under a mirror
756
+ # the buffer no longer matches the user's A, so mark it stale: the next
757
+ # factorize() will refresh from _lhs_user first (matching solve()'s rhs
758
+ # handling at the getrs site). With no mirror (gpu + inplace_a=True) the
759
+ # user's A IS the buffer and freshness is user-managed, so it stays synced.
760
+ self._lhs_synced = not self._spec.need_lhs_mirror
761
+
762
+ # Unconditional, blocking sync + cross-rank info check; raises on
763
+ # singular / illegal-argument.
764
+ factorization_state.sync_and_check_factorize_info(
765
+ stream_holder,
766
+ nccl_comm=self._runtime.distributed_ctx.nccl_comm,
767
+ )
768
+
769
+ # inplace_a on a CPU operand: getrf ran on the GPU mirror, so copy the LU
770
+ # back into the user's buffer. Done after the info check above so a
771
+ # failed (e.g. singular) factorization leaves the user's A intact rather
772
+ # than overwriting it with a partial LU.
773
+ if self._spec.need_lhs_mirror and self._options.inplace_a:
774
+ assert self._lhs_user is not None
775
+ self._lhs_user.copy_(self._lhs_compute, stream_holder=stream_holder)
776
+
777
+ self._solver_factorized = True
778
+ self._logger.info("Factorization has been completed on rank %d.", self._runtime.rank)
779
+
780
+ @utils.precondition(_check_valid_solver)
781
+ @utils.precondition(_check_valid_operands, "Solver Execution")
782
+ @utils.precondition(_check_planned, "Solver Execution")
783
+ @utils.precondition(_check_factorized, "Solver Execution")
784
+ def solve(self, *, stream: utils.AnyStream | int | None = None) -> Any:
785
+ """
786
+ Solve the factorized system :math:`a @ x = b` for :math:`x` using the factors
787
+ from the most recent :meth:`factorize` and the current right-hand side ``b``.
788
+
789
+ Each call uses the right-hand side ``b`` currently held by the solver.
790
+ To change the right-hand side values between calls, call :meth:`reset_operands`
791
+ (or :meth:`reset_operands_unchecked`) with the new ``b``. Alternatively, when
792
+ ``inplace_b=True`` and ``b`` is accessible from the execution space, users
793
+ may directly modify ``b`` in place, for example with ``b[:] = b_new``.
794
+ In that case, users are responsible for ensuring that ``b`` contains the
795
+ intended right-hand side values before each call, since :meth:`solve`
796
+ overwrites that operand with the solution.
797
+
798
+ Args:
799
+ stream: {stream}
800
+
801
+ Returns:
802
+ {result}
803
+
804
+ .. seealso::
805
+ :meth:`plan`, :meth:`factorize`, :meth:`reset_operands`.
806
+ """
807
+ self._logger.info("= SOLVER EXECUTION PHASE =")
808
+
809
+ stream_holder = utils.get_or_create_stream(self._runtime.device_id, stream, self._spec.stream_package)
810
+ self._logger.info(f"The specified stream for DirectSolver.solve is {stream_holder.obj}.")
811
+
812
+ # Refresh the RHS compute mirror from the user's current b when it is no
813
+ # longer in sync: getrs is destructive on the mirror, so once a prior
814
+ # solve has dirtied it we must
815
+ # re-read b before solving again (this is also what makes an in-place
816
+ # edit to b visible to the next solve without an intervening
817
+ # reset_operands). ``_rhs_synced`` lets us skip the copy when the mirror
818
+ # is already current (e.g. the first solve after construction/reset).
819
+ # Two sub-cases when a refresh is due:
820
+ # * tensor is None: the previous solve was cuda + inplace_b=False and
821
+ # handed the mirror buffer (holding x) to the caller, so allocate a
822
+ # fresh mirror from b. (release_operands() also nulls the slot, but
823
+ # _check_valid_operands has already rejected that path before here.)
824
+ # * tensor is live: copy b into the existing mirror (H2D for a cpu
825
+ # operand, D2D for a gpu operand).
826
+ # When there is no rhs mirror (gpu + inplace_b=True) the user's b IS the
827
+ # live buffer and needs no refresh; need_rhs_mirror is False there (and
828
+ # _rhs_user is None), so this block is skipped.
829
+ if self._spec.need_rhs_mirror and not self._rhs_synced:
830
+ assert self._rhs_user is not None, "rhs mirror expected but no user handle is held"
831
+ if self._rhs_compute.tensor is None:
832
+ self._rhs_compute = self._create_compute_mirror(self._rhs_user, stream_holder)
833
+ else:
834
+ self._rhs_compute.copy_(self._rhs_user, stream_holder=stream_holder)
835
+ self._rhs_synced = True
836
+
837
+ # narrow for mypy
838
+ factorization_state = self._factorization_state
839
+ device_workspace, host_workspace = self._device_workspace, self._host_workspace
840
+ assert factorization_state is not None
841
+ assert device_workspace is not None and host_workspace is not None
842
+
843
+ # ``blocking=False`` is intentional: solve() still always blocks now,
844
+ # but the sync happens in the return-path dispatch below (see that
845
+ # comment for the per-branch sync behavior). Blocking here too would
846
+ # just force a redundant sync.
847
+ with (
848
+ device_workspace.allocate_perhaps(
849
+ stream_holder,
850
+ get_last_event=lambda: self._last_compute_event,
851
+ ) as device_wsp,
852
+ host_workspace.allocate_perhaps(
853
+ stream_holder,
854
+ get_last_event=lambda: self._last_compute_event,
855
+ ) as host_wsp,
856
+ utils.cuda_call_ctx(stream_holder, blocking=False, timing=False) as (
857
+ self._last_compute_event,
858
+ _,
859
+ ),
860
+ ):
861
+ cusolverMp.set_stream(self._cusolvermp_handle, stream_holder.ptr)
862
+
863
+ # cuSOLVERMp 0.8.0 does not internally zero
864
+ # ``d_info`` at getrs entry: a successful call leaves
865
+ # the buffer holding whatever it had on entry, while
866
+ # only invalid-argument paths actually write a
867
+ # negative info. Zero it ourselves so the post-call
868
+ # value is deterministic (any future reader of getrs
869
+ # info sees 0 on success). Newer versions are correct.
870
+ if self._cusolvermp_version == 800:
871
+ factorization_state.reset_info_getrs_device(stream_holder)
872
+
873
+ cusolverMp.getrs(
874
+ self._cusolvermp_handle,
875
+ # trans=N: getrs only supports non-transposed
876
+ cublas.Operation.N,
877
+ self._spec.global_n,
878
+ self._spec.nrhs,
879
+ self._lhs_compute.data_ptr,
880
+ self._spec.ia,
881
+ self._spec.ja,
882
+ self._desc_a,
883
+ factorization_state.ipiv_device_ptr,
884
+ self._rhs_compute.data_ptr,
885
+ self._spec.ib,
886
+ self._spec.jb,
887
+ self._desc_b,
888
+ self._spec.solver_cuda_dtype,
889
+ device_wsp.raw_ptr,
890
+ device_wsp.size,
891
+ host_wsp.raw_ptr,
892
+ host_wsp.size,
893
+ factorization_state.info_getrs_device_ptr,
894
+ )
895
+
896
+ # getrs info is intentionally not checked: cuSOLVERMp's getrs only writes
897
+ # info on an invalid argument (info < 0), which would indicate a bug
898
+ # in the parameters we pass -- not a user error -- and would almost
899
+ # certainly have been caught by getrf, whose arguments are mostly the same.
900
+ # There is no info > 0 (singular) case for a triangular solve.
901
+
902
+ # getrs just overwrote the rhs mirror with x (or, in the no-mirror
903
+ # gpu + inplace_b=True case, the user's b in place). Mark the mirror
904
+ # dirty so the next solve re-reads b first. The no-mirror case never
905
+ # refreshes (guarded on need_rhs_mirror), so leaving it "synced" there
906
+ # is harmless.
907
+ self._rhs_synced = not self._spec.need_rhs_mirror
908
+
909
+ # Return-path dispatch, outer branch on operand memory
910
+ # space, inner branch on ``inplace_b``. The cpu arms always
911
+ # stream-sync (D->H via ``ndbuffer.copy_into``); the cuda
912
+ # arms leave the stream pending and sync at the end of their
913
+ # branch (solve() always blocks).
914
+ if self._spec.operands_memory_space == "cpu":
915
+ # cpu: getrs wrote x into the solver's GPU mirror, so a
916
+ # D->H copy is unavoidable on the way out. Both
917
+ # sub-branches go through ``ndbuffer.copy_into``'s D->H
918
+ # path, which stream-syncs internally -- the result is
919
+ # host-visible by the time the call returns.
920
+ assert self._rhs_user is not None
921
+ if self._options.inplace_b:
922
+ # Copy x back into the user's CPU b.
923
+ self._rhs_user.copy_(self._rhs_compute, stream_holder=stream_holder)
924
+ out = self._rhs_user.tensor
925
+ else:
926
+ # D2H into a fresh CPU tensor; keep the mirror as
927
+ # solver-owned scratch for the next solve.
928
+ out = self._rhs_compute.to("cpu", stream_holder).tensor
929
+ else:
930
+ # cuda: x already lives on the GPU; no D->H involved, so both
931
+ # sub-branches leave stream work pending and we sync explicitly
932
+ # (solve() always blocks). The cpu branch above needs no such
933
+ # sync -- its D->H copy already blocks the host.
934
+ if self._options.inplace_b:
935
+ # No mirror was allocated -- ``_rhs_compute`` is
936
+ # the user's b, getrs wrote x into it in place.
937
+ # Hand it back as-is.
938
+ out = self._rhs_compute.tensor
939
+ else:
940
+ # Hand the mirror buffer (now holding x) directly to the
941
+ # caller, then null the slot so the solver no longer references it.
942
+ # A subsequent solve()/reset_operands reallocates a fresh mirror.
943
+ out = self._rhs_compute.tensor
944
+ self._rhs_compute.tensor = None
945
+ stream_holder.obj.sync()
946
+
947
+ self._logger.info("Solver execution has been completed on rank %d.", self._runtime.rank)
948
+
949
+ return out
950
+
951
+ @utils.precondition(_check_valid_solver)
952
+ def release_operands(self) -> None:
953
+ """
954
+ {release_operands}
955
+ """
956
+ if self._operands_released:
957
+ self._logger.info("Operands have already been released; nothing to do.")
958
+ return
959
+
960
+ # Drop the active-buffer references. Under no-mirror operands
961
+ # ``_lhs_compute`` / ``_rhs_compute`` alias the user's GPU
962
+ # tensor; under any "has mirror" case they point at the internal
963
+ # mirror. The slots themselves are guaranteed non-None here:
964
+ # ``_resolve_active_operand_buffers`` sets them in ``__init__``, and
965
+ # ``_check_valid_solver`` gates entry to this method on
966
+ # ``_valid_state == True`` (set only at end-of-``__init__``).
967
+ self._lhs_compute.tensor = None
968
+ self._rhs_compute.tensor = None
969
+
970
+ # Per-operand user-handle cleanup. ``_lhs_user`` / ``_rhs_user``
971
+ # hold the user's tensor wrapper for the mirror case; null its
972
+ # ``.tensor``. Each handle is non-None whenever the matching
973
+ # ``_need_*_mirror`` is True (set when the mirror is allocated and
974
+ # refreshed on reset_operands), so the asserts below always hold.
975
+ if self._spec.need_lhs_mirror:
976
+ assert self._lhs_user is not None
977
+ self._lhs_user.tensor = None
978
+ if self._spec.need_rhs_mirror:
979
+ assert self._rhs_user is not None
980
+ self._rhs_user.tensor = None
981
+
982
+ self._operands_released = True
983
+ self._logger.info("User-provided operands have been released.")
984
+
985
+ def _reset_operands_impl(
986
+ self,
987
+ *,
988
+ a_wrapped: TensorHolder | None,
989
+ b_wrapped: TensorHolder | None,
990
+ stream: utils.AnyStream | int | None,
991
+ ) -> None:
992
+ if not self._spec.need_lhs_mirror and not self._spec.need_rhs_mirror:
993
+ # no mirror on either operand (cuda operands with
994
+ # ``inplace_a=True`` and ``inplace_b=True``)
995
+ if a_wrapped is not None:
996
+ self._lhs_compute.tensor = a_wrapped.tensor
997
+ if b_wrapped is not None:
998
+ self._rhs_compute.tensor = b_wrapped.tensor
999
+
1000
+ elif self._spec.operands_memory_space == self._spec.execution_space:
1001
+ # At least one side has a mirror, but both live on the
1002
+ # execution device -- no H2D is needed
1003
+ self._reset_operands_same_space(a_wrapped=a_wrapped, b_wrapped=b_wrapped, stream=stream)
1004
+
1005
+ else:
1006
+ # Cross-space (e.g. cpu operands, cuda execution): both
1007
+ # sides always have mirrors, the stream is always required
1008
+ # for the H2D copy.
1009
+ self._reset_operands_cross_space(a_wrapped=a_wrapped, b_wrapped=b_wrapped, stream=stream)
1010
+
1011
+ if a_wrapped is not None:
1012
+ # New A means the cached LU is stale; force factorize() to
1013
+ # run before the next solve().
1014
+ self._solver_factorized = False
1015
+
1016
+ # The mirror (or the live buffer, no-mirror case) now holds the
1017
+ # freshly pushed A, so the next factorize can skip the refresh copy.
1018
+ self._lhs_synced = True
1019
+
1020
+ if b_wrapped is not None:
1021
+ # The mirror (or the live buffer, no-mirror case) now holds the
1022
+ # freshly pushed b, so the next solve can skip the refresh copy.
1023
+ self._rhs_synced = True
1024
+
1025
+ self._operands_released = False
1026
+
1027
+ def _reset_operands_same_space(
1028
+ self,
1029
+ *,
1030
+ a_wrapped: TensorHolder | None,
1031
+ b_wrapped: TensorHolder | None,
1032
+ stream: utils.AnyStream | int | None,
1033
+ ) -> None:
1034
+ # In this case, a side either has a same-device mirror (D2D copy needed)
1035
+ # or is pure-aliased. The stream is therefore
1036
+ # acquired lazily on the first mirror side we encounter.
1037
+ stream_holder = None
1038
+ for src, need_mirror, slot, user_slot_name in (
1039
+ (a_wrapped, self._spec.need_lhs_mirror, self._lhs_compute, "_lhs_user"),
1040
+ (b_wrapped, self._spec.need_rhs_mirror, self._rhs_compute, "_rhs_user"),
1041
+ ):
1042
+ if src is None:
1043
+ continue
1044
+ if not need_mirror:
1045
+ # cuda + inplace=True: alias the user's GPU tensor.
1046
+ slot.tensor = src.tensor
1047
+ continue
1048
+ if stream_holder is None:
1049
+ stream_holder = utils.get_or_create_stream(self._runtime.device_id, stream, self._spec.stream_package)
1050
+ if self._operands_released or slot.tensor is None:
1051
+ # No live mirror to copy into: either release_operands()
1052
+ # freed it, or a previous solve handed it to the user
1053
+ # (cuda + inplace_b=False). Allocate a fresh same-device
1054
+ # buffer and copy in.
1055
+ slot.tensor = self._create_compute_mirror(src, stream_holder).tensor
1056
+ else:
1057
+ slot.copy_(src, stream_holder=stream_holder)
1058
+ setattr(self, user_slot_name, src)
1059
+
1060
+ def _reset_operands_cross_space(
1061
+ self,
1062
+ *,
1063
+ a_wrapped: TensorHolder | None,
1064
+ b_wrapped: TensorHolder | None,
1065
+ stream: utils.AnyStream | int | None,
1066
+ ) -> None:
1067
+ # Both sides always have a mirror (the user's bytes have to be
1068
+ # staged across address spaces), and the stream is unconditionally
1069
+ # needed for the H2D copy.
1070
+
1071
+ stream_holder = utils.get_or_create_stream(self._runtime.device_id, stream, self._spec.stream_package)
1072
+ needs_fresh_mirror = self._operands_released
1073
+ if a_wrapped is not None:
1074
+ if needs_fresh_mirror:
1075
+ # release_operands() freed the previous mirror; allocate
1076
+ # a fresh execution-space buffer and H2D into it.
1077
+ self._lhs_compute = self._create_compute_mirror(a_wrapped, stream_holder)
1078
+ else:
1079
+ self._lhs_compute.copy_(a_wrapped, stream_holder=stream_holder)
1080
+ self._lhs_user = a_wrapped
1081
+ if b_wrapped is not None:
1082
+ if needs_fresh_mirror:
1083
+ self._rhs_compute = self._create_compute_mirror(b_wrapped, stream_holder)
1084
+ else:
1085
+ self._rhs_compute.copy_(b_wrapped, stream_holder=stream_holder)
1086
+ self._rhs_user = b_wrapped
1087
+
1088
+ def reset_operands_unchecked(
1089
+ self,
1090
+ *,
1091
+ a: Any = None,
1092
+ b: Any = None,
1093
+ stream: utils.AnyStream | int | None = None,
1094
+ ) -> None:
1095
+ """
1096
+ {reset_operands_unchecked}
1097
+ """
1098
+ self._reset_operands_impl(
1099
+ a_wrapped=tensor_wrapper.wrap_operand(a) if a is not None else None,
1100
+ b_wrapped=tensor_wrapper.wrap_operand(b) if b is not None else None,
1101
+ stream=stream,
1102
+ )
1103
+
1104
+ @utils.precondition(_check_valid_solver)
1105
+ def reset_operands(
1106
+ self,
1107
+ *,
1108
+ a: Any = None,
1109
+ b: Any = None,
1110
+ stream: utils.AnyStream | int | None = None,
1111
+ ) -> None:
1112
+ """
1113
+ Reset one or both operands held by this :class:`DirectSolver` instance.
1114
+
1115
+ Args:
1116
+ a: New left-hand side, or ``None`` to keep the previous left-hand side.
1117
+
1118
+ b: New right-hand side, or ``None`` to keep the previous right-hand side.
1119
+
1120
+ stream: {stream}
1121
+
1122
+ Semantics:
1123
+ - At least one operand is required (all of them after
1124
+ :meth:`release_operands`), otherwise a :class:`ValueError` is raised.
1125
+
1126
+ - This method will perform various checks on the new operands to make sure:
1127
+
1128
+ - The local shapes, strides, and datatypes match those of the old ones.
1129
+ - The packages that the operands belong to match those of the old ones.
1130
+ - The device must match that of the old ones.
1131
+
1132
+ - The distribution of each operand is fixed at construction and cannot be
1133
+ changed by :meth:`reset_operands`; the new local shapes and strides need
1134
+ to be consistent with that distribution.
1135
+
1136
+ - Resetting ``a`` invalidates the current factorization, so :meth:`factorize`
1137
+ must be called again before the next :meth:`solve`.
1138
+
1139
+ .. seealso::
1140
+ :meth:`reset_operands_unchecked`, :meth:`release_operands`.
1141
+ """
1142
+ # if the operands have been released, both 'a' and 'b' must be provided
1143
+ if self._operands_released:
1144
+ if a is None or b is None:
1145
+ raise ValueError("After release_operands(), both 'a' and 'b' must be provided to reset_operands().")
1146
+ elif a is None and b is None:
1147
+ # All arguments are None: there is nothing to update, so reject the call.
1148
+ raise ValueError("reset_operands() requires at least one operand to be provided.")
1149
+
1150
+ a_wrapped = tensor_wrapper.wrap_operand(a) if a is not None else None
1151
+ b_wrapped = tensor_wrapper.wrap_operand(b) if b is not None else None
1152
+
1153
+ if a_wrapped is not None:
1154
+ self._spec.lhs_reset_invariants.check(a_wrapped)
1155
+ if b_wrapped is not None:
1156
+ self._spec.rhs_reset_invariants.check(b_wrapped)
1157
+
1158
+ self._reset_operands_impl(a_wrapped=a_wrapped, b_wrapped=b_wrapped, stream=stream)
1159
+
1160
+ if a_wrapped is not None:
1161
+ self._logger.info("User-provided lhs/a has been reset on rank %d.", self._runtime.rank)
1162
+ if b_wrapped is not None:
1163
+ self._logger.info("User-provided rhs/b has been reset on rank %d.", self._runtime.rank)
1164
+
1165
+ def _release_internal_resources(self) -> None:
1166
+ """
1167
+ (private) Resource cleanup. Safe on partial init and on
1168
+ repeat calls; the user-facing :meth:`free` wraps this with the
1169
+ ``_valid_state`` short-circuit.
1170
+
1171
+ NCCL communicator teardown is intentionally NOT done
1172
+ here because the comm is owned by ``nvmath.distributed`` and
1173
+ outlives individual solver instances.
1174
+ """
1175
+ if self._device_workspace is not None:
1176
+ self._device_workspace.release(self._last_compute_event)
1177
+ self._device_workspace = None
1178
+ if self._host_workspace is not None:
1179
+ self._host_workspace.release(self._last_compute_event)
1180
+ self._host_workspace = None
1181
+
1182
+ # Release the persistent factorization buffers, ordering their
1183
+ # stream-ordered free after the last compute event (mirrors the
1184
+ # workspace release above) so teardown stays safe even if execution
1185
+ # ever goes non-blocking. Done before nulling _last_compute_event.
1186
+ if self._factorization_state is not None:
1187
+ self._factorization_state.free(self._last_compute_event)
1188
+ self._factorization_state = None
1189
+
1190
+ self._last_compute_event = None
1191
+
1192
+ # Matrix descriptors then grids then handles. ``_runtime`` is set before
1193
+ # the construction try, so it's present on any rollback; getattr keeps
1194
+ # this defensive if cleanup ever runs before it was assigned.
1195
+ runtime = getattr(self, "_runtime", None)
1196
+ if runtime is not None:
1197
+ with utils.device_ctx(runtime.device_id):
1198
+ if self._desc_b is not None:
1199
+ cusolverMp.destroy_matrix_desc(self._desc_b)
1200
+ self._desc_b = None
1201
+ if self._desc_a is not None:
1202
+ cusolverMp.destroy_matrix_desc(self._desc_a)
1203
+ self._desc_a = None
1204
+ if self._cusolvermp_grid_b is not None:
1205
+ cusolverMp.destroy_grid(self._cusolvermp_grid_b)
1206
+ self._cusolvermp_grid_b = None
1207
+ if self._cusolvermp_grid_a is not None:
1208
+ cusolverMp.destroy_grid(self._cusolvermp_grid_a)
1209
+ self._cusolvermp_grid_a = None
1210
+ # Handle is either user-supplied (user-owned) or cached
1211
+ # (owned by ``_caching`` and cleared in ``finalize()``).
1212
+ # The instance never destroys it.
1213
+ self._cusolvermp_handle = None
1214
+
1215
+ # Drop refs to every remaining slot. Skip ``_logger`` so the
1216
+ # "released" log line in free() still works; skip the lifecycle
1217
+ # flags (_valid_state and the _check_* gates: _solver_planned,
1218
+ # _solver_factorized, _operands_released) so post-free
1219
+ # preconditions still see the right values.
1220
+ _keep = {
1221
+ "_logger",
1222
+ "_valid_state",
1223
+ "_solver_planned",
1224
+ "_solver_factorized",
1225
+ "_operands_released",
1226
+ }
1227
+ for attr in self.__slots__:
1228
+ if attr not in _keep:
1229
+ setattr(self, attr, None)
1230
+
1231
+ def free(self) -> None:
1232
+ """
1233
+ Free the :class:`DirectSolver` resources.
1234
+
1235
+ It is recommended that the :class:`DirectSolver` object be used within a context
1236
+ manager, but if that is not possible this method must be called explicitly to ensure
1237
+ the solver's resources are properly cleaned up.
1238
+ """
1239
+ # Idempotent
1240
+ if not self._valid_state:
1241
+ return
1242
+
1243
+ try:
1244
+ self._release_internal_resources()
1245
+ except Exception:
1246
+ self._logger.critical(
1247
+ "Internal error: only part of the DirectSolver object's resources have been released.",
1248
+ )
1249
+ raise
1250
+ finally:
1251
+ self._valid_state = False
1252
+
1253
+ self._logger.info("The DirectSolver object's resources have been released.")
1254
+
1255
+ def __enter__(self) -> DirectSolver:
1256
+ """Enter the context manager, returning this :class:`DirectSolver` instance."""
1257
+ return self
1258
+
1259
+ def __exit__(self, exc_type, exc_value, traceback) -> None:
1260
+ """Exit the context manager, releasing the solver's resources via :meth:`free`."""
1261
+ self.free()
1262
+
1263
+
1264
+ @utils.docstring_decorator(GENERIC_DIRECT_SOLVER_DOCUMENTATION, skip_missing=False)
1265
+ def direct_solver(
1266
+ a: AnyTensor,
1267
+ b: AnyTensor,
1268
+ /,
1269
+ *,
1270
+ distributions: Sequence[Distribution],
1271
+ options: DirectSolverOptions | dict[str, Any] | None = None,
1272
+ stream: utils.AnyStream | int | None = None,
1273
+ ) -> Any:
1274
+ """
1275
+ Solve the distributed dense linear system :math:`a @ x = b` for :math:`x`. This
1276
+ function-form API is a convenience wrapper around the stateful :class:`DirectSolver`
1277
+ object and is meant for *single* use (the user needs to perform just one solve, for
1278
+ example).
1279
+
1280
+ Args:
1281
+ a: {a}
1282
+
1283
+ b: {b}
1284
+
1285
+ distributions: {distributions}
1286
+
1287
+ options: {options}
1288
+
1289
+ stream: {stream}
1290
+
1291
+ Returns:
1292
+ {result}
1293
+
1294
+ Semantics:
1295
+ {semantics}
1296
+
1297
+ Requirements:
1298
+ {requirements}
1299
+
1300
+ .. seealso::
1301
+ :class:`DirectSolver`, :class:`DirectSolverOptions`.
1302
+
1303
+ Examples:
1304
+
1305
+ >>> import numpy as np
1306
+ >>> import nvmath.distributed
1307
+ >>> from nvmath.distributed.distribution import Slab
1308
+ >>> from nvmath.distributed.linalg import direct_solver
1309
+
1310
+ Get the process group used to initialize nvmath.distributed (for information on
1311
+ initializing ``nvmath.distributed``, you can refer to the documentation or to the
1312
+ direct solver examples in `nvmath/examples/distributed/linalg/generic/direct_solver
1313
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/linalg/
1314
+ generic/direct_solver>`_):
1315
+
1316
+ >>> process_group = nvmath.distributed.get_context().process_group
1317
+
1318
+ Get my process rank and the total number of processes:
1319
+
1320
+ >>> rank = process_group.rank
1321
+ >>> nranks = process_group.nranks
1322
+
1323
+ We will solve the dense square system :math:`a @ x = b`, where :math:`a` has shape
1324
+ ``(n, n)`` and :math:`b` has shape ``(n, nrhs)``. Both operands use a
1325
+ :class:`~nvmath.distributed.distribution.Slab` distribution, so each process owns a
1326
+ contiguous block of rows of the global matrices (chosen here for convenience of
1327
+ exposition; a :class:`~nvmath.distributed.distribution.BlockCyclic` distribution is
1328
+ generally preferred for :math:`a`, as it improves load balancing across processes).
1329
+
1330
+ Create the local row slabs on the CPU (cuSOLVERMp requires column-major), and make
1331
+ the global :math:`a` diagonally dominant so the system is well-conditioned. The
1332
+ diagonal entry ``a[i, i]`` of this rank's row band is at local position
1333
+ ``[k, rank * local_n + k]``:
1334
+
1335
+ >>> n, nrhs = 256, 8
1336
+ >>> local_n = n // nranks # assume n is divisible by the process count
1337
+ >>> rng = np.random.default_rng(rank)
1338
+ >>> a = rng.random((local_n, n)).astype(np.float64, order="F")
1339
+ >>> b = rng.random((local_n, nrhs)).astype(np.float64, order="F")
1340
+ >>> idx = np.arange(local_n)
1341
+ >>> a[idx, rank * local_n + idx] += n
1342
+
1343
+ Solve the system with a single call to :func:`direct_solver`. By default the
1344
+ solution :math:`x` overwrites ``b`` in place and is returned, so ``x`` is a NumPy
1345
+ ndarray with the same distribution and local shape as ``b``:
1346
+
1347
+ >>> distributions = [Slab.X, Slab.X]
1348
+ >>> x = direct_solver(a, b, distributions=distributions)
1349
+ >>> assert x is b
1350
+
1351
+ Options can be provided to control the behavior of the operation using the
1352
+ ``options`` argument (see :class:`DirectSolverOptions`).
1353
+
1354
+ Notes:
1355
+ - This function is a convenience wrapper around :class:`DirectSolver` and is
1356
+ specifically meant for *single* use. To amortize the cost of the factorization
1357
+ across multiple right-hand sides, use the stateful :class:`DirectSolver` API.
1358
+
1359
+ Further examples can be found in the
1360
+ `nvmath/examples/distributed/linalg/generic/direct_solver
1361
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/linalg/
1362
+ generic/direct_solver>`_
1363
+ directory.
1364
+ """
1365
+ with DirectSolver(a, b, distributions=distributions, options=options, stream=stream) as solver:
1366
+ solver.plan(stream=stream)
1367
+ solver.factorize(stream=stream)
1368
+ return solver.solve(stream=stream)