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,1284 @@
1
+ # Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ __all__ = ["Redistribute", "redistribute"]
6
+
7
+ import logging
8
+ import math
9
+ from collections.abc import Sequence
10
+ from dataclasses import dataclass
11
+ from typing import Any, Final, Literal, cast
12
+
13
+ import nvmath.distributed
14
+ from nvmath.bindings import (
15
+ cufftMp, # type: ignore
16
+ nvshmem, # type: ignore
17
+ )
18
+ from nvmath.distributed._internal import tensor_wrapper
19
+ from nvmath.distributed._internal.nvshmem import NvshmemMemoryManager
20
+ from nvmath.distributed._internal.tensor_ifc import DistributedTensor
21
+ from nvmath.distributed.distribution import Box, ConvertDistributionError, Distribution
22
+ from nvmath.internal import formatters, utils
23
+ from nvmath.internal.package_wrapper import AnyStream, StreamHolder
24
+
25
+ from ._configuration import RedistributeOptions
26
+
27
+
28
+ @dataclass
29
+ class TensorLayout:
30
+ """An internal data class for capturing the tensor layout."""
31
+
32
+ shape: Sequence[int]
33
+ strides: Sequence[int]
34
+
35
+
36
+ @dataclass
37
+ class _ProblemSpec:
38
+ """This is used in a custom reduction to check that the Redistribute problem
39
+ specification is consistent across processes, and to infer global information
40
+ (e.g shape and memory layout)."""
41
+
42
+ @dataclass
43
+ class Options:
44
+ """
45
+ This is used for _ProblemSpec instead of ._configuration.RedistributeOptions
46
+ because it's going to be serialized as part of the custom reduction of the
47
+ _ProblemSpec, and we want to control which fields are included (for example
48
+ we don't need the logger).
49
+ """
50
+
51
+ def __init__(self, options: RedistributeOptions):
52
+ self.blocking = options.blocking
53
+
54
+ blocking: Literal[True, "auto"]
55
+
56
+ shape: tuple[int, ...] # operand local shape
57
+ is_F: bool # Is Fortran memory layout
58
+ is_C: bool # Is C memory layout
59
+ operand_dtype: str # str because TensorHolder.dtype returns str
60
+ package: Literal["numpy", "cupy", "torch"] # operand package
61
+ memory_space: Literal["cuda", "cpu"] # operand memory space
62
+ input_distribution: Distribution
63
+ output_distribution: Distribution
64
+ options: Options # Redistribute options
65
+ rank: int # only valid if is_leaf=True
66
+
67
+ # Global number of elements in the operand (calculated as part of the reduction).
68
+ global_size: int = 0
69
+ # is_leaf=True means that this is the _ProblemSpec of a process before reducing
70
+ # with that of another process.
71
+ is_leaf: bool = True
72
+
73
+
74
+ SHARED_REDISTRIBUTE_DOCUMENTATION = utils.COMMON_SHARED_DOC_MAP.copy()
75
+ SHARED_REDISTRIBUTE_DOCUMENTATION.update(
76
+ {
77
+ "operand": SHARED_REDISTRIBUTE_DOCUMENTATION["operand"],
78
+ #
79
+ "operand_admonitions": """
80
+ .. important::
81
+ GPU operands must be on the symmetric heap (for example, allocated with
82
+ ``nvmath.distributed.allocate_symmetric_memory()``).
83
+ """,
84
+ #
85
+ "options": """\
86
+ Specify options for Redistribute as a :class:`RedistributeOptions` object. Alternatively, a `dict`
87
+ containing the parameters for the ``RedistributeOptions`` constructor can also be provided. If not
88
+ specified, the value will be set to the default-constructed ``RedistributeOptions`` object.""".replace("\n", " "),
89
+ #
90
+ "input_distribution": """\
91
+ The distribution of the input operand across processes, determining which portion of
92
+ the global array each process holds.""".replace("\n", " "),
93
+ #
94
+ "output_distribution": """\
95
+ The desired distribution of the result across processes, where each process
96
+ specifies which portion of the global array it will hold after reshaping.""".replace("\n", " "),
97
+ #
98
+ "sync_symmetric_memory": """\
99
+ Indicates whether to issue a symmetric memory synchronization operation on the execute stream
100
+ before the redistribute operation. Note that before Redistribute starts executing, it is
101
+ required that the source operand be ready on all processes. A symmetric memory synchronization
102
+ ensures completion and visibility by all processes of previously issued local stores to
103
+ symmetric memory. Advanced users who choose to manage the synchronization on their own using
104
+ the appropriate NVSHMEM API, or who know that GPUs are already synchronized on the source
105
+ operand, can set this to False.""".replace("\n", " "),
106
+ #
107
+ "function_signature": """\
108
+ operand,
109
+ input_distribution: Distribution,
110
+ output_distribution: Distribution,
111
+ sync_symmetric_memory: bool = True,
112
+ options: RedistributeOptions | dict[str, Any] | None = None,
113
+ stream: AnyStream | None = None
114
+ """.replace("\n", " "),
115
+ #
116
+ "reset_operand_unchecked": utils._reset_operand_unchecked_docstring(
117
+ False, validation_examples="package match, data type match, shape/strides match", experimental=False
118
+ ),
119
+ }
120
+ )
121
+
122
+
123
+ def _calculate_strides(shape, axis_order):
124
+ """
125
+ Calculate the strides for the provided shape and axis order.
126
+ """
127
+ strides = [None] * len(shape)
128
+
129
+ stride = 1
130
+ for axis in axis_order:
131
+ strides[axis] = stride
132
+ stride *= shape[axis]
133
+
134
+ return strides
135
+
136
+
137
+ def _alloc_and_copy_to_exespace_mirror_or_identity(
138
+ user_operand: DistributedTensor,
139
+ stream_holder,
140
+ execution_space,
141
+ memory_space,
142
+ device_id: int | Literal["cpu"],
143
+ ) -> tuple[DistributedTensor, DistributedTensor | None]:
144
+ """
145
+ Allocate the exespace mirror for the given execution/memory space, or
146
+ return the user operand as-is (identity) when no mirror is needed.
147
+
148
+ Returns ``(operand, operand_backup)`` where:
149
+ - Same-space: ``operand`` is the user operand itself, ``operand_backup``
150
+ is ``None`` (no mirror needed).
151
+ - Cross-space: ``operand`` is the newly allocated device-side mirror,
152
+ ``operand_backup`` is the original user operand (for result copy-back).
153
+ """
154
+ if execution_space == memory_space:
155
+ return user_operand, None
156
+ assert execution_space == "cuda"
157
+ mirror = user_operand.to(device_id, stream_holder, symmetric_memory="nvshmem")
158
+ return mirror, user_operand
159
+
160
+
161
+ def _problem_spec_reducer(p1: _ProblemSpec, p2: _ProblemSpec):
162
+ try:
163
+ if isinstance(p1, Exception):
164
+ return p1 # propagate exception
165
+
166
+ if isinstance(p2, Exception):
167
+ return p2 # propagate exception
168
+
169
+ if len(p1.shape) != len(p2.shape):
170
+ return ValueError("The number of dimensions of the input operand is inconsistent across processes")
171
+
172
+ if len(p1.shape) <= 0 or len(p1.shape) > 3:
173
+ return ValueError(
174
+ "Redistribute currently only supports 1-D, 2-D and 3-D tensors."
175
+ f" The number of dimensions of the operand is {len(p1.shape)}."
176
+ )
177
+
178
+ if p1.operand_dtype != p2.operand_dtype:
179
+ return ValueError("The operand dtype is inconsistent across processes")
180
+
181
+ if p1.package != p2.package:
182
+ return ValueError("operand doesn't belong to the same package on all processes")
183
+
184
+ if p1.memory_space != p2.memory_space:
185
+ return ValueError('operand is not on the same memory space ("cpu", "cuda") on all processes')
186
+
187
+ if p1.options != p2.options:
188
+ return ValueError(f"options are inconsistent across processes: {p1.options} != {p2.options}")
189
+
190
+ # Determine the memory layout shared by all processes. Note that it's possible for
191
+ # a process to have both C and F layout (e.g. (2, 1) input shape).
192
+ p1.is_C &= p2.is_C
193
+ p1.is_F &= p2.is_F
194
+ if not p1.is_F and not p1.is_C:
195
+ return ValueError("The input memory layout is not C or Fortran, or is inconsistent across processes")
196
+
197
+ if type(p1.input_distribution) is not type(p2.input_distribution):
198
+ return ValueError(
199
+ f"input distribution types are inconsistent across processes: "
200
+ f"{type(p1.input_distribution)} != {type(p2.input_distribution)}"
201
+ )
202
+
203
+ if type(p1.output_distribution) is not type(p2.output_distribution):
204
+ return ValueError(
205
+ f"output distribution types are inconsistent across processes: "
206
+ f"{type(p1.output_distribution)} != {type(p2.output_distribution)}"
207
+ )
208
+
209
+ for p_spec in (p1, p2):
210
+ if p_spec.is_leaf:
211
+ p_spec.input_distribution._global_shape_reduction_prologue(p_spec.rank, p_spec.shape)
212
+ if isinstance(p_spec.output_distribution, Box):
213
+ # This allows checking (after the reduction) that the output boxes
214
+ # are valid.
215
+ p_spec.output_distribution._global_shape_reduction_prologue(
216
+ p_spec.rank, p_spec.output_distribution.box_shape
217
+ )
218
+
219
+ p1.input_distribution._global_shape_reduction(p2.input_distribution)
220
+ if isinstance(p1.output_distribution, Box):
221
+ p1.output_distribution._global_shape_reduction(p2.output_distribution)
222
+ elif p1.output_distribution != p2.output_distribution:
223
+ return ValueError(
224
+ f"output distribution types are inconsistent across processes: "
225
+ f"{p1.output_distribution} != {p2.output_distribution}"
226
+ )
227
+
228
+ if p1 is not p2: # with nranks=1 p1 is p2
229
+ p1.global_size += p2.global_size
230
+
231
+ except Exception as e:
232
+ return e
233
+ p1.is_leaf = False
234
+ return p1
235
+
236
+
237
+ class InvalidRedistributeState(Exception):
238
+ pass
239
+
240
+
241
+ @utils.docstring_decorator(SHARED_REDISTRIBUTE_DOCUMENTATION, skip_missing=False)
242
+ class Redistribute:
243
+ """
244
+ Create a stateful object that encapsulates the specified Redistribute operation
245
+ and required resources. This object ensures the validity of resources during use and
246
+ releases them when they are no longer needed to prevent misuse.
247
+
248
+ This object encompasses all functionalities of the function-form API
249
+ :func:`redistribute`, which is a convenience wrapper around it. The stateful object
250
+ also allows for the amortization of preparatory costs when the same Redistribute
251
+ operation is to be performed on multiple operands with the same problem specification
252
+ (see :meth:`reset_operand` for more details).
253
+
254
+ Using the stateful object typically involves the following steps:
255
+
256
+ 1. **Problem Specification**: Initialize the object with a defined operation and
257
+ options.
258
+ 2. **Preparation**: Use :meth:`plan` to determine the best distributed algorithmic
259
+ implementation for this specific Redistribute operation.
260
+ 3. **Execution**: Perform the redistribution with :meth:`execute`.
261
+ 4. **Resource Management**: Ensure all resources are released either by explicitly
262
+ calling :meth:`free` or by managing the stateful object within a context manager.
263
+
264
+ Detailed information on each step described above can be obtained by passing in a
265
+ :class:`logging.Logger` object to :class:`RedistributeOptions` or by setting the
266
+ appropriate options in the root logger object, which is used by default:
267
+
268
+ >>> import logging
269
+ >>> logging.basicConfig(
270
+ ... level=logging.INFO,
271
+ ... format="%(asctime)s %(levelname)-8s %(message)s",
272
+ ... datefmt="%m-%d %H:%M:%S",
273
+ ... )
274
+
275
+ .. versionchanged:: 1.0
276
+ This class was previously ``nvmath.distributed.reshape.Reshape``.
277
+
278
+ Args:
279
+ operand: {operand}
280
+ {operand_admonitions}
281
+
282
+ input_distribution: {input_distribution}
283
+
284
+ output_distribution: {output_distribution}
285
+
286
+ options: {options}
287
+
288
+ stream: {stream}
289
+
290
+ .. seealso::
291
+ :meth:`plan`, :meth:`reset_operand`, :meth:`execute`,
292
+ :meth:`release_operand`
293
+
294
+ Examples:
295
+
296
+ >>> import cupy as cp
297
+ >>> import nvmath.distributed
298
+
299
+ Get process group used to initialize nvmath.distributed (for information on
300
+ initializing nvmath.distributed, you can refer to the documentation or to the
301
+ Redistribute examples in `nvmath/examples/distributed/redistribute
302
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/redistribute>`_):
303
+
304
+ >>> process_group = nvmath.distributed.get_context().process_group
305
+ >>> rank = process_group.rank
306
+
307
+ Let's create a 3D floating-point ndarray on GPU, distributed across a certain number
308
+ of processes, according to the Slab distribution with partitioning on the X axis.
309
+
310
+ >>> shape = Slab.X.shape(rank, (global_shape))
311
+
312
+ Redistribute uses the NVSHMEM PGAS model, which requires GPU operands to be on the
313
+ symmetric heap:
314
+
315
+ >>> a = nvmath.distributed.allocate_symmetric_memory(shape, cp)
316
+ >>> a[:] = cp.random.rand(*shape)
317
+
318
+ With Redistribute, we will change how the ndarray is distributed, by specifying
319
+ the input distribution and the desired output distribution. In this example,
320
+ we redistribute from Slab.X to Slab.Y.
321
+
322
+ Create a Redistribute object encapsulating the problem specification above:
323
+
324
+ >>> r = nvmath.distributed.distribution.Redistribute(a, Slab.X, Slab.Y)
325
+
326
+ Options can be provided above to control the behavior of the operation using the
327
+ `options` argument (see :class:`RedistributeOptions`).
328
+
329
+ Next, plan the Redistribute:
330
+
331
+ >>> r.plan()
332
+
333
+ Now execute the Redistribute, and obtain the result `b` as a CuPy ndarray.
334
+ Redistribute always performs the distributed operation on the GPU.
335
+
336
+ >>> b = r.execute()
337
+
338
+ Finally, free the Redistribute object's resources. To avoid this explicit call, it's
339
+ recommended to use the Redistribute object as a context manager as shown below, if
340
+ possible.
341
+
342
+ >>> r.free()
343
+
344
+ Any symmetric memory that is owned by the user must be deleted explicitly (this is
345
+ a collective call and must be called by all processes):
346
+
347
+ >>> nvmath.distributed.free_symmetric_memory(a, b)
348
+
349
+ Note that all :class:`Redistribute` methods execute on the current stream by
350
+ default. Alternatively, the `stream` argument can be used to run a method on
351
+ a specified stream.
352
+
353
+ Let's now look at the same problem with NumPy ndarrays on the CPU.
354
+
355
+ >>> import numpy as np
356
+ >>> a = np.random.rand(*shape) # each process holds a different section
357
+
358
+ Create a Redistribute object encapsulating the problem specification described
359
+ earlier and use it as a context manager.
360
+
361
+ >>> with nvmath.distributed.distribution.Redistribute(a, Slab.X, Slab.Y) as r:
362
+ ... r.plan()
363
+ ...
364
+ ... # Execute the Redistribute to redistribute the ndarray.
365
+ ... b = r.execute()
366
+
367
+ All the resources used by the object are released at the end of the block.
368
+
369
+ Redistribute always executes on the GPU. In this case, because ``a`` resides in host
370
+ memory, the NumPy array is temporarily copied to device memory (on the symmetric
371
+ memory heap), re-distributed on the GPU, and the result is copied to host memory
372
+ as a NumPy array.
373
+
374
+ Further examples can be found in the `nvmath/examples/distributed/redistribute
375
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/redistribute>`_
376
+ directory.
377
+ """
378
+
379
+ def _free_internal_sheap(self, exception: Exception | None = None) -> bool:
380
+ # This is a fail-safe to free NVSHMEM internal memory in case of invalid
381
+ # state (Redistribute constructor fails). Since we might call nvshmem_free here,
382
+ # we're assuming that all processes equally failed in the ctor, which might not
383
+ # be true, but if it weren't true they would end up in deadlock most likely anyway.
384
+ if (
385
+ hasattr(self, "memory_space")
386
+ and self.memory_space == "cpu"
387
+ and self.operand is not None
388
+ and self.operand.device == "cuda"
389
+ ):
390
+ with utils.device_ctx(self.device_id):
391
+ self.operand.free_symmetric()
392
+ return True
393
+
394
+ @utils.atomic(_free_internal_sheap, method=True)
395
+ def __init__(
396
+ self,
397
+ operand,
398
+ /,
399
+ input_distribution: Distribution,
400
+ output_distribution: Distribution,
401
+ *,
402
+ options: RedistributeOptions | dict[str, Any] | None = None,
403
+ stream: AnyStream | None = None,
404
+ ):
405
+ distributed_ctx = nvmath.distributed.get_context()
406
+ if distributed_ctx is None:
407
+ raise RuntimeError(
408
+ "nvmath.distributed has not been initialized. Refer to "
409
+ "https://docs.nvidia.com/cuda/nvmath-python/latest/distributed-apis/runtime.html"
410
+ " for more information."
411
+ )
412
+ if not distributed_ctx.nvshmem_available:
413
+ raise RuntimeError("nvmath.distributed wasn't initialized with NVSHMEM backend")
414
+ self.process_group = distributed_ctx.process_group
415
+ rank = self.process_group.rank
416
+ nranks = self.process_group.nranks
417
+
418
+ self.operand = operand = tensor_wrapper.wrap_operand(operand)
419
+ self.options = options = cast(
420
+ RedistributeOptions, utils.check_or_create_options(RedistributeOptions, options, "Redistribute options")
421
+ )
422
+ self.package = operand.name
423
+
424
+ is_C = sorted(operand.strides, reverse=True) == list(operand.strides)
425
+ is_F = sorted(operand.strides) == list(operand.strides)
426
+
427
+ input_distribution = input_distribution.copy()
428
+ output_distribution = output_distribution.copy()
429
+
430
+ # Merge the problem specification across processes to make sure that there are no
431
+ # inconsistencies and to calculate the global shape. Importantly, this also does
432
+ # collective error checking of the Redistribute input parameters, to ensure that all
433
+ # processes fail on error of any one process, thus preventing deadlock.
434
+ problem_spec = _ProblemSpec(
435
+ input_distribution=input_distribution,
436
+ output_distribution=output_distribution,
437
+ shape=tuple(operand.shape),
438
+ operand_dtype=operand.dtype,
439
+ options=_ProblemSpec.Options(options),
440
+ package=self.package,
441
+ memory_space=operand.device,
442
+ global_size=math.prod(operand.shape),
443
+ is_C=is_C,
444
+ is_F=is_F,
445
+ rank=rank,
446
+ )
447
+ if nranks > 1:
448
+ problem_spec = self.process_group.allreduce_object(problem_spec, op=_problem_spec_reducer)
449
+ else:
450
+ # Ensure we error-check with one rank.
451
+ problem_spec = _problem_spec_reducer(problem_spec, problem_spec)
452
+ if isinstance(problem_spec, Exception):
453
+ # There is an error or inconsistency in the problem spec across processes.
454
+ # Note that since this comes from an allreduce, all processes will have
455
+ # received the same exception.
456
+ raise problem_spec
457
+
458
+ if problem_spec.is_C:
459
+ self.layout: Final = "C"
460
+ else:
461
+ assert problem_spec.is_F, "Internal Error." # The reducer is supposed to have detected this
462
+ self.layout: Final = "F" # type: ignore
463
+
464
+ self.operand_dim = len(operand.shape)
465
+
466
+ self.logger = options.logger if options.logger is not None else logging.getLogger()
467
+
468
+ # Infer the global shape from the input distribution.
469
+ global_shape = problem_spec.input_distribution._global_shape_reduction_epilogue()
470
+ self.logger.info(f"The global shape of the operand is {global_shape}.")
471
+
472
+ # Bind can't throw error since the local operand shape was already checked
473
+ # against the distribution in the ProblemSpec reducer.
474
+ input_distribution._bind(global_shape, shape=operand.shape)
475
+ try:
476
+ input_box = cast(Box, input_distribution.to(Box))
477
+ except ConvertDistributionError as e:
478
+ raise TypeError("Redistribute requires distributions compatible with Box distribution") from e
479
+
480
+ # Bind output distribution to the global shape and convert to Box if
481
+ # needed (for cuFFTMp).
482
+ # Note that cuFFTMp reshape doesn't support changing the global shape
483
+ # (the output will have the same global shape).
484
+ if isinstance(output_distribution, Box):
485
+ global_shape_out = problem_spec.output_distribution._global_shape_reduction_epilogue()
486
+ # The merged (global) output box must have the same shape as the
487
+ # global input operand.
488
+ if global_shape != global_shape_out:
489
+ raise ValueError(
490
+ "The global shape derived from the input and output distributions doesn't match: "
491
+ f"{global_shape} != {global_shape_out}"
492
+ )
493
+ try:
494
+ output_box = cast(Box, output_distribution._bind(global_shape).to(Box))
495
+ except ConvertDistributionError as e:
496
+ raise TypeError("Redistribute requires distributions compatible with Box distribution") from e
497
+
498
+ # The global number of elements must be compatible with the global shape.
499
+ if problem_spec.global_size != math.prod(global_shape):
500
+ raise ValueError(f"The global number of elements is incompatible with the inferred global shape {global_shape}")
501
+
502
+ # Store the local input and output box.
503
+ self.input_box: Box = input_box
504
+ self.output_box: Box = output_box
505
+
506
+ self.operand_data_type = operand.dtype
507
+ # TODO: change to `operand.dtype.itemsize` once operand is StridedMemoryView.
508
+ itemsize = operand.itemsize
509
+ if itemsize not in (4, 8, 16):
510
+ raise ValueError(
511
+ f"Redistribute only supports element sizes in (4, 8, 16) bytes. The operand's element size is {itemsize}"
512
+ )
513
+
514
+ self.logger.info(f"The Redistribute data type is {self.operand_data_type}.")
515
+
516
+ # Infer execution and memory space.
517
+ execution_device_id: int = distributed_ctx.device_id
518
+ if operand.device_id != "cpu": # exec space matches the mem space
519
+ self.memory_space = "cuda"
520
+ self.device_id = operand.device_id
521
+ assert operand.device_id == execution_device_id
522
+ else: # we need to move inputs cpu -> gpu and outputs gpu -> cpu
523
+ self.memory_space = "cpu"
524
+ self.device_id = execution_device_id
525
+ self.execution_space = "cuda"
526
+ self.operand_device_id = operand.device_id
527
+ self.internal_op_package = self._internal_operand_package(self.package)
528
+ stream_holder: StreamHolder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
529
+
530
+ if self.memory_space == "cuda" and not operand.is_symmetric_memory:
531
+ raise TypeError("Redistribute requires GPU operand to be on symmetric memory")
532
+
533
+ self.logger.info(
534
+ f"The input tensor's memory space is {self.memory_space}, and the execution space "
535
+ f"is {self.execution_space}, with device {self.device_id}."
536
+ )
537
+
538
+ self.logger.info(f"The specified stream for the Redistribute ctor is {stream_holder and stream_holder.obj}")
539
+
540
+ # Copy the operand to execution_space's device if needed.
541
+ self.operand, self.operand_backup = _alloc_and_copy_to_exespace_mirror_or_identity(
542
+ operand,
543
+ stream_holder,
544
+ self.execution_space,
545
+ self.memory_space,
546
+ self.device_id,
547
+ )
548
+ operand = self.operand
549
+ # Capture operand layout for consistency checks when resetting operands.
550
+ self.operand_layout = TensorLayout(shape=operand.shape, strides=operand.strides)
551
+
552
+ self.result_layout: TensorLayout | None = None
553
+ # We'll infer the result layout at plan time.
554
+ self.result_class: DistributedTensor = operand.__class__
555
+ self.result_data_type = operand.dtype
556
+
557
+ # Set blocking or non-blocking behavior.
558
+ self.blocking = self.options.blocking is True or self.memory_space == "cpu"
559
+ if self.blocking:
560
+ self.call_prologue = "This call is blocking and will return only after the operation is complete."
561
+ else:
562
+ self.call_prologue = (
563
+ "This call is non-blocking and will return immediately after the operation is launched on the device."
564
+ )
565
+
566
+ # Set memory allocator.
567
+ self.allocator = NvshmemMemoryManager(self.device_id, self.logger)
568
+
569
+ # Create handle.
570
+ with utils.device_ctx(self.device_id):
571
+ self.handle = cufftMp.create_reshape()
572
+
573
+ self.redistribute_planned = False
574
+
575
+ # Workspace attributes.
576
+ self.workspace_ptr, self.workspace_size = None, None
577
+ self._workspace_allocated_here = False
578
+
579
+ # Attributes to establish stream ordering.
580
+ self.workspace_stream = None
581
+ self.last_compute_event = None
582
+
583
+ # Track whether the user has called release_operand(). This flag is
584
+ # checked in _check_valid_operand to prevent execution after the user
585
+ # has released their operand. It is cleared by reset_operand().
586
+ self._operand_released = False
587
+
588
+ self.valid_state = True
589
+ self.logger.info("The Redistribute operation has been created.")
590
+
591
+ def __enter__(self):
592
+ return self
593
+
594
+ def __exit__(self, exc_type, exc_value, traceback):
595
+ self.free()
596
+
597
+ def _check_valid_redistribute(self, *args, **kwargs):
598
+ """
599
+ Check if Redistribute object is alive and well.
600
+ """
601
+ if not self.valid_state:
602
+ raise InvalidRedistributeState("The Redistribute object cannot be used after resources are free'd")
603
+
604
+ def _free_plan_resources(self, exception: Exception | None = None) -> bool:
605
+ """
606
+ Free resources allocated in planning.
607
+ """
608
+ self.redistribute_planned = False
609
+ return True
610
+
611
+ def _internal_operand_package(self, package_name):
612
+ return package_name if package_name != "numpy" else "cuda"
613
+
614
+ def _allocate_result_operand(self, exec_stream_holder, log_debug):
615
+ if log_debug:
616
+ self.logger.debug("Beginning output (empty) tensor creation...")
617
+ self.logger.debug(
618
+ f"The output tensor shape = {self.result_layout.shape} with strides = "
619
+ f"{self.result_layout.strides} and data type '{self.result_data_type}'."
620
+ )
621
+ result = utils.create_empty_tensor(
622
+ self.result_class,
623
+ self.result_layout.shape,
624
+ self.result_data_type,
625
+ self.device_id,
626
+ exec_stream_holder,
627
+ verify_strides=False,
628
+ strides=self.result_layout.strides,
629
+ symmetric_memory="nvshmem",
630
+ make_symmetric=True,
631
+ logger=self.logger,
632
+ )
633
+ if log_debug:
634
+ self.logger.debug("The output (empty) tensor has been created.")
635
+ return result
636
+
637
+ @utils.precondition(_check_valid_redistribute)
638
+ @utils.atomic(_free_plan_resources, method=True)
639
+ def plan(self, stream: AnyStream | None = None):
640
+ """Plan the Redistribute.
641
+
642
+ Args:
643
+ stream: {stream}
644
+ """
645
+ log_info = self.logger.isEnabledFor(logging.INFO)
646
+
647
+ if self.redistribute_planned:
648
+ self.logger.debug("The Redistribute has already been planned, and redoing the plan is not supported.")
649
+ return
650
+
651
+ stream_holder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
652
+ self.workspace_stream = stream_holder.obj
653
+
654
+ if log_info:
655
+ self.logger.info("Starting Redistribute planning...")
656
+
657
+ lower_input, upper_input = self.input_box
658
+ lower_output, upper_output = self.output_box
659
+
660
+ def calculate_redistribute_params(lower, upper, operand, order: Literal["C", "F"]):
661
+ """Calculate shape and strides for input or output of Redistribute."""
662
+ local_shape = tuple(upper[i] - lower[i] for i in range(self.operand_dim))
663
+ if operand is not None:
664
+ # Take the strides from the operand.
665
+ strides = operand.strides
666
+ elif order == "F":
667
+ # This is out=None with F layout.
668
+ strides = _calculate_strides(local_shape, range(self.operand_dim))
669
+ else:
670
+ # This is out=None with C layout.
671
+ strides = _calculate_strides(local_shape, reversed(range(self.operand_dim)))
672
+ return local_shape, strides
673
+
674
+ input_local_shape, strides_input = calculate_redistribute_params(lower_input, upper_input, self.operand, self.layout)
675
+ output_local_shape, strides_output = calculate_redistribute_params(lower_output, upper_output, None, self.layout)
676
+ self.result_layout = TensorLayout(shape=output_local_shape, strides=strides_output)
677
+
678
+ if self.operand_dim < 3:
679
+ # cufftMp.make_reshape only supports 3D, so we broadcast the local operands.
680
+ N = 3 - self.operand_dim
681
+ lower_input = tuple(lower_input) + (0,) * N
682
+ upper_input = tuple(upper_input) + (1,) * N
683
+ lower_output = tuple(lower_output) + (0,) * N
684
+ upper_output = tuple(upper_output) + (1,) * N
685
+
686
+ strides_input = self.operand.reshape(input_local_shape + (1,) * N, copy=False).strides
687
+ if self.layout == "F":
688
+ strides_output = _calculate_strides(output_local_shape + (1,) * N, (0, 1, 2))
689
+ else:
690
+ strides_output = _calculate_strides(output_local_shape + (1,) * N, (2, 1, 0))
691
+
692
+ # cuFFTMp only supports decreasing strides. To support increasing strides, we'll
693
+ # pass the required metadata to cuFFTMp with axes transposed (but won't touch
694
+ # the data).
695
+ if self.layout == "F":
696
+ lower_input = tuple(reversed(lower_input))
697
+ upper_input = tuple(reversed(upper_input))
698
+ strides_input = tuple(reversed(strides_input))
699
+ lower_output = tuple(reversed(lower_output))
700
+ upper_output = tuple(reversed(upper_output))
701
+ strides_output = tuple(reversed(strides_output))
702
+
703
+ with utils.host_call_ctx(timing=log_info) as elapsed, utils.device_ctx(self.device_id):
704
+ nullptr = 0
705
+ cufftMp.make_reshape(
706
+ self.handle,
707
+ # TODO: change to `operand.dtype.itemsize` once operand is StridedMemoryView
708
+ self.operand.itemsize,
709
+ 3,
710
+ lower_input,
711
+ upper_input,
712
+ strides_input,
713
+ lower_output,
714
+ upper_output,
715
+ strides_output,
716
+ nullptr,
717
+ cufftMp.MpCommType.COMM_NONE,
718
+ )
719
+ self.workspace_size = cufftMp.get_reshape_size(self.handle)
720
+
721
+ self.redistribute_planned = True
722
+
723
+ if log_info and elapsed.data is not None:
724
+ self.logger.info(f"The Redistribute planning phase took {elapsed.data:.3f} ms to complete.")
725
+
726
+ def _validate_reset_operand(self, operand) -> DistributedTensor:
727
+ """
728
+ (private) Validate operand for reset_operand performing
729
+ all precondition checks without any side effects.
730
+
731
+ This method does **not** mutate any instance state.
732
+ """
733
+ # First wrap operand.
734
+ wrapped = tensor_wrapper.wrap_operand(operand)
735
+
736
+ # Check package match.
737
+ package = utils.infer_object_package(wrapped.tensor)
738
+ if self.package != package:
739
+ message = f"Library package mismatch: '{self.package}' => '{package}'"
740
+ raise TypeError(message)
741
+
742
+ # Check the dtype.
743
+ utils.check_attribute_match(self.operand_data_type, wrapped.dtype, "data type")
744
+
745
+ # In principle, we could support memory_space change,
746
+ # but to handle it properly we need to update self.memory_space and
747
+ # some dependent properties, like self.blocking, which may be error-prone
748
+ # from the user perspective. It would prevent inplace optimizations as well.
749
+ operand_device_id = wrapped.device_id
750
+ if operand_device_id != self.operand_device_id:
751
+
752
+ def device_str(device_id: int | Literal["cpu"]) -> str:
753
+ return f"cuda:{device_id}" if isinstance(device_id, int) else f"{device_id}"
754
+
755
+ raise ValueError(
756
+ f"The new operand must be on the same device as the original one. "
757
+ f"The new operand's device is {device_str(operand_device_id)}, "
758
+ f"the original device is {device_str(self.operand_device_id)}"
759
+ )
760
+
761
+ if self.memory_space == "cuda" and not wrapped.is_symmetric_memory:
762
+ raise TypeError("Redistribute requires GPU operand to be on symmetric memory")
763
+
764
+ # The plan was made for a specific input box and strides, so the new operand must
765
+ # match.
766
+ if wrapped.shape != self.operand_layout.shape:
767
+ raise ValueError(
768
+ f"The shape of the new operand ({wrapped.shape}) does not match "
769
+ f"the original shape ({self.operand_layout.shape})."
770
+ )
771
+ if wrapped.strides != self.operand_layout.strides:
772
+ raise ValueError(
773
+ f"The strides of the new operand ({wrapped.strides}) do not match "
774
+ f"the original strides ({self.operand_layout.strides})."
775
+ )
776
+ return wrapped
777
+
778
+ def _apply_reset_operand(self, operand, stream: AnyStream | None, operand_wrapped: DistributedTensor | None = None):
779
+ """
780
+ (private) Perform the actual operand swap.
781
+
782
+ Receives the raw (unwrapped) user operand. For the cross-space
783
+ (CPU) path the operand is wrapped if not already provided via
784
+ *operand_wrapped*; for the same-space (GPU) path the raw tensor
785
+ is stored directly.
786
+ """
787
+ if self.memory_space == "cpu":
788
+ stream_holder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
789
+ if operand_wrapped is None:
790
+ operand_wrapped = tensor_wrapper.wrap_operand(operand)
791
+ if self._operand_released:
792
+ new_mirror = operand_wrapped.to(self.device_id, stream_holder, symmetric_memory="nvshmem")
793
+ self.operand.tensor = new_mirror.tensor
794
+ else:
795
+ self.operand.copy_(operand_wrapped, stream_holder=stream_holder)
796
+ self.operand_backup.tensor = operand_wrapped.tensor # type: ignore[union-attr]
797
+ else:
798
+ self.operand.tensor = operand
799
+
800
+ self._operand_released = False
801
+
802
+ @utils.precondition(_check_valid_redistribute)
803
+ def reset_operand(self, operand, *, stream: AnyStream | None = None):
804
+ """
805
+ Reset the operand held by this :class:`Redistribute` instance. This method is used
806
+ to provide a new operand for execution.
807
+
808
+ Args:
809
+ operand: A tensor (ndarray-like object) compatible with the previous one.
810
+ The new operand is considered compatible if all the following properties
811
+ match with the previous one:
812
+
813
+ - The operand distribution.
814
+ - The package that the new operand belongs to.
815
+ - The dtype of the new operand.
816
+ - The shape and strides of the new operand.
817
+ - The memory space of the new operand (CPU or GPU).
818
+ - The device that new operand belongs to if it is on GPU.
819
+
820
+ stream: {stream}
821
+
822
+ .. seealso::
823
+ :meth:`release_operand`
824
+
825
+ Examples:
826
+
827
+ >>> import cupy as cp
828
+ >>> import nvmath.distributed
829
+
830
+ Get process group used to initialize nvmath.distributed (for information on
831
+ initializing nvmath.distributed, you can refer to the documentation or to the
832
+ Redistribute examples in `nvmath/examples/distributed/redistribute
833
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/redistribute>`_):
834
+
835
+ >>> process_group = nvmath.distributed.get_context().process_group
836
+ >>> nranks = process_group.nranks
837
+
838
+ Create a 3-D complex128 ndarray on GPU symmetric memory, initially partitioned
839
+ on the X axis (the global shape is (128, 128, 128)):
840
+
841
+ >>> shape = 128 // nranks, 128, 128
842
+ >>> dtype = cp.complex128
843
+ >>> a = nvmath.distributed.allocate_symmetric_memory(shape, cp, dtype=dtype)
844
+ >>> a[:] = cp.random.rand(*shape) + 1j * cp.random.rand(*shape)
845
+
846
+ Create a Redistribute object as a context manager
847
+
848
+ >>> from nvmath.distributed.distribution import Redistribute
849
+ >>> with Redistribute(a, in_distribution, out_distribution) as r:
850
+ ... # Plan the Redistribute
851
+ ... r.plan()
852
+ ...
853
+ ... # Execute the Redistribute to get the first result.
854
+ ... result1 = r.execute()
855
+ ...
856
+ ... # Reset the operand to a new CuPy ndarray.
857
+ ... b = nvmath.distributed.allocate_symmetric_memory(shape, cp, dtype=dtype)
858
+ ... b[:] = cp.random.rand(*shape) + 1j * cp.random.rand(*shape)
859
+ ... r.reset_operand(b)
860
+ ...
861
+ ... # Execute to get the new result corresponding to the updated operand.
862
+ ... result2 = r.execute()
863
+
864
+ With :meth:`reset_operand`, minimal overhead is achieved as problem
865
+ specification and planning are only performed once.
866
+
867
+ For the particular example above, explicitly calling :meth:`reset_operand` is
868
+ equivalent to updating the operand in-place, i.e, replacing
869
+ ``r.reset_operand(b)`` with ``a[:]=b``. Note that updating the operand in-place
870
+ should be adopted with caution as it can only yield the expected result and
871
+ incur no additional copies under the additional constraints below:
872
+
873
+ - The operand's distribution is the same.
874
+
875
+ For more details, please refer to `inplace update example
876
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/redistribute/example05_stateful_reset_inplace.py>`_.
877
+ """
878
+ self.logger.info("Resetting operand...")
879
+
880
+ if operand is None:
881
+ raise ValueError("reset_operand() requires a valid operand.")
882
+
883
+ operand_wrapped = self._validate_reset_operand(operand)
884
+ self._apply_reset_operand(operand, stream, operand_wrapped=operand_wrapped)
885
+ self.logger.info(f"The reset operand shape = {self.operand_layout.shape}, and strides = {self.operand_layout.strides}.")
886
+ self.logger.info("The operand has been reset to the specified operand.")
887
+
888
+ def reset_operand_unchecked(self, operand, *, stream: AnyStream | None = None):
889
+ """
890
+ {reset_operand_unchecked}
891
+ """
892
+ self._apply_reset_operand(operand, stream)
893
+
894
+ @utils.precondition(_check_valid_redistribute)
895
+ def release_operand(self):
896
+ """
897
+ {release_operand}
898
+ """
899
+ if self._operand_released:
900
+ self.logger.info("Operand has already been released; nothing to do.")
901
+ return
902
+
903
+ # Keep the DistributedTensor wrappers alive and only release the
904
+ # internal tensor reference. This is useful when
905
+ # resetting operands subsequently because it can reuse the
906
+ # existing wrappers, saving overhead.
907
+ if self.memory_space == self.execution_space:
908
+ # Same-space (GPU input): self.operand is the user's tensor.
909
+ self.operand.tensor = None
910
+ else:
911
+ # Cross-space (CPU input): self.operand_backup is the user's tensor,
912
+ # self.operand is an internal nvshmem device mirror.
913
+ # Release user reference and free the nvshmem mirror.
914
+ # Cross-space execution is always blocking, so no synchronization
915
+ # is needed before freeing.
916
+ self.operand_backup.tensor = None
917
+ with utils.device_ctx(self.device_id):
918
+ self.operand.free_symmetric()
919
+ self.operand.tensor = None
920
+
921
+ self._operand_released = True
922
+ self.logger.info("User-provided operand has been released.")
923
+
924
+ def _check_planned(self, *args, **kwargs):
925
+ """ """
926
+ what = kwargs["what"]
927
+ if not self.redistribute_planned:
928
+ raise RuntimeError(f"{what} cannot be performed before plan() has been called.")
929
+
930
+ def _check_valid_operand(self, *args, **kwargs):
931
+ """ """
932
+ what = kwargs["what"]
933
+ if self._operand_released:
934
+ raise RuntimeError(
935
+ f"{what} cannot be performed after the operand has been released. Use reset_operand() to provide a new "
936
+ f"operand before performing the {what.lower()}."
937
+ )
938
+
939
+ def _free_workspace_memory(self, exception: Exception | None = None) -> bool:
940
+ """
941
+ Free workspace by releasing the MemoryPointer object.
942
+ """
943
+ if self.workspace_ptr is None:
944
+ return True
945
+
946
+ with utils.device_ctx(self.device_id):
947
+ # Calling nvshmem_free on memory that's still in use is not safe
948
+ # (nvshmem_free is not stream-ordered), so we need to wait for the
949
+ # computation to finish.
950
+ if self.workspace_stream is not None:
951
+ self.workspace_stream.sync()
952
+ self.workspace_ptr.free()
953
+ self.workspace_ptr = None
954
+ self.logger.debug("[_free_workspace_memory] The workspace has been released.")
955
+
956
+ return True
957
+
958
+ @utils.precondition(_check_valid_redistribute)
959
+ @utils.precondition(_check_planned, "Workspace memory allocation")
960
+ @utils.atomic(_free_workspace_memory, method=True)
961
+ def _allocate_workspace_memory(self, stream_holder):
962
+ """
963
+ Allocate workspace memory using the specified allocator.
964
+ """
965
+
966
+ assert self._workspace_allocated_here is False, "Internal Error."
967
+
968
+ self.logger.debug("Allocating workspace for performing the Redistribute...")
969
+ with utils.device_ctx(self.device_id), stream_holder.ctx:
970
+ try:
971
+ self.workspace_ptr = self.allocator.memalloc(self.workspace_size)
972
+ self._workspace_allocated_here = True
973
+ except TypeError as e:
974
+ message = (
975
+ "The method 'memalloc' in the allocator object must conform to the interface in the "
976
+ "'BaseCUDAMemoryManager' protocol."
977
+ )
978
+ raise TypeError(message) from e
979
+
980
+ self.workspace_stream = stream_holder.obj
981
+ self.logger.debug(
982
+ f"Finished allocating device workspace of size {formatters.MemoryStr(self.workspace_size)} in the context "
983
+ f"of stream {self.workspace_stream}."
984
+ )
985
+
986
+ def _allocate_workspace_memory_perhaps(self, stream_holder):
987
+ """
988
+ Allocate workspace memory using the specified allocator, if it hasn't already been
989
+ done.
990
+ """
991
+ if self.execution_space != "cuda" or self.workspace_ptr is not None:
992
+ return
993
+
994
+ return self._allocate_workspace_memory(stream_holder)
995
+
996
+ @utils.precondition(_check_valid_redistribute)
997
+ def _free_workspace_memory_perhaps(self, release_workspace):
998
+ """
999
+ Free workspace memory if if 'release_workspace' is True.
1000
+ """
1001
+ if not release_workspace:
1002
+ return
1003
+
1004
+ # Establish ordering wrt the computation and free workspace if it's more than the
1005
+ # specified cache limit.
1006
+ if self.last_compute_event is not None:
1007
+ with utils.device_ctx(self.device_id):
1008
+ self.workspace_stream.wait(self.last_compute_event)
1009
+ self.logger.debug("Established ordering with respect to the computation before releasing the workspace.")
1010
+ self.last_compute_event = None
1011
+
1012
+ self.logger.debug("[_free_workspace_memory_perhaps] The workspace memory will be released.")
1013
+ self._free_workspace_memory()
1014
+
1015
+ return True
1016
+
1017
+ def _release_workspace_memory_perhaps(self, exception: Exception | None = None) -> bool:
1018
+ """
1019
+ Free workspace memory if it was allocated in this call
1020
+ (self._workspace_allocated_here == True) when an exception occurs.
1021
+ """
1022
+ release_workspace = self._workspace_allocated_here
1023
+ self.logger.debug(
1024
+ f"[_release_workspace_memory_perhaps] The release_workspace flag is set to {release_workspace} based upon "
1025
+ "the value of 'workspace_allocated_here'."
1026
+ )
1027
+ self._free_workspace_memory_perhaps(release_workspace)
1028
+ self._workspace_allocated_here = False
1029
+ return True
1030
+
1031
+ @utils.precondition(_check_valid_redistribute)
1032
+ @utils.precondition(_check_planned, "Execution")
1033
+ @utils.precondition(_check_valid_operand, "Execution")
1034
+ @utils.atomic(_release_workspace_memory_perhaps, method=True)
1035
+ def execute(self, stream: AnyStream | None = None, release_workspace: bool = False, sync_symmetric_memory: bool = True):
1036
+ """
1037
+ Execute the Redistribute operation.
1038
+
1039
+ Args:
1040
+ stream: {stream}
1041
+
1042
+ release_workspace: A value of `True` specifies that the Redistribute object
1043
+ should release workspace memory back to the symmetric memory pool on
1044
+ function return, while a value of `False` specifies that the object
1045
+ should retain the memory. This option may be set to `True` if the
1046
+ application performs other operations that consume a lot of memory between
1047
+ successive calls to the (same or different) :meth:`execute` API, but incurs
1048
+ an overhead due to obtaining and releasing workspace memory from and
1049
+ to the symmetric memory pool on every call. The default is `False`.
1050
+ **NOTE: All processes must use the same value or the application can
1051
+ deadlock.**
1052
+
1053
+ sync_symmetric_memory: {sync_symmetric_memory}
1054
+
1055
+ Returns:
1056
+ The redistributed operand, which remains on the same device and utilizes the
1057
+ same package as the input operand. For GPU operands, the result will be in
1058
+ symmetric memory and the user is responsible for explicitly deallocating it
1059
+ (for example, using ``nvmath.distributed.free_symmetric_memory(tensor)``).
1060
+ """
1061
+
1062
+ log_info = self.logger.isEnabledFor(logging.INFO)
1063
+ log_debug = self.logger.isEnabledFor(logging.DEBUG)
1064
+
1065
+ stream_holder: StreamHolder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
1066
+
1067
+ # Allocate workspace if needed.
1068
+ self._allocate_workspace_memory_perhaps(stream_holder)
1069
+
1070
+ # Allocate output operand.
1071
+ with utils.device_ctx(self.device_id):
1072
+ result = self._allocate_result_operand(stream_holder, log_debug)
1073
+
1074
+ if log_info:
1075
+ self.logger.info("Starting Redistribute...")
1076
+ self.logger.info(f"{self.call_prologue}")
1077
+
1078
+ with utils.cuda_call_ctx(stream_holder, self.blocking, timing=log_info) as (
1079
+ self.last_compute_event,
1080
+ elapsed,
1081
+ ):
1082
+ raw_workspace_ptr = utils.get_ptr_from_memory_pointer(self.workspace_ptr)
1083
+ if sync_symmetric_memory:
1084
+ nvshmem.sync_all_on_stream(stream_holder.ptr)
1085
+ if log_info:
1086
+ self.logger.info(
1087
+ "sync_symmetric_memory is enabled (this may incur redundant multi-GPU "
1088
+ "synchronization, please refer to the documentation for more information)"
1089
+ )
1090
+ elif log_info:
1091
+ self.logger.info("sync_symmetric_memory is disabled")
1092
+ cufftMp.exec_reshape_async(
1093
+ self.handle, result.data_ptr, self.operand.data_ptr, raw_workspace_ptr, stream_holder.ptr
1094
+ )
1095
+
1096
+ if log_info and elapsed.data is not None:
1097
+ self.logger.info(f"The Redistribute took {elapsed.data:.3f} ms to complete.")
1098
+
1099
+ # Establish ordering wrt the computation and free workspace if it's more than the
1100
+ # specified cache limit.
1101
+ self._free_workspace_memory_perhaps(release_workspace)
1102
+
1103
+ # reset workspace allocation tracking to False at the end of the methods where
1104
+ # workspace memory is potentially allocated. This is necessary to prevent any
1105
+ # exceptions raised before method entry from using stale tracking values.
1106
+ self._workspace_allocated_here = False
1107
+
1108
+ if self.memory_space == "cpu":
1109
+ out = result.to("cpu", stream_holder=stream_holder).tensor
1110
+ with utils.device_ctx(self.device_id):
1111
+ # Since the execution when user passes CPU operands is blocking, it's
1112
+ # safe to call nvshmem_free here without additional synchronization.
1113
+ result.free_symmetric()
1114
+ else:
1115
+ out = result.tensor
1116
+
1117
+ return out
1118
+
1119
+ def free(self):
1120
+ """Free Redistribute resources.
1121
+
1122
+ It is recommended that the :class:`Redistribute` object be used within a context,
1123
+ but if it's not possible then this method must be called explicitly to ensure that
1124
+ the Redistribute resources (especially internal library objects) are properly
1125
+ cleaned up.
1126
+ """
1127
+
1128
+ if not self.valid_state:
1129
+ return
1130
+
1131
+ try:
1132
+ # Future operations on the workspace stream should be ordered after the
1133
+ # computation.
1134
+ if self.last_compute_event is not None and self.workspace_stream is not None:
1135
+ with utils.device_ctx(self.device_id):
1136
+ self.workspace_stream.wait(self.last_compute_event)
1137
+ self.last_compute_event = None
1138
+
1139
+ self._free_workspace_memory()
1140
+
1141
+ with utils.device_ctx(self.device_id):
1142
+ if self.handle is not None:
1143
+ cufftMp.destroy_reshape(self.handle)
1144
+ self.handle = None
1145
+
1146
+ if self.memory_space == "cpu" and not self._operand_released:
1147
+ # In this case, self.operand is an internal GPU operand owned by
1148
+ # Redistribute. Since the execution when user passes CPU operands is
1149
+ # blocking, it's safe to call nvshmem_free here without additional
1150
+ # synchronization.
1151
+ self.operand.free_symmetric()
1152
+
1153
+ # Set all attributes to None except for logger and valid_state
1154
+ _keep = {"logger", "valid_state"}
1155
+ for attr in list(vars(self)):
1156
+ if attr not in _keep:
1157
+ setattr(self, attr, None)
1158
+
1159
+ except Exception as e:
1160
+ self.logger.critical("Internal error: only part of the Redistribute object's resources have been released.")
1161
+ self.logger.critical(str(e))
1162
+ raise e
1163
+ finally:
1164
+ self.valid_state = False
1165
+
1166
+ self.logger.info("The Redistribute object's resources have been released.")
1167
+
1168
+
1169
+ @utils.docstring_decorator(SHARED_REDISTRIBUTE_DOCUMENTATION, skip_missing=False)
1170
+ def redistribute(
1171
+ operand,
1172
+ /,
1173
+ input_distribution: Distribution,
1174
+ output_distribution: Distribution,
1175
+ *,
1176
+ sync_symmetric_memory: bool = True,
1177
+ options: RedistributeOptions | dict[str, Any] | None = None,
1178
+ stream: AnyStream | None = None,
1179
+ ):
1180
+ r"""
1181
+ redistribute({function_signature})
1182
+
1183
+ Perform a redistribute operation on the provided operand to change its distribution
1184
+ across processes.
1185
+
1186
+ .. versionchanged:: 1.0
1187
+ This function was previously ``nvmath.distributed.reshape.reshape``.
1188
+
1189
+ Args:
1190
+ operand: {operand}
1191
+ {operand_admonitions}
1192
+
1193
+ input_distribution: {input_distribution}
1194
+
1195
+ output_distribution: {output_distribution}
1196
+
1197
+ sync_symmetric_memory: {sync_symmetric_memory}
1198
+
1199
+ options: {options}
1200
+
1201
+ stream: {stream}
1202
+
1203
+ Returns:
1204
+ A tensor that remains on the same device and belongs to the same package as
1205
+ the input operand, with shape according to output_box.
1206
+
1207
+ .. seealso::
1208
+ :class:`Redistribute`.
1209
+
1210
+ Examples:
1211
+
1212
+ >>> import cupy as cp
1213
+ >>> import nvmath.distributed
1214
+
1215
+ Get process group used to initialize nvmath.distributed (for information on
1216
+ initializing nvmath.distributed, you can refer to the documentation or to the
1217
+ Redistribute examples in `nvmath/examples/distributed/redistribute
1218
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/redistribute>`_):
1219
+
1220
+ >>> process_group = nvmath.distributed.get_context().process_group
1221
+ >>> rank = process_group.rank
1222
+
1223
+ Let's create a 3D floating-point ndarray on GPU, distributed across a certain number
1224
+ of processes, according to the Slab distribution with partitioning on the X axis.
1225
+
1226
+ >>> shape = Slab.X.shape(rank, (global_shape))
1227
+
1228
+ Redistribute uses the NVSHMEM PGAS model, which requires GPU operands to be on the
1229
+ symmetric heap:
1230
+
1231
+ >>> a = nvmath.distributed.allocate_symmetric_memory(shape, cp)
1232
+ >>> a[:] = cp.random.rand(*shape)
1233
+
1234
+ With Redistribute, we will change how the ndarray is distributed, by specifying
1235
+ the input distribution and the desired output distribution. In this example,
1236
+ we redistribute from Slab.X to Slab.Y.
1237
+
1238
+ Perform the redistribute operation using :func:`redistribute`:
1239
+
1240
+ >>> r = nvmath.distributed.distribution.redistribute(a, Slab.X, Slab.Y)
1241
+
1242
+ See :class:`RedistributeOptions` for the complete list of available options.
1243
+
1244
+ The package current stream is used by default, but a stream can be explicitly
1245
+ provided to the Redistribute operation. This can be done if the Redistribute
1246
+ operand is computed on a different stream, for example:
1247
+
1248
+ >>> s = cp.cuda.Stream()
1249
+ >>> with s:
1250
+ ... a = nvmath.distributed.allocate_symmetric_memory(shape, cp)
1251
+ ... a[:] = cp.random.rand(*shape)
1252
+ >>> r = nvmath.distributed.distribution.redistribute(a, Slab.X, Slab.Y, stream=s)
1253
+
1254
+ The operation above runs on stream `s` and is ordered with respect to the input
1255
+ computation.
1256
+
1257
+ Create a NumPy ndarray on the CPU.
1258
+
1259
+ >>> import numpy as np
1260
+ >>> b = np.random.rand(*shape)
1261
+
1262
+ Provide the NumPy ndarray to :func:`redistribute`, with the result also being
1263
+ a NumPy ndarray:
1264
+
1265
+ >>> r = nvmath.distributed.distribution.redistribute(b, Slab.X, Slab.Y)
1266
+
1267
+ Notes:
1268
+ - This function is a convenience wrapper around :class:`Redistribute` and is
1269
+ specifically meant for *single* use. The same computation can be performed
1270
+ with the stateful API.
1271
+
1272
+ Further examples can be found in the `nvmath/examples/distributed/redistribute
1273
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/redistribute>`_
1274
+ directory.
1275
+ """
1276
+
1277
+ with Redistribute(operand, input_distribution, output_distribution, options=options, stream=stream) as redistribute_obj:
1278
+ # Plan the Redistribute.
1279
+ redistribute_obj.plan(stream=stream)
1280
+
1281
+ # Execute the Redistribute.
1282
+ result = redistribute_obj.execute(sync_symmetric_memory=sync_symmetric_memory, stream=stream)
1283
+
1284
+ return result