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,2742 @@
1
+ # Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ __all__ = ["allocate_operand", "FFT", "fft", "ifft", "rfft", "irfft"]
6
+
7
+ import functools
8
+ import logging
9
+ import math
10
+ from collections.abc import Sequence
11
+ from dataclasses import dataclass
12
+ from types import ModuleType
13
+ from typing import Any, Literal, cast
14
+
15
+ import numpy as np
16
+
17
+ import nvmath.distributed
18
+ from nvmath import memory
19
+ from nvmath._internal.layout import is_overlapping_layout
20
+ from nvmath.bindings import cufftMp as cufft # type: ignore
21
+ from nvmath.bindings import nvshmem # type: ignore
22
+ from nvmath.distributed._internal import tensor_wrapper
23
+ from nvmath.distributed._internal.nvshmem import NvshmemMemoryManager
24
+ from nvmath.distributed._internal.tensor_ifc import DistributedTensor
25
+ from nvmath.distributed._internal.tensor_ifc_numpy import CudaDistributedTensor
26
+ from nvmath.distributed.distribution import Box, Distribution, Slab
27
+ from nvmath.internal import formatters, ndbuffer, utils
28
+ from nvmath.internal.package_wrapper import AnyStream, StreamHolder
29
+ from nvmath.internal.typemaps import NAME_TO_DATA_TYPE, NAME_TO_ITEM_SIZE
30
+
31
+ from ._configuration import FFTDirection, FFTOptions
32
+
33
+
34
+ @dataclass
35
+ class TensorLayout:
36
+ """An internal data class for capturing the tensor layout."""
37
+
38
+ shape: Sequence[int]
39
+ strides: Sequence[int]
40
+
41
+
42
+ @dataclass
43
+ class _ProblemSpec:
44
+ """This is used in a custom reduction to check that the FFT problem specification
45
+ is consistent across processes, and to infer global information (e.g shape)."""
46
+
47
+ @dataclass
48
+ class Options:
49
+ """
50
+ This is used for _ProblemSpec instead of FFTOptions
51
+ because it's going to be serialized as part of the custom reduction of the
52
+ _ProblemSpec, and we want to control which fields are included (for example
53
+ we don't need the logger).
54
+ """
55
+
56
+ def __init__(self, options: FFTOptions):
57
+ self.fft_type = options.fft_type
58
+ self.redistribute = options.redistribute
59
+ self.blocking = options.blocking
60
+
61
+ fft_type: Literal["C2C", "C2R", "R2C"] | None
62
+ redistribute: bool
63
+ blocking: Literal[True, "auto"]
64
+
65
+ shape: list[int] # operand shape
66
+ is_C: bool # Is C memory layout
67
+ operand_dtype: str # str because TensorHolder.dtype returns str
68
+ package: Literal["numpy", "cupy", "torch"] # operand package
69
+ memory_space: Literal["cuda", "cpu"] # operand memory space
70
+ distribution: Slab | Sequence[Box] # distribution of FFT input/output operands
71
+ rank: int
72
+ options: Options # FFT options
73
+
74
+ # Global number of elements in the operand (calculated as part of the reduction).
75
+ # NOTE: Only computed and used with box distribution.
76
+ global_size: int = 0
77
+ # Max number of elements of the input operand across processes.
78
+ # NOTE: Only computed and used with box distribution.
79
+ input_max_elements: int = 0
80
+ # Max number of elements of the output operand across processes.
81
+ # NOTE: Only computed and used with box distribution.
82
+ output_max_elements: int = 0
83
+ # is_leaf=True means that this is the _ProblemSpec of a process before reducing
84
+ # with that of another process.
85
+ is_leaf: bool = True
86
+
87
+
88
+ SHARED_FFT_DOCUMENTATION = utils.COMMON_SHARED_DOC_MAP.copy()
89
+ SHARED_FFT_DOCUMENTATION.update(
90
+ {
91
+ "operand": SHARED_FFT_DOCUMENTATION["operand"],
92
+ #
93
+ "operand_admonitions": """
94
+ .. important::
95
+ GPU operands must be on the symmetric heap (for example, allocated with
96
+ ``nvmath.distributed.allocate_symmetric_memory()``).
97
+ """,
98
+ #
99
+ "options": """\
100
+ Specify options for the FFT as a :class:`FFTOptions` object. Alternatively, a `dict` containing the parameters for the
101
+ ``FFTOptions`` constructor can also be provided. If not specified, the value will be set to the default-constructed
102
+ ``FFTOptions`` object.""".replace("\n", " "),
103
+ #
104
+ "distribution": """\
105
+ Specifies the distribution of input and output operands across processes, which can be: (i) according to
106
+ a Slab distribution (see :class:`nvmath.distributed.distribution.Slab`), or (ii) a custom box distribution
107
+ (see :class:`nvmath.distributed.distribution.Box`). With Slab distribution, this indicates the distribution
108
+ of the input operand (the output operand will use the complementary Slab distribution).
109
+ With box distribution, this indicates the input and output boxes.""".replace("\n", " "),
110
+ #
111
+ "direction": """\
112
+ Specify whether forward or inverse FFT is performed (:class:`FFTDirection` object, or as a string from ['forward',
113
+ 'inverse'], "or as an int from [-1, 1] denoting forward and inverse directions respectively).""".replace("\n", " "),
114
+ #
115
+ "sync_symmetric_memory": """\
116
+ Indicates whether to issue a symmetric memory synchronization operation on the execute stream
117
+ before the FFT. Note that before the FFT starts executing, it is required that the input operand
118
+ be ready on all processes. A symmetric memory synchronization ensures completion and visibility
119
+ by all processes of previously issued local stores to symmetric memory. Advanced users who choose
120
+ to manage the synchronization on their own using the appropriate NVSHMEM API, or who know that
121
+ GPUs are already synchronized on the source operand, can set this to False.""".replace("\n", " "),
122
+ #
123
+ "function_signature": """\
124
+ operand,
125
+ /,
126
+ *,
127
+ distribution: Distribution | Sequence[Box],
128
+ sync_symmetric_memory: bool = True,
129
+ options: FFTOptions | dict[str, Any] | None = None,
130
+ stream: AnyStream | None = None
131
+ """.replace("\n", " "),
132
+ #
133
+ "reset_operand_unchecked": utils._reset_operand_unchecked_docstring(
134
+ False, version_added="0.9.0", validation_examples="package match, data type match, distribution validation"
135
+ ),
136
+ }
137
+ )
138
+
139
+
140
+ def _calculate_slab_shape_strides(global_extents, partition_dim, rank, nranks, global_extents_padded=None):
141
+ """Calculate the local slab shape and strides for the given rank, given the global shape
142
+ and partition dimension. If `global_extents_padded` is provided, calculate the strides
143
+ based on this shape.
144
+ """
145
+ n = nranks
146
+ S = global_extents[partition_dim]
147
+ partition_dim_local_size = (S // n + 1) if rank < S % n else S // n
148
+ slab_shape = list(global_extents)
149
+ slab_shape[partition_dim] = partition_dim_local_size
150
+ if global_extents_padded is not None:
151
+ _, strides = _calculate_slab_shape_strides(global_extents_padded, partition_dim, rank, nranks)
152
+ else:
153
+ strides = calculate_strides(slab_shape, reversed(range(len(global_extents))))
154
+ return tuple(slab_shape), strides
155
+
156
+
157
+ def _calculate_local_box(global_shape, partition_dim, rank, nranks):
158
+ """Given a global shape of data that is partitioned across ranks along the
159
+ `partition_dim` dimension according to cuFFTMp's slab distribution, return
160
+ the local box of this rank (as lower and upper coordinates in the global shape).
161
+ """
162
+ lower = [0] * len(global_shape)
163
+ for i in range(rank):
164
+ shape, _ = _calculate_slab_shape_strides(global_shape, partition_dim, i, nranks)
165
+ lower[partition_dim] += shape[partition_dim]
166
+ shape, _ = _calculate_slab_shape_strides(global_shape, partition_dim, rank, nranks)
167
+ upper = list(shape)
168
+ upper[partition_dim] += lower[partition_dim]
169
+ return lower, upper
170
+
171
+
172
+ def _get_fft_concrete_type(dtype, fft_abstract_type):
173
+ FFTType = cufft.Type
174
+ if fft_abstract_type == "C2C":
175
+ if dtype == "complex64":
176
+ return FFTType["C2C"]
177
+ elif dtype == "complex128":
178
+ return FFTType["Z2Z"]
179
+ else:
180
+ raise ValueError(f"Incompatible dtype '{dtype}' for complex-to-complex transform.")
181
+ elif fft_abstract_type == "R2C":
182
+ if dtype == "float32":
183
+ return FFTType["R2C"]
184
+ elif dtype == "float64":
185
+ return FFTType["D2Z"]
186
+ else:
187
+ raise ValueError(f"Incompatible dtype '{dtype}' for real-to-complex transform.")
188
+ elif fft_abstract_type == "C2R":
189
+ if dtype == "complex64":
190
+ return FFTType["C2R"]
191
+ elif dtype == "complex128":
192
+ return FFTType["Z2D"]
193
+ else:
194
+ raise ValueError(f"Incompatible dtype '{dtype}' for complex-to-real transform.")
195
+ else:
196
+ raise ValueError(f"Unsupported FFT Type: '{fft_abstract_type}'")
197
+
198
+
199
+ def _get_validate_direction(direction, fft_abstract_type):
200
+ if isinstance(direction, str) and (d := direction.upper()) in ["FORWARD", "INVERSE"]:
201
+ direction = FFTDirection[d]
202
+ else:
203
+ direction = FFTDirection(direction)
204
+
205
+ if fft_abstract_type == "C2R":
206
+ if direction != FFTDirection.INVERSE:
207
+ raise ValueError(
208
+ f"The specified direction {direction.name} is not compatible with the FFT type '{fft_abstract_type}'."
209
+ )
210
+ elif fft_abstract_type == "R2C": # noqa: SIM102
211
+ if direction != FFTDirection.FORWARD:
212
+ raise ValueError(
213
+ f"The specified direction {direction.name} is not compatible with the FFT type '{fft_abstract_type}'."
214
+ )
215
+ return direction
216
+
217
+
218
+ def complex_to_real_equivalent(name):
219
+ assert "complex" in name, f"Internal Error ({name=})"
220
+ m = name.split("complex")
221
+ assert len(m) in (1, 2)
222
+ size = int(m[-1]) // 2
223
+ if len(m) == 1:
224
+ return f"float{size}"
225
+ else:
226
+ return f"{m[0]}float{size}"
227
+
228
+
229
+ def real_to_complex_equivalent(name):
230
+ assert "float" in name, f"Internal Error ({name=})"
231
+ m = name.split("float")
232
+ assert len(m) in (1, 2)
233
+ size = int(m[-1])
234
+ if len(m) == 1:
235
+ return f"complex{size * 2}"
236
+ else:
237
+ return f"{m[0]}complex{size * 2}"
238
+
239
+
240
+ def _get_default_fft_abstract_type(dtype, fft_type) -> Literal["R2C", "C2R", "C2C"]:
241
+ if fft_type is not None:
242
+ return fft_type
243
+
244
+ f, c = "float", "complex"
245
+ if dtype[: len(f)] == f:
246
+ fft_type = "R2C"
247
+ elif dtype[: len(c)] == c:
248
+ fft_type = "C2C"
249
+ else:
250
+ raise ValueError(f"Unsupported dtype '{dtype}' for FFT.")
251
+ return fft_type
252
+
253
+
254
+ def _get_fft_result_and_compute_types(dtype, fft_abstract_type):
255
+ """
256
+ Return result and compute data type given the input data type and the FFT type.
257
+ """
258
+ if fft_abstract_type == "C2C":
259
+ return dtype, dtype
260
+ elif fft_abstract_type == "C2R":
261
+ return complex_to_real_equivalent(dtype), dtype
262
+ elif fft_abstract_type == "R2C":
263
+ return real_to_complex_equivalent(dtype), dtype
264
+ else:
265
+ raise ValueError(f"Unsupported FFT Type: '{fft_abstract_type}'")
266
+
267
+
268
+ def _get_fft_default_direction(fft_abstract_type) -> FFTDirection:
269
+ """
270
+ Return the default FFT direction (as object of type configuration.FFTDirection) based on
271
+ the FFT type.
272
+ """
273
+ if fft_abstract_type in ["C2C", "R2C"]:
274
+ return FFTDirection.FORWARD
275
+ elif fft_abstract_type == "C2R":
276
+ return FFTDirection.INVERSE
277
+ else:
278
+ raise ValueError(f"Unsupported FFT Type: '{fft_abstract_type}'")
279
+
280
+
281
+ def check_inplace_overlapping_layout(operand: utils.TensorHolder):
282
+ if is_overlapping_layout(operand.shape, operand.strides):
283
+ raise ValueError(
284
+ f"In-place transform is not supported because the tensor with shape "
285
+ f"{operand.shape} and strides {operand.strides} overlaps in memory."
286
+ )
287
+
288
+
289
+ def calculate_strides(shape, axis_order):
290
+ """
291
+ Calculate the strides for the provided shape and axis order.
292
+ """
293
+ strides = [None] * len(shape)
294
+
295
+ stride = 1
296
+ for axis in axis_order:
297
+ strides[axis] = stride
298
+ stride *= shape[axis]
299
+
300
+ return tuple(strides)
301
+
302
+
303
+ def _allocate_with_padded_buffer(
304
+ shape: Sequence[int],
305
+ capacity: int,
306
+ input_dtype,
307
+ memory_space: Literal["cpu", "cuda"],
308
+ package: ModuleType,
309
+ ):
310
+ """Allocate distributed tensor with memory for `capacity` elements of `input_dtype`
311
+ dtype on each rank, and return a view of shape `shape` of the first prod(shape)
312
+ elements in the 1D array.
313
+
314
+ Args:
315
+ shape: Shape of the view returned. Note that this view will have a base tensor with
316
+ possibly larger capacity than required for this shape. The shape can vary across
317
+ ranks.
318
+
319
+ capacity: capacity of the allocated buffer in number of elements of the specified
320
+ dtype. **NOTE: the capacity must be the same on every rank and this is not
321
+ checked. Non-uniform capacity across ranks can lead to undefined behavior**.
322
+
323
+ input_dtype: dtype of the tensor elements.
324
+ """
325
+ size = math.prod(shape)
326
+ assert size <= capacity, f"Internal error: requested shape {shape} exceeds specified capacity {capacity}"
327
+ if memory_space == "cuda":
328
+ if package is ndbuffer:
329
+ # Mimics allocate_symmetric_memory, just with NDBuffer which is not
330
+ # included there, as allocate_symmetric_memory is user-facing
331
+ # and NDBuffer is internal.
332
+ ctx = nvmath.distributed.get_context()
333
+ assert ctx is not None
334
+ a = CudaDistributedTensor.empty(
335
+ capacity,
336
+ dtype=input_dtype,
337
+ device_id=ctx.device_id,
338
+ symmetric_memory="nvshmem",
339
+ make_symmetric=False,
340
+ skip_symmetric_check=True,
341
+ ).tensor
342
+ else:
343
+ a = nvmath.distributed.allocate_symmetric_memory((capacity,), package, dtype=input_dtype, skip_symmetric_check=True)
344
+ else:
345
+ a = package.empty((capacity,), dtype=input_dtype)
346
+ return tensor_wrapper.wrap_operand(a[:size]).reshape(shape, copy=False)
347
+
348
+
349
+ def _calculate_capacity(
350
+ problem_spec: _ProblemSpec,
351
+ global_shape: Sequence[int],
352
+ fft_type: Literal["C2C", "C2R", "R2C"],
353
+ nranks: int,
354
+ ) -> int:
355
+ """Calculate the max number of elements that the input buffer on every rank must be able
356
+ to hold in order to perform the specified distributed FFT. Since the memory allocation
357
+ is on the symmetric heap, we need to use the same (max) capacity on every rank. Also
358
+ recall that the transform is inplace, so the buffer must be able to hold both the input
359
+ and output given the FFT type and input/output operand distribution."""
360
+
361
+ distribution = problem_spec.distribution
362
+ if fft_type == "C2C":
363
+ if isinstance(distribution, Slab):
364
+ # capacity is max of X-slab and Y-slab size on rank 0.
365
+ s1, _ = _calculate_slab_shape_strides(global_shape, 0, 0, nranks) # X-slab
366
+ s2, _ = _calculate_slab_shape_strides(global_shape, 1, 0, nranks) # Y-slab
367
+ return max(math.prod(s1), math.prod(s2))
368
+ else:
369
+ # capacity is the max number of elements across ranks for both input and output.
370
+ return max(problem_spec.input_max_elements, problem_spec.output_max_elements)
371
+ elif fft_type == "R2C":
372
+ if isinstance(distribution, Slab):
373
+ # capacity is max of X-slab and Y-slab size on rank 0 for complex shape.
374
+ global_output_shape = list(global_shape)
375
+ global_output_shape[-1] = global_output_shape[-1] // 2 + 1 # this is the complex shape
376
+
377
+ s1, _ = _calculate_slab_shape_strides(global_output_shape, 0, 0, nranks) # X-slab
378
+ s2, _ = _calculate_slab_shape_strides(global_output_shape, 1, 0, nranks) # Y-slab
379
+
380
+ # Capacity is returned in terms of input (real) elements.
381
+ return max(math.prod(s1) * 2, math.prod(s2) * 2)
382
+ else:
383
+ # Capacity is returned in terms of input (real) elements.
384
+ return max(problem_spec.input_max_elements, 2 * problem_spec.output_max_elements)
385
+ elif fft_type == "C2R":
386
+ if isinstance(distribution, Slab):
387
+ # capacity is max of X-slab and Y-slab size on rank 0.
388
+ s1, _ = _calculate_slab_shape_strides(global_shape, 0, 0, nranks) # X-slab
389
+ s2, _ = _calculate_slab_shape_strides(global_shape, 1, 0, nranks) # Y-slab
390
+ return max(math.prod(s1), math.prod(s2))
391
+ else:
392
+ # Capacity is returned in terms of input (complex) elements.
393
+ return max(
394
+ problem_spec.input_max_elements, problem_spec.output_max_elements // 2 + problem_spec.output_max_elements % 2
395
+ )
396
+ raise AssertionError(f"Internal error: Unknown FFT type {fft_type}")
397
+
398
+
399
+ def _allocate_for_fft(
400
+ global_input_shape: Sequence[int],
401
+ shape: Sequence[int],
402
+ distribution: Slab | Sequence[Box],
403
+ input_dtype,
404
+ memory_space: Literal["cpu", "cuda"],
405
+ package: ModuleType,
406
+ fft_type: Literal["C2C", "C2R", "R2C"],
407
+ capacity: int,
408
+ rank: int,
409
+ nranks: int,
410
+ ):
411
+ """Allocate distributed tensor for the given distributed FFT operation. The same
412
+ capacity must be provided on every rank, and must be large enough for the specified
413
+ transform."""
414
+ if fft_type == "R2C" and isinstance(distribution, Slab):
415
+ partition_dim = distribution.partition_dim
416
+
417
+ # For input, the strides depend on the padding.
418
+ global_output_shape = list(global_input_shape)
419
+ global_output_shape[-1] = global_output_shape[-1] // 2 + 1 # this is the complex shape
420
+ global_input_shape_padded = list(global_output_shape)
421
+ global_input_shape_padded[-1] *= 2
422
+
423
+ padded_shape, _ = _calculate_slab_shape_strides(global_input_shape_padded, partition_dim, rank, nranks)
424
+ a = _allocate_with_padded_buffer(padded_shape, capacity, input_dtype, memory_space, package)
425
+
426
+ # Return a view strided on the last axis.
427
+ return tensor_wrapper.wrap_operand(a.tensor[..., : shape[-1]])
428
+ else:
429
+ # These might not be the most efficient input strides for the R2C FFT (the whole
430
+ # input is packed at the beginning of the buffer with no strides), but to support
431
+ # other strides we probably need the user to pass them.
432
+ return _allocate_with_padded_buffer(shape, capacity, input_dtype, memory_space, package)
433
+
434
+
435
+ _SUPPORTED_PACKAGES = ("numpy", "cupy", "torch")
436
+
437
+
438
+ @utils.docstring_decorator(SHARED_FFT_DOCUMENTATION, skip_missing=True)
439
+ def allocate_operand(
440
+ shape: Sequence[int],
441
+ package: ModuleType,
442
+ *,
443
+ input_dtype=None,
444
+ distribution: Distribution | Sequence[Box],
445
+ memory_space: Literal["cpu", "cuda"] | None = None,
446
+ fft_type: Literal["C2C", "C2R", "R2C"] | None = None,
447
+ logger: logging.Logger | None = None,
448
+ ):
449
+ """Return uninitialized operand of the given shape and type, to use as input for
450
+ distributed FFT. The resulting tensor is backed by a buffer large enough for the
451
+ specified FFT (the buffer can hold both the input and output -distributed FFT is
452
+ inplace-, accounting for both the input and output distribution).
453
+ For CUDA memory space, the tensor is allocated on the symmetric heap, on the
454
+ device on which nvmath.distributed was initialized.
455
+ **This is a collective operation and must be called by all processes**.
456
+
457
+ Args:
458
+ shape: Shape of the tensor to allocate.
459
+
460
+ package: Python package determining the tensor type (e.g. numpy, cupy, torch).
461
+
462
+ input_dtype: Tensor dtype in a form recognized by the package. If None, will use
463
+ the package's default dtype.
464
+
465
+ distribution: {distribution}
466
+
467
+ memory_space: The memory space (``'cpu'`` or ``'cuda'``) on which to allocate
468
+ the tensor. If not provided, this is inferred for packages that support
469
+ a single memory space like numpy and cupy. For other packages it must be
470
+ provided.
471
+
472
+ fft_type: The type of FFT to perform. Available options include ``'C2C'``,
473
+ ``'C2R'``, and ``'R2C'``. The default is ``'C2C'`` for complex input and
474
+ ``'R2C'`` for real input.
475
+
476
+ logger (logging.Logger): Python Logger object. The root logger will be used if a
477
+ logger object is not provided.
478
+ """
479
+
480
+ package_name = package.__name__
481
+ if package_name not in _SUPPORTED_PACKAGES:
482
+ raise ValueError(f"The package must be one of {_SUPPORTED_PACKAGES}. Got {package}.")
483
+
484
+ if memory_space is None:
485
+ if package_name == "cupy":
486
+ memory_space = "cuda"
487
+ elif package_name == "numpy":
488
+ memory_space = "cpu"
489
+ else:
490
+ raise ValueError(f"You must provide memory_space for package {package}")
491
+
492
+ if memory_space not in ("cuda", "cpu"):
493
+ raise ValueError(f"memory_space must be 'cuda' or 'cpu'. Got {memory_space}")
494
+
495
+ if (package_name == "cupy" and memory_space == "cpu") or (package_name == "numpy" and memory_space == "cuda"):
496
+ raise ValueError(f"'{memory_space}' memory space is not compatible with package {package_name}")
497
+
498
+ distributed_ctx = nvmath.distributed.get_context()
499
+ if distributed_ctx is None:
500
+ raise RuntimeError(
501
+ "nvmath.distributed has not been initialized. Refer to "
502
+ "https://docs.nvidia.com/cuda/nvmath-python/latest/distributed-apis/runtime.html"
503
+ " for more information."
504
+ )
505
+ if not distributed_ctx.nvshmem_available:
506
+ raise RuntimeError("nvmath.distributed wasn't initialized with NVSHMEM backend")
507
+ process_group = distributed_ctx.process_group
508
+ rank = process_group.rank
509
+ nranks = process_group.nranks
510
+
511
+ if package_name in ("numpy", "cupy"):
512
+ if input_dtype is None:
513
+ # This mimics numpy and cupy
514
+ input_dtype = np.float64
515
+
516
+ input_dtype_name = np.dtype(input_dtype).name
517
+ elif package_name == "torch":
518
+ if input_dtype is None:
519
+ import torch
520
+
521
+ input_dtype = torch.get_default_dtype()
522
+
523
+ input_dtype_name = str(input_dtype).split(".")[-1]
524
+
525
+ package_name = cast(Literal["numpy", "cupy", "torch"], package_name)
526
+
527
+ if isinstance(distribution, Distribution):
528
+ distribution = distribution.to(Slab, ndim=len(shape), copy=True)
529
+ distribution = cast(Slab, distribution)
530
+ else:
531
+ # Must be a Box pair (this is checked in the ProblemSpec reducer).
532
+ distribution = tuple(cast(Box, box.copy()) for box in distribution)
533
+
534
+ options = FFTOptions(fft_type=fft_type)
535
+ problem_spec = _ProblemSpec(
536
+ distribution=distribution,
537
+ shape=list(shape),
538
+ is_C=True,
539
+ operand_dtype=input_dtype_name,
540
+ options=_ProblemSpec.Options(options),
541
+ package=package_name,
542
+ memory_space=memory_space,
543
+ global_size=math.prod(shape),
544
+ rank=rank,
545
+ )
546
+ if nranks > 1:
547
+ problem_spec = process_group.allreduce_object(problem_spec, op=_problem_spec_reducer)
548
+ else:
549
+ # This ensures error-checking with one rank.
550
+ problem_spec = _problem_spec_reducer(problem_spec, problem_spec)
551
+ if isinstance(problem_spec, Exception):
552
+ # There is an error or inconsistency in the problem spec across processes.
553
+ # Note that since this comes from an allreduce, all processes will have
554
+ # received the same exception.
555
+ raise problem_spec
556
+
557
+ fft_type = _get_default_fft_abstract_type(input_dtype_name, fft_type)
558
+ if (fft_type == "R2C" and "float" not in input_dtype_name) or (
559
+ fft_type in ("C2C", "C2R") and "complex" not in input_dtype_name
560
+ ):
561
+ raise ValueError(f"input dtype {input_dtype_name} is not compatible with FFT type {fft_type}")
562
+
563
+ logger = logger if logger is not None else logging.getLogger()
564
+ logger.info(
565
+ f"Allocating {package.__name__} operand with shape {shape} and dtype "
566
+ f"{input_dtype_name} for FFT type {fft_type} on {memory_space}, with "
567
+ f"distribution {distribution}."
568
+ )
569
+
570
+ # Infer global shape.
571
+ if isinstance(distribution, Slab):
572
+ global_shape = cast(Slab, problem_spec.distribution)._global_shape_reduction_epilogue()
573
+ else:
574
+ global_shape = cast(Sequence[Box], problem_spec.distribution)[0]._global_shape_reduction_epilogue()
575
+
576
+ # Calculate max capacity for this transform.
577
+ capacity = _calculate_capacity(problem_spec, global_shape, fft_type, nranks)
578
+
579
+ return _allocate_for_fft(
580
+ global_shape, shape, distribution, input_dtype, memory_space, package, fft_type, capacity, rank, nranks
581
+ ).tensor
582
+
583
+
584
+ def _get_view(
585
+ array,
586
+ desired_shape: Sequence[int],
587
+ desired_dtype: str,
588
+ process_group,
589
+ collective_error_checking: bool,
590
+ ):
591
+ """Returns view of the array of the desired shape and dtype. If the given array doesn't
592
+ have the same dtype and number of elements, tries to return a view from the base array
593
+ (original array that owns the memory), where elements are taken from contiguous memory
594
+ starting from the beginning of the buffer."""
595
+ error = None
596
+ desired_size = math.prod(desired_shape) # number of elements
597
+ rank = process_group.rank
598
+ try:
599
+ if array.dtype == desired_dtype and array.size == desired_size:
600
+ if tuple(array.shape) != tuple(desired_shape):
601
+ result = array.reshape(desired_shape, copy=False)
602
+ else:
603
+ result = array
604
+ else:
605
+
606
+ def error_msg(base):
607
+ return (
608
+ f"[{rank}] Internal error: tensor doesn't have a base array large enough "
609
+ "for the required shape and dtype: base array shape and dtype is "
610
+ f"({base.shape}, {base.dtype}), desired shape and dtype is "
611
+ f"({desired_shape}, {desired_dtype}). Consider allocating the operand "
612
+ "for this FFT with nvmath.distributed.fft.allocate_operand()"
613
+ )
614
+
615
+ try:
616
+ base = array.tensor.base
617
+ except AttributeError:
618
+ base = array.tensor._base
619
+
620
+ if base is None:
621
+ base = array.tensor
622
+
623
+ dtype = array.name_to_dtype[desired_dtype]
624
+ itemsize = NAME_TO_ITEM_SIZE[desired_dtype]
625
+ nbytes_required = desired_size * itemsize
626
+
627
+ if array.name == "cuda":
628
+ layout = base.layout
629
+ assert layout.is_contiguous_c
630
+ if layout.memory_range_size_in_bytes < nbytes_required:
631
+ raise RuntimeError(error_msg(base))
632
+ elif base.nbytes < nbytes_required: # type: ignore
633
+ # Note: if this error occurs, it can easily happen on one process
634
+ # but not others.
635
+ raise RuntimeError(error_msg(base))
636
+
637
+ if len(base.shape) > 1:
638
+ # Flatten the base array.
639
+ base = base.reshape(-1) # type: ignore
640
+
641
+ v = base.view(dtype)[:desired_size] # type: ignore
642
+ result = array.__class__(v).reshape(desired_shape, copy=False)
643
+ except Exception as e:
644
+ error = e
645
+
646
+ if collective_error_checking:
647
+ error = process_group.allreduce_object(error, op=_reduce_exception)
648
+ if error:
649
+ raise error
650
+
651
+ return result
652
+
653
+
654
+ def _alloc_and_copy_to_exespace_mirror(
655
+ user_operand: DistributedTensor,
656
+ stream_holder: StreamHolder,
657
+ fft_abstract_type: Literal["C2C", "C2R", "R2C"],
658
+ global_shape: Sequence[int],
659
+ distribution: Slab | Sequence[Box],
660
+ capacity: int,
661
+ rank: int,
662
+ nranks: int,
663
+ ) -> DistributedTensor:
664
+ """
665
+ Allocate a CUDA symmetric-memory buffer, copy the CPU operand into it,
666
+ and return a reference to the new device-side :class:`DistributedTensor`.
667
+ """
668
+ if user_operand.name == "numpy":
669
+ package: ModuleType = ndbuffer
670
+ dtype = user_operand.dtype
671
+ else:
672
+ import torch as package
673
+
674
+ dtype = user_operand.tensor.dtype
675
+
676
+ exespace_mirror_operand = _allocate_for_fft(
677
+ global_shape,
678
+ user_operand.shape,
679
+ distribution,
680
+ dtype,
681
+ "cuda",
682
+ package,
683
+ fft_abstract_type,
684
+ capacity,
685
+ rank,
686
+ nranks,
687
+ )
688
+ exespace_mirror_operand.copy_(user_operand, stream_holder)
689
+ return exespace_mirror_operand
690
+
691
+
692
+ def _alloc_and_copy_to_exespace_mirror_or_identity(
693
+ user_operand: DistributedTensor,
694
+ stream_holder: StreamHolder,
695
+ execution_space: Literal["cuda", "cpu"],
696
+ memory_space: Literal["cuda", "cpu"],
697
+ device_id: int | Literal["cpu"],
698
+ fft_abstract_type: Literal["C2C", "C2R", "R2C"],
699
+ global_shape: Sequence[int],
700
+ distribution: Slab | Sequence[Box],
701
+ capacity: int,
702
+ rank: int,
703
+ nranks: int,
704
+ ) -> tuple[DistributedTensor, DistributedTensor | None]:
705
+ """
706
+ Allocate the exespace mirror for the given execution/memory space, or
707
+ return the user operand as-is (identity) when no mirror is needed.
708
+
709
+ Used during construction and after release_operand() when the exespace
710
+ mirror needs to be created from scratch.
711
+
712
+ Returns ``(operand, operand_backup)`` where:
713
+ - Same-space: ``operand`` is the user operand itself, ``operand_backup``
714
+ is ``None`` (no mirror needed).
715
+ - Cross-space: ``operand`` is the newly allocated device-side mirror,
716
+ ``operand_backup`` is the original user operand (for result copy-back).
717
+ """
718
+ if execution_space == memory_space:
719
+ return user_operand, None
720
+ assert execution_space == "cuda"
721
+ exespace_mirror_operand = _alloc_and_copy_to_exespace_mirror(
722
+ user_operand, stream_holder, fft_abstract_type, global_shape, distribution, capacity, rank, nranks
723
+ )
724
+ # Sanity-check: the allocation above is done on the device specified by the
725
+ # distributed context, which must match the device the FFT object expects.
726
+ assert exespace_mirror_operand.device_id == device_id
727
+ return exespace_mirror_operand, user_operand
728
+
729
+
730
+ def _copy_to_exespace_mirror_or_identity(
731
+ exespace_mirror_operand: DistributedTensor,
732
+ new_operand: DistributedTensor,
733
+ stream_holder: StreamHolder,
734
+ execution_space: Literal["cuda", "cpu"],
735
+ memory_space: Literal["cuda", "cpu"],
736
+ ) -> tuple[DistributedTensor, DistributedTensor | None]:
737
+ """
738
+ Copy new operand data into an existing exespace mirror, or return the
739
+ new operand as-is (identity) when no mirror is needed.
740
+
741
+ Used by reset_operand() when the operand has NOT been released, so the
742
+ exespace mirror is still valid and can be reused via in-place copy.
743
+
744
+ Returns ``(operand, operand_backup)`` where:
745
+ - Same-space: ``operand`` is the new user operand, ``operand_backup``
746
+ is ``None`` (no mirror needed).
747
+ - Cross-space: ``operand`` is the existing mirror (updated in-place),
748
+ ``operand_backup`` is the new user operand (for result copy-back).
749
+ """
750
+ if execution_space == memory_space:
751
+ return new_operand, None
752
+ else:
753
+ exespace_mirror_operand.copy_(new_operand, stream_holder=stream_holder)
754
+ return exespace_mirror_operand, new_operand
755
+
756
+
757
+ def _problem_spec_reducer(p1: _ProblemSpec, p2: _ProblemSpec):
758
+ try:
759
+ if isinstance(p1, Exception):
760
+ return p1 # propagate exception
761
+
762
+ if isinstance(p2, Exception):
763
+ return p2 # propagate exception
764
+
765
+ if len(p1.shape) != len(p2.shape):
766
+ return ValueError("The number of dimensions of the input operand is inconsistent across processes")
767
+
768
+ # Check if rank is 2-D or 3-D.
769
+ if len(p1.shape) not in (2, 3):
770
+ return ValueError(
771
+ "Distributed FFT is currently supported only for 2-D and 3-D tensors."
772
+ f" The number of dimensions of the operand is {len(p1.shape)}."
773
+ )
774
+
775
+ if p1.operand_dtype != p2.operand_dtype:
776
+ return ValueError("The operand dtype is inconsistent across processes")
777
+
778
+ if p1.package != p2.package:
779
+ return ValueError("operand doesn't belong to the same package on all processes")
780
+
781
+ if p1.memory_space != p2.memory_space:
782
+ return ValueError('operand is not on the same memory space ("cpu", "cuda") on all processes')
783
+
784
+ if p1.options != p2.options:
785
+ return ValueError(f"options are inconsistent across processes: {p1.options} != {p2.options}")
786
+
787
+ # Determine the memory layout shared by all processes.
788
+ p1.is_C &= p2.is_C
789
+ if not p1.is_C:
790
+ return ValueError("The input memory layout is not C on every process")
791
+
792
+ is_box_1 = not isinstance(p1.distribution, Slab)
793
+ is_box_2 = not isinstance(p2.distribution, Slab)
794
+ if is_box_1 != is_box_2:
795
+ return ValueError("distribution must be either Slab or box on all processes, not a mix of both")
796
+
797
+ fft_abstract_type = _get_default_fft_abstract_type(p1.operand_dtype, p1.options.fft_type)
798
+
799
+ if len(p1.shape) == 2 and not is_box_1:
800
+ if fft_abstract_type == "R2C" and p1.distribution != Slab.X:
801
+ return ValueError("2D FFT R2C only supports X-slab input")
802
+ elif fft_abstract_type == "C2R" and p1.distribution != Slab.Y:
803
+ return ValueError("2D FFT C2R only supports Y-slab input")
804
+
805
+ for p_spec in (p1, p2):
806
+ if p_spec.is_leaf:
807
+ if not is_box_1:
808
+ # Using cuFFTMp slab distribution.
809
+ slab = cast(Slab, p_spec.distribution)
810
+ if slab.partition_dim not in (0, 1):
811
+ return ValueError("The Slab partition dimension must be X or Y")
812
+
813
+ slab._global_shape_reduction_prologue(p_spec.rank, p_spec.shape)
814
+ else:
815
+ # Custom distribution given by input and output boxes on each process.
816
+ if not isinstance(p_spec.distribution, Sequence) or not all(
817
+ isinstance(d, Box) for d in p_spec.distribution
818
+ ):
819
+ return ValueError("distribution must be a Slab or a Box pair")
820
+
821
+ if len(p_spec.distribution) != 2: # type: ignore
822
+ return ValueError("Must provide a Box pair on every process")
823
+
824
+ input_box, output_box = cast(Sequence[Box], p_spec.distribution)
825
+
826
+ p_spec.input_max_elements = math.prod(p_spec.shape)
827
+ input_box._global_shape_reduction_prologue(p_spec.rank, p_spec.shape)
828
+
829
+ output_box_shape = output_box.box_shape
830
+ p_spec.output_max_elements = math.prod(output_box_shape)
831
+ output_box._global_shape_reduction_prologue(p_spec.rank, output_box_shape)
832
+
833
+ if not is_box_1:
834
+ cast(Slab, p1.distribution)._global_shape_reduction(cast(Slab, p2.distribution))
835
+ else:
836
+ # Reduce input boxes.
837
+ p1_boxes = cast(Sequence[Box], p1.distribution)
838
+ p2_boxes = cast(Sequence[Box], p2.distribution)
839
+ p1_boxes[0]._global_shape_reduction(p2_boxes[0])
840
+ # Reduce output boxes.
841
+ p1_boxes[1]._global_shape_reduction(p2_boxes[1])
842
+
843
+ if p1 is not p2: # with nranks=1 p1 is p2
844
+ p1.global_size += p2.global_size
845
+
846
+ p1.input_max_elements = max(p1.input_max_elements, p2.input_max_elements)
847
+ p1.output_max_elements = max(p1.output_max_elements, p2.output_max_elements)
848
+
849
+ except Exception as e:
850
+ return e
851
+ p1.is_leaf = False
852
+ return p1
853
+
854
+
855
+ def _reduce_exception(e1, e2):
856
+ if e1 is not None:
857
+ return e1
858
+ return e2
859
+
860
+
861
+ class InvalidFFTState(Exception):
862
+ pass
863
+
864
+
865
+ @utils.docstring_decorator(SHARED_FFT_DOCUMENTATION, skip_missing=False)
866
+ class FFT:
867
+ """
868
+ Create a stateful object that encapsulates the specified distributed FFT computations
869
+ and required resources. This object ensures the validity of resources during use and
870
+ releases them when they are no longer needed to prevent misuse.
871
+
872
+ This object encompasses all functionalities of function-form APIs :func:`fft`,
873
+ :func:`ifft`, :func:`rfft`, and :func:`irfft`, which are convenience wrappers around it.
874
+ The stateful object also allows for the amortization of preparatory costs when the same
875
+ FFT operation is to be performed on multiple operands with the same problem
876
+ specification (see :meth:`reset_operand` for more details).
877
+
878
+ Using the stateful object typically involves the following steps:
879
+
880
+ 1. **Problem Specification**: Initialize the object with a defined operation and
881
+ options.
882
+ 2. **Preparation**: Use :meth:`plan` to determine the best algorithmic implementation
883
+ for this specific FFT operation.
884
+ 3. **Execution**: Perform the FFT computation with :meth:`execute`, which can be either
885
+ forward or inverse FFT transformation.
886
+ 4. **Resource Management**: Ensure all resources are released either by explicitly
887
+ calling :meth:`free` or by managing the stateful object within a context manager.
888
+
889
+ Detailed information on each step described above can be obtained by passing in a
890
+ :class:`logging.Logger` object to :class:`FFTOptions` or by setting the appropriate
891
+ options in the root logger object, which is used by default:
892
+
893
+ >>> import logging
894
+ >>> logging.basicConfig(
895
+ ... level=logging.INFO,
896
+ ... format="%(asctime)s %(levelname)-8s %(message)s",
897
+ ... datefmt="%m-%d %H:%M:%S",
898
+ ... )
899
+
900
+ .. versionchanged:: 0.9.0
901
+ The `operand` parameter is now positional-only.
902
+
903
+ Args:
904
+ operand: {operand}
905
+ {operand_admonitions}
906
+
907
+ distribution: {distribution}
908
+
909
+ options: {options}
910
+
911
+ stream: {stream}
912
+
913
+ .. seealso::
914
+ :meth:`plan`, :meth:`reset_operand`, :meth:`execute`,
915
+ :meth:`release_operand`
916
+
917
+ Examples:
918
+
919
+ >>> import cupy as cp
920
+ >>> import nvmath.distributed
921
+
922
+ Get process group used to initialize nvmath.distributed (for information on
923
+ initializing nvmath.distributed, you can refer to the documentation or to the
924
+ FFT examples in `nvmath/examples/distributed/fft
925
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/fft>`_):
926
+
927
+ >>> process_group = nvmath.distributed.get_context().process_group
928
+
929
+ Get the number of processes:
930
+
931
+ >>> nranks = process_group.nranks
932
+
933
+ Create a 3-D complex128 ndarray on GPU symmetric memory, distributed according to
934
+ the Slab distribution on the X axis (the global shape is (128, 128, 128)):
935
+
936
+ >>> from nvmath.distributed.distribution import Slab
937
+ >>> shape = 128 // nranks, 128, 128
938
+
939
+ cuFFTMp uses the NVSHMEM PGAS model for distributed computation, which requires GPU
940
+ operands to be on the symmetric heap:
941
+
942
+ >>> a = nvmath.distributed.allocate_symmetric_memory(shape, cp, dtype=cp.complex128)
943
+
944
+ After allocating, we initialize the CuPy ndarray's memory:
945
+
946
+ >>> a[:] = cp.random.rand(*shape) + 1j * cp.random.rand(*shape)
947
+
948
+ We will define a 3-D C2C FFT operation, creating an FFT object encapsulating the
949
+ above problem specification. Each process provides their own local operand (which
950
+ is part of the PGAS space, but otherwise can be operated on as any other CuPy
951
+ ndarray for local operations) and specifies how the operand is distributed across
952
+ processes:
953
+
954
+ >>> f = nvmath.distributed.fft.FFT(a, distribution=Slab.X)
955
+
956
+ More information on distribution of operands can be found in the documentation:
957
+ https://docs.nvidia.com/cuda/nvmath-python/latest/distributed-apis/fft/index.html
958
+
959
+ Options can be provided above to control the behavior of the operation using the
960
+ `options` argument (see :class:`FFTOptions`).
961
+
962
+ Next, plan the FFT:
963
+
964
+ >>> f.plan()
965
+
966
+ Now execute the FFT, and obtain the result `r1` as a CuPy ndarray. Note that
967
+ distributed FFT computations are inplace, so operands a and r1 share the same
968
+ symmetric memory buffer:
969
+
970
+ >>> r1 = f.execute()
971
+
972
+ Finally, free the FFT object's resources. To avoid this explicit call, it's
973
+ recommended to use the FFT object as a context manager as shown below, if possible.
974
+
975
+ >>> f.free()
976
+
977
+ Any symmetric memory that is owned by the user must be deleted explicitly (this is
978
+ a collective call and must be called by all processes). Note that because operands
979
+ a and r1 share the same buffer, only one of them must be freed:
980
+
981
+ >>> nvmath.distributed.free_symmetric_memory(a)
982
+
983
+ Note that all :class:`FFT` methods execute on the current stream by default.
984
+ Alternatively, the `stream` argument can be used to run a method on a specified
985
+ stream.
986
+
987
+ Let's now look at the same problem with NumPy ndarrays on the CPU.
988
+
989
+ Create a 3-D complex128 NumPy ndarray on the CPU:
990
+
991
+ >>> import numpy as np
992
+ >>> shape = 128 // nranks, 128, 128
993
+ >>> a = np.random.rand(*shape) + 1j * np.random.rand(*shape)
994
+
995
+ Create an FFT object encapsulating the problem specification described earlier and
996
+ use it as a context manager.
997
+
998
+ >>> with nvmath.distributed.fft.FFT(a, distribution=Slab.X) as f:
999
+ ... f.plan()
1000
+ ...
1001
+ ... # Execute the FFT to get the first result.
1002
+ ... r1 = f.execute()
1003
+
1004
+ All the resources used by the object are released at the end of the block.
1005
+
1006
+ The operation was performed on the GPU, with the NumPy array temporarily copied to
1007
+ GPU symmetric memory and transformed on the GPU.
1008
+
1009
+ Further examples can be found in the `nvmath/examples/distributed/fft
1010
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/fft>`_
1011
+ directory.
1012
+ """
1013
+
1014
+ def _free_internal_sheap(self, exception: Exception | None = None) -> bool:
1015
+ # This is a fail-safe to free NVSHMEM internal memory in case of invalid
1016
+ # state (FFT constructor fails). Since we might call nvshmem_free here, we're
1017
+ # assuming that all processes equally failed in the ctor, which might not be true,
1018
+ # but if it weren't true they would end up in deadlock most likely anyway.
1019
+ if (
1020
+ hasattr(self, "memory_space")
1021
+ and self.memory_space == "cpu"
1022
+ and self.operand is not None
1023
+ and self.operand.device == "cuda"
1024
+ ):
1025
+ with utils.device_ctx(self.device_id):
1026
+ self.operand.free_symmetric()
1027
+ return True
1028
+
1029
+ @utils.atomic(_free_internal_sheap, method=True)
1030
+ def __init__(
1031
+ self,
1032
+ operand,
1033
+ /,
1034
+ *,
1035
+ distribution: Distribution | Sequence[Box],
1036
+ options: FFTOptions | dict[str, Any] | None = None,
1037
+ stream: AnyStream | None = None,
1038
+ ):
1039
+ distributed_ctx = nvmath.distributed.get_context()
1040
+ if distributed_ctx is None:
1041
+ raise RuntimeError(
1042
+ "nvmath.distributed has not been initialized. Refer to "
1043
+ "https://docs.nvidia.com/cuda/nvmath-python/latest/distributed-apis/runtime.html"
1044
+ " for more information."
1045
+ )
1046
+ if not distributed_ctx.nvshmem_available:
1047
+ raise RuntimeError("nvmath.distributed wasn't initialized with NVSHMEM backend")
1048
+ self.process_group = process_group = distributed_ctx.process_group
1049
+ self.rank = rank = process_group.rank
1050
+ self.nranks = nranks = process_group.nranks
1051
+
1052
+ self.operand = operand = tensor_wrapper.wrap_operand(operand)
1053
+ self.options = options = cast(FFTOptions, utils.check_or_create_options(FFTOptions, options, "Distributed FFT options"))
1054
+ self.package = operand.name
1055
+
1056
+ if isinstance(distribution, Distribution):
1057
+ distribution = distribution.to(Slab, ndim=len(operand.shape), copy=True)
1058
+ distribution = cast(Slab, distribution)
1059
+ else:
1060
+ # Must be a Box pair (this is checked in the ProblemSpec reducer).
1061
+ distribution = tuple(cast(Box, box.copy()) for box in distribution)
1062
+
1063
+ is_C = sorted(operand.strides, reverse=True) == list(operand.strides)
1064
+
1065
+ # Merge the problem specification across processes to make sure that there are no
1066
+ # inconsistencies and to calculate the global shape. Importantly, this also does
1067
+ # collective error checking of the FFT input parameters, to ensure that all
1068
+ # processes fail on error of any one process, thus preventing deadlock.
1069
+ problem_spec = _ProblemSpec(
1070
+ distribution=distribution,
1071
+ shape=list(operand.shape),
1072
+ is_C=is_C,
1073
+ operand_dtype=operand.dtype,
1074
+ options=_ProblemSpec.Options(options),
1075
+ package=self.package,
1076
+ memory_space=operand.device,
1077
+ global_size=math.prod(operand.shape),
1078
+ rank=rank,
1079
+ )
1080
+ if nranks > 1:
1081
+ problem_spec = process_group.allreduce_object(problem_spec, op=_problem_spec_reducer)
1082
+ else:
1083
+ # Ensure we error-check with one rank.
1084
+ problem_spec = _problem_spec_reducer(problem_spec, problem_spec)
1085
+ if isinstance(problem_spec, Exception):
1086
+ # There is an error or inconsistency in the problem spec across processes.
1087
+ # Note that since this comes from an allreduce, all processes will have
1088
+ # received the same exception.
1089
+ raise problem_spec
1090
+
1091
+ self.operand_dim = len(operand.shape)
1092
+
1093
+ self.operand_data_type = operand.dtype
1094
+ self.fft_abstract_type = _get_default_fft_abstract_type(self.operand_data_type, options.fft_type)
1095
+
1096
+ self.result_data_type, self.compute_data_type = _get_fft_result_and_compute_types(operand.dtype, self.fft_abstract_type)
1097
+
1098
+ self.logger = options.logger if options.logger is not None else logging.getLogger()
1099
+ self.logger.info(f"The FFT type is {self.fft_abstract_type}.")
1100
+ self.logger.info(
1101
+ f"The input data type is {self.operand_data_type}, and the result data type is {self.result_data_type}."
1102
+ )
1103
+
1104
+ # cuFFTMp doesn't support complex32.
1105
+ if self.result_data_type == "complex32":
1106
+ raise TypeError(f"The result data type {self.result_data_type} is not supported.")
1107
+
1108
+ # Infer operand package, execution space, and memory space.
1109
+ execution_device_id: int = distributed_ctx.device_id
1110
+ if operand.device_id != "cpu": # exec space matches the mem space
1111
+ self.memory_space = "cuda"
1112
+ self.device_id = operand.device_id
1113
+ assert operand.device_id == execution_device_id
1114
+ else: # we need to move inputs cpu -> gpu and outputs gpu -> cpu
1115
+ self.memory_space = "cpu"
1116
+ self.device_id = execution_device_id
1117
+ self.execution_space = "cuda"
1118
+ self.operand_device_id = operand.device_id
1119
+ self.internal_op_package = self._internal_operand_package(self.package)
1120
+ stream_holder: StreamHolder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
1121
+
1122
+ if self.memory_space == "cuda" and not operand.is_symmetric_memory:
1123
+ raise TypeError("Distributed FFT requires GPU operand to be on symmetric memory")
1124
+
1125
+ self.logger.info(
1126
+ f"The input tensor's memory space is {self.memory_space}, and the execution space "
1127
+ f"is {self.execution_space}, with device {self.device_id}."
1128
+ )
1129
+
1130
+ self.logger.info(f"The specified stream for the FFT ctor is {stream_holder and stream_holder.obj}")
1131
+
1132
+ # Infer the global extents.
1133
+ if isinstance(distribution, Slab):
1134
+ self.global_extents = cast(Slab, problem_spec.distribution)._global_shape_reduction_epilogue()
1135
+ # Check that this process has the correct slab shape.
1136
+ error = None
1137
+ try:
1138
+ distribution._bind(self.global_extents, shape=self.operand.shape)
1139
+ except Exception as e:
1140
+ error = e
1141
+ error = process_group.allreduce_object(error, op=_reduce_exception)
1142
+ if error:
1143
+ raise error
1144
+ else:
1145
+ # Infer the global shape from the global input box.
1146
+ self.global_extents = cast(Sequence[Box], problem_spec.distribution)[0]._global_shape_reduction_epilogue()
1147
+
1148
+ # This can't throw error since the local operand shape was already checked
1149
+ # against the box shape in the ProblemSpec reducer.
1150
+ distribution[0]._bind(self.global_extents, shape=self.operand.shape)
1151
+
1152
+ # The global number of elements must be compatible with the global shape.
1153
+ if problem_spec.global_size != math.prod(self.global_extents):
1154
+ raise ValueError(
1155
+ f"The global number of elements is incompatible with the inferred global shape {self.global_extents}"
1156
+ )
1157
+
1158
+ for i in (0, 1):
1159
+ if self.global_extents[i] < nranks:
1160
+ raise ValueError(
1161
+ f"The FFT dimension {i} has global length {self.global_extents[i]} which "
1162
+ f"is smaller than the number of processes ({nranks})"
1163
+ )
1164
+
1165
+ self.logger.info(f"The global FFT extents are {self.global_extents}.")
1166
+
1167
+ # Calculate the required buffer capacity (in number of elements) for this transform.
1168
+ self.capacity: int = _calculate_capacity(problem_spec, self.global_extents, self.fft_abstract_type, nranks)
1169
+
1170
+ # Copy the operand to execution_space's device if needed.
1171
+ self.operand, self.operand_backup = _alloc_and_copy_to_exespace_mirror_or_identity(
1172
+ operand,
1173
+ stream_holder,
1174
+ self.execution_space, # type: ignore[arg-type]
1175
+ self.memory_space, # type: ignore[arg-type]
1176
+ self.device_id,
1177
+ self.fft_abstract_type,
1178
+ self.global_extents,
1179
+ distribution,
1180
+ self.capacity,
1181
+ rank,
1182
+ nranks,
1183
+ )
1184
+
1185
+ # Track whether the user has called release_operand(). This flag is
1186
+ # checked in _check_valid_operand to prevent execution after the user
1187
+ # has released their operand. It is cleared by reset_operand().
1188
+ self._operand_released = False
1189
+
1190
+ operand = self.operand
1191
+ # Capture operand layout for consistency checks when resetting operands.
1192
+ self.operand_layout = TensorLayout(shape=operand.shape, strides=operand.strides)
1193
+
1194
+ self.logger.info("The FFT will be performed in-place, with the result overwriting the input.")
1195
+
1196
+ # The result's package and device.
1197
+ self.result_class: DistributedTensor = operand.__class__
1198
+
1199
+ # Set blocking or non-blocking behavior.
1200
+ self.blocking = self.options.blocking is True or self.memory_space == "cpu"
1201
+ if self.blocking:
1202
+ self.call_prologue = "This call is blocking and will return only after the operation is complete."
1203
+ else:
1204
+ self.call_prologue = (
1205
+ "This call is non-blocking and will return immediately after the operation is launched on the device."
1206
+ )
1207
+
1208
+ if not isinstance(distribution, Slab):
1209
+ # Redistribute only applies to cuFFTMp's default slab distribution.
1210
+ self.options.redistribute = False
1211
+ self.logger.info("Redistribute option is ignored when using box distribution.")
1212
+
1213
+ # Set memory allocator.
1214
+ self.allocator = NvshmemMemoryManager(self.device_id, self.logger)
1215
+
1216
+ self.distribution: Slab | Sequence[Box] = distribution
1217
+ # Map possible distributions to the corresponding operand TensorLayout.
1218
+ self.distribution_layout: dict[Slab | Box, TensorLayout] = {}
1219
+ # The subformat is an identifier that cuFFTMp uses to refer to an operand
1220
+ # distribution. It can be one of:
1221
+ # - cufftMp.XtSubFormat.FORMAT_INPLACE (refers to Slab.X)
1222
+ # - cufftMp.XtSubFormat.FORMAT_INPLACE_SHUFFLED (refers to Slab.Y)
1223
+ # - cufft.XtSubFormat.FORMAT_DISTRIBUTED_INPUT (the input box at FFT plan time)
1224
+ # - cufft.XtSubFormat.FORMAT_DISTRIBUTED_OUTPUT (the output box at FFT plan time)
1225
+ self.subformat: int = -1
1226
+ if isinstance(distribution, Slab):
1227
+ self.distribution_layout[distribution] = self.operand_layout
1228
+
1229
+ if self.options.redistribute:
1230
+ from_axis, to_axis = ("X", "X") if distribution == Slab.X else ("Y", "Y")
1231
+ else:
1232
+ from_axis, to_axis = ("X", "Y") if distribution == Slab.X else ("Y", "X")
1233
+ self.logger.info(
1234
+ f"The operand distribution is Slab, with input partitioned on {from_axis} axis "
1235
+ f"and output on {to_axis} (redistribute={self.options.redistribute})."
1236
+ )
1237
+ else:
1238
+ input_box, output_box = distribution
1239
+ self.distribution_layout[input_box] = self.operand_layout
1240
+
1241
+ self.logger.info(f"The operand distribution is based on custom input box {input_box} and output box {output_box}.")
1242
+
1243
+ # Infer result shape and strides.
1244
+
1245
+ self.global_result_extents = list(self.global_extents)
1246
+ global_result_extents_padded = None
1247
+ if self.fft_abstract_type == "R2C":
1248
+ self.global_result_extents[-1] = self.global_result_extents[-1] // 2 + 1
1249
+ elif self.fft_abstract_type == "C2R":
1250
+ self.global_result_extents[-1] = (self.global_result_extents[-1] - 1) * 2
1251
+ if options.last_axis_parity == "odd":
1252
+ self.global_result_extents[-1] += 1
1253
+ global_result_extents_padded = list(self.global_result_extents)
1254
+ global_result_extents_padded[-1] = self.global_extents[-1] * 2
1255
+
1256
+ if not isinstance(distribution, Slab):
1257
+ global_boxes = cast(Sequence[Box], problem_spec.distribution)
1258
+ lower, upper = global_boxes[1]
1259
+ actual_global_result_extents = tuple(int(upper[i] - lower[i]) for i in range(self.operand_dim))
1260
+ if actual_global_result_extents != tuple(self.global_result_extents):
1261
+ raise ValueError(
1262
+ "The global box derived from the output boxes doesn't have the expected shape: "
1263
+ f"global_input_box={problem_spec.distribution[0]}, global_output_box={problem_spec.distribution[1]}" # type: ignore
1264
+ )
1265
+
1266
+ if self.options.redistribute:
1267
+ partition_dim = distribution.partition_dim # type: ignore
1268
+ if self.fft_abstract_type == "C2R":
1269
+ self.result_shape_padded, _ = _calculate_slab_shape_strides(
1270
+ global_result_extents_padded, partition_dim, rank, nranks
1271
+ )
1272
+ self.result_shape, self.result_strides = _calculate_slab_shape_strides(
1273
+ self.global_result_extents, partition_dim, rank, nranks, global_result_extents_padded
1274
+ )
1275
+
1276
+ # The input of the redistribute is the output of the FFT and will have these
1277
+ # strides. Note the special strides of the C2R output based on the output's
1278
+ # padded last axis.
1279
+ _, self.intermediate_strides = _calculate_slab_shape_strides(
1280
+ self.global_result_extents, 1 - partition_dim, rank, nranks, global_result_extents_padded
1281
+ )
1282
+ elif not isinstance(self.distribution, Slab):
1283
+ output_lower, output_upper = output_box
1284
+ self.result_shape = tuple(output_upper[i] - output_lower[i] for i in range(self.operand_dim))
1285
+ self.result_strides = calculate_strides(self.result_shape, reversed(range(self.operand_dim)))
1286
+ self.distribution_layout[output_box] = TensorLayout(shape=self.result_shape, strides=self.result_strides)
1287
+ output_box._bind(self.global_result_extents, shape=self.result_shape)
1288
+ else:
1289
+ result_partition_dim = 1 - distribution.partition_dim # type: ignore
1290
+ if self.fft_abstract_type == "C2R":
1291
+ self.result_shape_padded, _ = _calculate_slab_shape_strides(
1292
+ global_result_extents_padded, result_partition_dim, rank, nranks
1293
+ )
1294
+ self.result_shape, self.result_strides = _calculate_slab_shape_strides(
1295
+ self.global_result_extents, result_partition_dim, rank, nranks, global_result_extents_padded
1296
+ )
1297
+ self.distribution_layout[Slab.X if distribution == Slab.Y else Slab.Y] = TensorLayout(
1298
+ shape=self.result_shape, strides=self.result_strides
1299
+ )
1300
+
1301
+ # Obtain the result operand (the one that will be returned to the user with the
1302
+ # expected shape and dtype on this rank according to the FFT type and operand
1303
+ # distributions). Note that since the FFT is inplace, the result operand shares
1304
+ # the same buffer with the input operand.
1305
+ self._update_result_view_attributes(collective_error_checking=True)
1306
+
1307
+ # Create handle.
1308
+ with utils.device_ctx(self.device_id):
1309
+ self.handle = cufft.create()
1310
+ # Dummy handle to create a cufft descriptor with initial tiny data buffer.
1311
+ # We'll reuse this descriptor to call cufft.xt_exec_descriptor, by
1312
+ # setting the data pointer and subformat in the descriptor.
1313
+ self.memory_desc_handle = cufft.create()
1314
+ if self.options.redistribute:
1315
+ self.reshape_handle = cufft.create_reshape()
1316
+
1317
+ # Set stream for the FFT.
1318
+ with utils.device_ctx(self.device_id):
1319
+ cufft.set_stream(self.handle, stream_holder.ptr) # type: ignore[union-attr]
1320
+
1321
+ # Plan attributes.
1322
+ cufft.set_auto_allocation(self.handle, 0)
1323
+
1324
+ self.fft_planned = False
1325
+ # Descriptor to call cufft.xt_exec_descriptor (by setting the
1326
+ # data pointer and subformat in the descriptor before execute).
1327
+ self.memory_desc = None
1328
+ # Pointer to tiny data buffer of descriptor when first created.
1329
+ self.dummy_desc_data_ptr = None
1330
+
1331
+ # Workspace attributes.
1332
+ self.workspace_ptr: None | memory.MemoryPointer = None
1333
+ self.workspace_size = 0
1334
+ self._workspace_allocated_here = False
1335
+ self.reshaped_operand = None
1336
+
1337
+ # Attributes to establish stream ordering.
1338
+ self.workspace_stream = None
1339
+ self.last_compute_event = None
1340
+
1341
+ self.valid_state = True
1342
+ self.logger.info("The distributed FFT operation has been created.")
1343
+
1344
+ def __enter__(self):
1345
+ return self
1346
+
1347
+ def __exit__(self, exc_type, exc_value, traceback):
1348
+ self.free()
1349
+
1350
+ def _check_valid_fft(self, *args, **kwargs):
1351
+ """
1352
+ Check if FFT object is alive and well.
1353
+ """
1354
+ if not self.valid_state:
1355
+ raise InvalidFFTState("The FFT object cannot be used after resources are free'd")
1356
+
1357
+ def _free_plan_resources(self, exception: Exception | None = None) -> bool:
1358
+ """
1359
+ Free resources allocated in planning.
1360
+ """
1361
+ if self.memory_desc is not None:
1362
+ with utils.device_ctx(self.device_id):
1363
+ cufft.xt_free(self.memory_desc)
1364
+ self.memory_desc = None
1365
+
1366
+ self.fft_planned = False
1367
+ return True
1368
+
1369
+ def _internal_operand_package(self, package_name):
1370
+ if self.execution_space == "cuda":
1371
+ return package_name if package_name != "numpy" else "cuda"
1372
+ else:
1373
+ return package_name if package_name != "cupy" else "cupy_host"
1374
+
1375
+ def _allocate_reshape_operand(self, exec_stream_holder: StreamHolder | None, log_debug):
1376
+ if log_debug:
1377
+ self.logger.debug("Beginning empty tensor creation to hold redistribute value...")
1378
+ self.logger.debug(
1379
+ f"The redistributed tensor shape = {self.result_shape} with strides = "
1380
+ f"{self.result_strides} and data type '{self.result_data_type}'."
1381
+ )
1382
+
1383
+ capacity_out_dtype = (
1384
+ self.capacity * 2
1385
+ if self.fft_abstract_type == "C2R"
1386
+ else self.capacity // 2
1387
+ if self.fft_abstract_type == "R2C"
1388
+ else self.capacity
1389
+ )
1390
+ # For C2R we preserve the last axis strides of the real output
1391
+ # when we reshape.
1392
+ result = _allocate_for_fft(
1393
+ self.global_result_extents,
1394
+ self.result_shape,
1395
+ self.distribution,
1396
+ self.result_operand.name_to_dtype[self.result_data_type],
1397
+ "cuda",
1398
+ self.result_operand.module,
1399
+ self.fft_abstract_type[::-1], # type: ignore
1400
+ capacity_out_dtype,
1401
+ self.rank,
1402
+ self.nranks,
1403
+ )
1404
+ if log_debug:
1405
+ self.logger.debug("The redistributed output (empty) tensor has been created.")
1406
+ return result
1407
+
1408
+ def _get_result_views(self, collective_error_checking):
1409
+ """Compute result-operand views from the current operand buffer.
1410
+
1411
+ Returns ``(result, cpu_result)`` where *result* is a view of
1412
+ ``self.operand`` shaped for the FFT output and *cpu_result* is the
1413
+ corresponding view of ``self.operand_backup`` (or ``None`` when there
1414
+ is no cross-space backup).
1415
+ """
1416
+ if isinstance(self.distribution, Slab) and self.fft_abstract_type == "C2R":
1417
+
1418
+ def strided_view(x):
1419
+ v = _get_view(x, self.result_shape_padded, self.result_data_type, self.process_group, collective_error_checking)
1420
+ return v.__class__(v.tensor[..., : self.result_shape[-1]])
1421
+
1422
+ cpu_result = strided_view(self.operand_backup) if self.operand_backup is not None else None
1423
+ result = strided_view(self.operand)
1424
+ else:
1425
+ cpu_result = (
1426
+ _get_view(
1427
+ self.operand_backup, self.result_shape, self.result_data_type, self.process_group, collective_error_checking
1428
+ )
1429
+ if self.operand_backup is not None
1430
+ else None
1431
+ )
1432
+ result = _get_view(
1433
+ self.operand, self.result_shape, self.result_data_type, self.process_group, collective_error_checking
1434
+ )
1435
+ return result, cpu_result
1436
+
1437
+ def _update_result_view_attributes(self, *, collective_error_checking=False, keep_wrappers=False):
1438
+ """Rebuild result-operand views from the current operand buffer.
1439
+
1440
+ When *keep_wrappers* is ``False`` (default), ``self.result_operand``
1441
+ and ``self.cpu_result_operand`` are replaced with new wrapper objects.
1442
+ When ``True``, only ``.tensor`` is rebound on the existing wrappers.
1443
+ """
1444
+ result, cpu_result = self._get_result_views(collective_error_checking)
1445
+ if keep_wrappers:
1446
+ self.result_operand.tensor = result.tensor
1447
+ if cpu_result is not None:
1448
+ self.cpu_result_operand.tensor = cpu_result.tensor
1449
+ else:
1450
+ self.result_operand = result
1451
+ if cpu_result is not None:
1452
+ self.cpu_result_operand = cpu_result
1453
+
1454
+ @utils.precondition(_check_valid_fft)
1455
+ @utils.atomic(_free_plan_resources, method=True)
1456
+ def plan(self, *, stream: AnyStream | None = None):
1457
+ """Plan the FFT.
1458
+
1459
+ Args:
1460
+ stream: {stream}
1461
+ """
1462
+ log_info = self.logger.isEnabledFor(logging.INFO)
1463
+ log_debug = self.logger.isEnabledFor(logging.DEBUG)
1464
+
1465
+ if self.fft_planned:
1466
+ self.logger.debug("The FFT has already been planned, and redoing the plan is not supported.")
1467
+ return
1468
+
1469
+ stream_holder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
1470
+ self.workspace_stream = stream_holder.obj
1471
+
1472
+ # Set stream for the FFT.
1473
+ with utils.device_ctx(self.device_id):
1474
+ cufft.set_stream(self.handle, stream_holder.ptr)
1475
+
1476
+ check_inplace_overlapping_layout(self.operand)
1477
+ if self.operand_backup is not None:
1478
+ check_inplace_overlapping_layout(self.operand_backup)
1479
+
1480
+ if log_debug:
1481
+ self.logger.debug(
1482
+ f"The operand CUDA type is {NAME_TO_DATA_TYPE[self.operand_data_type].name}, and the result CUDA type is "
1483
+ f"{NAME_TO_DATA_TYPE[self.result_data_type].name}."
1484
+ )
1485
+ self.logger.debug(f"The CUDA type used for compute is {NAME_TO_DATA_TYPE[self.compute_data_type].name}.")
1486
+ if log_info:
1487
+ self.logger.info("Starting distributed FFT planning...")
1488
+
1489
+ planner = None
1490
+ if self.operand_dim == 2:
1491
+ planner = cufft.make_plan2d
1492
+ elif self.operand_dim == 3:
1493
+ planner = cufft.make_plan3d
1494
+ else:
1495
+ raise AssertionError("Internal error: unsupported dimensionality for distributed FFT in plan().")
1496
+
1497
+ if self.options.redistribute:
1498
+ # Plan a reshape of the FFT output back to the original slab distribution of the
1499
+ # FFT input.
1500
+ from_partition_dim, to_partition_dim = (1, 0) if self.distribution == Slab.X else (0, 1)
1501
+ # cuFFTMP reshape API only supports 3D, so we broadcast 2D operands.
1502
+ X, Y = self.global_result_extents[:2]
1503
+ Z = self.global_result_extents[2] if self.operand_dim == 3 else 1
1504
+ global_shape = (X, Y, Z)
1505
+ reshape_input_box = _calculate_local_box(global_shape, from_partition_dim, self.rank, self.nranks)
1506
+ reshape_output_box = _calculate_local_box(global_shape, to_partition_dim, self.rank, self.nranks)
1507
+ lower, upper = reshape_input_box
1508
+ reshape_input_strides = (
1509
+ self.intermediate_strides if self.operand_dim == 3 else tuple(self.intermediate_strides) + (1,)
1510
+ )
1511
+ reshape_output_strides = self.result_strides if self.operand_dim == 3 else tuple(self.result_strides) + (1,)
1512
+
1513
+ with utils.cuda_call_ctx(stream_holder, blocking=True, timing=log_info) as (
1514
+ self.last_compute_event,
1515
+ elapsed,
1516
+ ):
1517
+ if isinstance(self.distribution, Slab):
1518
+ self.subformat = self.distribution._cufftmp_value
1519
+ else:
1520
+ if self.fft_abstract_type == "C2R":
1521
+ # C2R plans only support CUFFT_XT_FORMAT_DISTRIBUTED_OUTPUT,
1522
+ # i.e., (lower_input, upper_input) should describe the real data
1523
+ # distribution and (lower_output, upper_output) the complex data
1524
+ # distribution.
1525
+ lower_input, upper_input = self.distribution[1]
1526
+ lower_output, upper_output = self.distribution[0]
1527
+ strides_input = self.result_strides
1528
+ strides_output = self.operand_layout.strides
1529
+ else:
1530
+ lower_input, upper_input = self.distribution[0]
1531
+ lower_output, upper_output = self.distribution[1]
1532
+ strides_input = self.operand_layout.strides
1533
+ strides_output = self.result_strides
1534
+
1535
+ cufft.xt_set_distribution(
1536
+ self.handle,
1537
+ self.operand_dim,
1538
+ lower_input,
1539
+ upper_input,
1540
+ lower_output,
1541
+ upper_output,
1542
+ strides_input,
1543
+ strides_output,
1544
+ )
1545
+ self.box_to_subformat = {}
1546
+ self.box_to_subformat[Box(lower_input, upper_input)] = cufft.XtSubFormat.FORMAT_DISTRIBUTED_INPUT
1547
+ self.box_to_subformat[Box(lower_output, upper_output)] = cufft.XtSubFormat.FORMAT_DISTRIBUTED_OUTPUT
1548
+ self.subformat = (
1549
+ cufft.XtSubFormat.FORMAT_DISTRIBUTED_INPUT
1550
+ if self.fft_abstract_type != "C2R"
1551
+ else cufft.XtSubFormat.FORMAT_DISTRIBUTED_OUTPUT
1552
+ )
1553
+
1554
+ fft_concrete_type = _get_fft_concrete_type(self.operand_data_type, self.fft_abstract_type)
1555
+ self.logger.debug(f"The FFT concrete type is {fft_concrete_type.name}.")
1556
+ # NVSHMEM is already initialized (no need to pass MPI comm to the library).
1557
+ cufft.attach_comm(self.handle, cufft.MpCommType.COMM_NONE, 0)
1558
+ if self.fft_abstract_type == "C2R":
1559
+ self.workspace_size = planner(self.handle, *self.global_result_extents, fft_concrete_type)
1560
+ else:
1561
+ self.workspace_size = planner(self.handle, *self.global_extents, fft_concrete_type)
1562
+
1563
+ # Create memory descriptor using dummy handle.
1564
+ _ = planner(self.memory_desc_handle, *[1] * self.operand_dim, fft_concrete_type)
1565
+ self.memory_desc = cufft.xt_malloc(self.memory_desc_handle, cufft.XtSubFormat.FORMAT_INPLACE)
1566
+
1567
+ if self.options.redistribute:
1568
+ nullptr = 0
1569
+ cufft.make_reshape(
1570
+ self.reshape_handle,
1571
+ self.result_operand.itemsize,
1572
+ 3,
1573
+ reshape_input_box[0],
1574
+ reshape_input_box[1],
1575
+ reshape_input_strides,
1576
+ reshape_output_box[0],
1577
+ reshape_output_box[1],
1578
+ reshape_output_strides,
1579
+ nullptr,
1580
+ cufft.MpCommType.COMM_NONE,
1581
+ )
1582
+ reshape_workspace_size = cufft.get_reshape_size(self.reshape_handle)
1583
+ self.workspace_size = max(self.workspace_size, reshape_workspace_size)
1584
+
1585
+ self.logger.debug(
1586
+ f"The workspace required on process {self.rank} for the distributed"
1587
+ f" FFT operation is {formatters.MemoryStr(self.workspace_size)}."
1588
+ )
1589
+
1590
+ # Store memory descriptor's buffer pointer, to be able to free it later.
1591
+ self.dummy_desc_data_ptr = cufft.set_descriptor_data(self.memory_desc, 0, self.subformat)
1592
+
1593
+ self.fft_planned = True
1594
+
1595
+ if log_info and elapsed.data is not None:
1596
+ self.logger.info(f"The FFT planning phase took {elapsed.data:.3f} ms to complete.")
1597
+
1598
+ def _validate_reset_operand(self, operand, input_distribution, stream):
1599
+ """
1600
+ (private) Validate operand and distribution for reset_operand.
1601
+
1602
+ Performs all precondition checks (operand compatibility,
1603
+ distribution compatibility) without any side effects.
1604
+
1605
+ Returns ``(distribution, distribution_unchanged)``
1606
+ where *distribution* is in internal form (Slab or Box-tuple,
1607
+ **not** yet bound) and *distribution_unchanged* is a bool.
1608
+
1609
+ This method does **not** mutate ``self.distribution``,
1610
+ ``self.subformat``, or the distribution object.
1611
+ """
1612
+ wrapped = tensor_wrapper.wrap_operand(operand)
1613
+
1614
+ if self.package != wrapped.name:
1615
+ raise TypeError(f"Library package mismatch: '{self.package}' => '{wrapped.name}'")
1616
+
1617
+ utils.check_attribute_match(self.operand_data_type, wrapped.dtype, "data type")
1618
+
1619
+ if len(wrapped.shape) != self.operand_dim:
1620
+ raise ValueError(
1621
+ f"The reset operand number of dimensions ({len(wrapped.shape)}) does not "
1622
+ f"match the FFT number of dimensions ({self.operand_dim})"
1623
+ )
1624
+
1625
+ stream_holder: StreamHolder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
1626
+ self.logger.info(f"The specified stream for reset_operand() is {stream_holder and stream_holder.obj}.")
1627
+
1628
+ # In principle, we could support memory_space change,
1629
+ # but to handle it properly we need to update self.memory_space and
1630
+ # some dependent properties, like self.blocking, which may be error-prone
1631
+ # from the user perspective. It would prevent inplace optimizations as well.
1632
+ operand_device_id = wrapped.device_id
1633
+ if operand_device_id != self.operand_device_id:
1634
+
1635
+ def device_str(device_id: int | Literal["cpu"]) -> str:
1636
+ return f"cuda:{device_id}" if isinstance(device_id, int) else f"{device_id}"
1637
+
1638
+ raise ValueError(
1639
+ f"The new operand must be on the same device as the original one. "
1640
+ f"The new operand's device is {device_str(operand_device_id)}, "
1641
+ f"the original device is {device_str(self.operand_device_id)}"
1642
+ )
1643
+
1644
+ if self.memory_space == "cuda" and not wrapped.is_symmetric_memory:
1645
+ raise TypeError("Distributed FFT requires GPU operand to be on symmetric memory")
1646
+
1647
+ # Check for C memory layout.
1648
+ if sorted(wrapped.strides, reverse=True) != list(wrapped.strides):
1649
+ raise ValueError("The reset operand memory layout is not C")
1650
+
1651
+ # Check that the distribution of the reset operand is compatible.
1652
+ if input_distribution is None:
1653
+ raise ValueError("Please specify the distribution of the operand for reset_operand")
1654
+
1655
+ if isinstance(input_distribution, Distribution):
1656
+ distribution = input_distribution.to(Slab, ndim=self.operand_dim, copy=True)
1657
+ else:
1658
+ # Must be a Box pair.
1659
+ distribution = tuple(cast(Box, box.copy()) for box in input_distribution)
1660
+
1661
+ distribution_type_old = "slab" if isinstance(self.distribution, Slab) else "box"
1662
+ distribution_type_new = "slab" if isinstance(distribution, Slab) else "box"
1663
+ if distribution_type_old != distribution_type_new:
1664
+ raise ValueError(
1665
+ f"This FFT uses {distribution_type_old} distribution, but got "
1666
+ f"{distribution_type_new} distribution in reset_operand."
1667
+ )
1668
+
1669
+ if self.fft_abstract_type in ("R2C", "C2R") and self.distribution != distribution:
1670
+ raise ValueError(f"Can't change distribution with FFT type {self.fft_abstract_type}")
1671
+
1672
+ distribution_unchanged = self.distribution == distribution
1673
+
1674
+ if distribution_type_old == "slab":
1675
+ if self.options.redistribute and not distribution_unchanged:
1676
+ raise ValueError("Can't change distribution when using redistribute=True")
1677
+ else:
1678
+ distribution = cast(Sequence[Box], distribution)
1679
+ input_box, output_box = distribution
1680
+ if {input_box, output_box} != set(self.box_to_subformat):
1681
+ raise ValueError("The reset operand distribution must use the original (input, output) box pair (in any order)")
1682
+
1683
+ # Check the operand shape matches the distribution.
1684
+ d = distribution if isinstance(distribution, Slab) else distribution[0]
1685
+ expected_shape = d.shape(self.rank, self.global_extents)
1686
+
1687
+ if tuple(wrapped.shape) != expected_shape:
1688
+ raise ValueError(
1689
+ f"Expected operand shape {expected_shape} for {distribution} "
1690
+ f"with global shape {self.global_extents}, got {tuple(wrapped.shape)}"
1691
+ )
1692
+
1693
+ return distribution, distribution_unchanged
1694
+
1695
+ def _bind_and_apply_distribution(self, distribution):
1696
+ """
1697
+ (private) Bind *distribution* to the global shape and apply it.
1698
+
1699
+ Attach the new distribution to the global shape so it knows the
1700
+ local partition on this rank, then update the cufftMp subformat
1701
+ identifier that tells the library which distribution to use
1702
+ during execution.
1703
+
1704
+ Shape validation is **not** performed here; the caller is
1705
+ responsible for verifying the operand shape beforehand (the
1706
+ checked path does this in ``_validate_reset_operand``).
1707
+ """
1708
+ if isinstance(distribution, Slab):
1709
+ distribution._bind(self.global_extents)
1710
+ self.subformat = distribution._cufftmp_value
1711
+ else:
1712
+ distribution[0]._bind(self.global_extents)
1713
+ self.subformat = self.box_to_subformat[distribution[0]]
1714
+ self.distribution = distribution
1715
+
1716
+ def _log_distribution(self):
1717
+ """
1718
+ (private) Log the current distribution axis / box information.
1719
+ """
1720
+ if isinstance(self.distribution, Slab):
1721
+ if self.options.redistribute:
1722
+ from_axis, to_axis = ("X", "X") if self.distribution == Slab.X else ("Y", "Y")
1723
+ else:
1724
+ from_axis, to_axis = ("X", "Y") if self.distribution == Slab.X else ("Y", "X")
1725
+ self.logger.info(
1726
+ f"The operand distribution is Slab, with input partitioned on {from_axis} axis "
1727
+ f"and output on {to_axis} (redistribute={self.options.redistribute})."
1728
+ )
1729
+ else:
1730
+ self.logger.info("The operand distribution is based on custom input and output boxes given on each process.")
1731
+
1732
+ def _reset_operand_same_distribution_same_space(self, operand, *, enable_logging=True):
1733
+ """
1734
+ (private) Reset operand when the distribution is unchanged
1735
+ and execution_space == memory_space.
1736
+
1737
+ This method is hit when the distribution does not change compared to
1738
+ the one used at plan time. This is always the case for R2C/C2R since
1739
+ these forbid distribution changes, and also covers C2C when the caller
1740
+ reuses the same distribution.
1741
+
1742
+ Because both the distribution and memory space are unchanged, this is
1743
+ the leanest path: the user operand already resides in the execution
1744
+ memory space (GPU symmetric memory), so we just swap the underlying
1745
+ tensor reference in the wrapper.
1746
+ """
1747
+ log_info = enable_logging and self.logger.isEnabledFor(logging.INFO)
1748
+
1749
+ self.operand.tensor = operand
1750
+
1751
+ if log_info:
1752
+ self._log_distribution()
1753
+ self.logger.info(f"The reset operand shape = {self.operand.shape}, and strides = {self.operand.strides}.")
1754
+ self.logger.info(f"The result shape = {self.result_shape}, and strides = {self.result_strides}.")
1755
+
1756
+ # self.result_operand was set during plan() and may be the same
1757
+ # object as self.operand (C2C with reshape, where result shape
1758
+ # matches operand shape). In that case, the tensor swap above
1759
+ # already implies that self.result_operand is updated too.
1760
+ # Otherwise it is a separate wrapper and must be rebuilt.
1761
+ if self.result_operand is not self.operand:
1762
+ self._update_result_view_attributes(keep_wrappers=True)
1763
+ self._operand_released = False
1764
+
1765
+ def _reset_operand_same_distribution_cross_space(self, operand, stream, *, enable_logging=True):
1766
+ """
1767
+ (private) Reset operand when the distribution is unchanged
1768
+ but execution_space != memory_space.
1769
+
1770
+ This method is hit when the distribution does not change compared to
1771
+ the one used at plan time. This is always the case for R2C/C2R since
1772
+ these forbid distribution changes, and also covers C2C when the caller
1773
+ reuses the same distribution.
1774
+
1775
+ The user operand lives on CPU while the FFT executes on CUDA.
1776
+ We have two attributes to deal with:
1777
+
1778
+ - ``self.operand``: points to the GPU mirror in symmetric memory, used by cufftMp.
1779
+ - ``self.operand_backup``: points to the user's CPU tensor, for result copy-back.
1780
+ """
1781
+ log_info = enable_logging and self.logger.isEnabledFor(logging.INFO)
1782
+
1783
+ stream_holder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
1784
+ operand_wrapped = tensor_wrapper.wrap_operand(operand)
1785
+ if self._operand_released:
1786
+ # release_operand() freed the symmetric memory but kept the
1787
+ # TensorHolder wrappers alive. So here we allocate a fresh GPU buffer
1788
+ # and rebind .tensor on the existing wrappers.
1789
+ new_mirror = _alloc_and_copy_to_exespace_mirror(
1790
+ operand_wrapped,
1791
+ stream_holder,
1792
+ self.fft_abstract_type,
1793
+ self.global_extents,
1794
+ self.distribution,
1795
+ self.capacity,
1796
+ self.rank,
1797
+ self.nranks,
1798
+ )
1799
+ self.operand.tensor = new_mirror.tensor
1800
+ self.operand_backup.tensor = operand_wrapped.tensor
1801
+ else:
1802
+ # GPU mirror is alive, we copy the new CPU data into it and
1803
+ # update the backup to reference the new CPU tensor.
1804
+ self.operand.copy_(operand_wrapped, stream_holder=stream_holder)
1805
+ self.operand_backup.tensor = operand_wrapped.tensor
1806
+
1807
+ if log_info:
1808
+ self._log_distribution()
1809
+ self.logger.info(f"The reset operand shape = {self.operand.shape}, and strides = {self.operand.strides}.")
1810
+ self.logger.info(f"The result shape = {self.result_shape}, and strides = {self.result_strides}.")
1811
+
1812
+ # self.result_operand was set during plan() and may be the same
1813
+ # object as self.operand (C2C with reshape, where result shape matches
1814
+ # operand shape). In that case, the rebinds above already implies
1815
+ # that self.result_operand is updated too.
1816
+ # Otherwise it is a separate wrapper and must be rebuilt.
1817
+ if self.result_operand is not self.operand:
1818
+ self._update_result_view_attributes(keep_wrappers=True)
1819
+ self._operand_released = False
1820
+
1821
+ def _reset_operand_new_distribution(self, operand, stream, *, enable_logging=True):
1822
+ """Reset operand when the distribution has changed (C2C only).
1823
+
1824
+ This method is hit when the caller switches between distributions
1825
+ (e.g. Slab.X ↔ Slab.Y). This path is only reachable for C2C
1826
+ transforms (R2C/C2R cannot change distribution by contract), so
1827
+ all logic below assumes C2C.
1828
+
1829
+ Because the distribution changed, the local operand shape may
1830
+ differ from the previous call. This method therefore recomputes
1831
+ the operand and result layouts and rebuilds the result views.
1832
+
1833
+ The caller must have already bound the new distribution and
1834
+ applied it (via ``_bind_and_apply_distribution``) before
1835
+ invoking this method.
1836
+ """
1837
+ log_info = enable_logging and self.logger.isEnabledFor(logging.INFO)
1838
+
1839
+ if log_info:
1840
+ self._log_distribution()
1841
+
1842
+ # Phase 1: Update self.operand to point at the new data
1843
+ # ------------------------------------------------------
1844
+ # The new distribution may change the local shape. Both branches
1845
+ # below rebind .tensor on the existing TensorHolder wrappers rather
1846
+ # than replacing them. This is safe because the wrappers carry no
1847
+ # distribution-specific state — shape and strides are derived from
1848
+ # .tensor. In the same-space case the user already provides a tensor
1849
+ # on symmetric memory, so we just swap the reference. In the
1850
+ # cross-space case, we must copy the CPU data into the GPU mirror
1851
+ # (allocating one if it was released).
1852
+ if self.execution_space == self.memory_space:
1853
+ self.operand.tensor = operand
1854
+ else:
1855
+ # Cross-space: two wrappers track the operand:
1856
+ # - self.operand: points to the GPU mirror in symmetric memory, used by cufftMp
1857
+ # - self.operand_backup: points to the user's CPU tensor, for result copy-back
1858
+ stream_holder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
1859
+ operand_wrapped = tensor_wrapper.wrap_operand(operand)
1860
+ if self._operand_released:
1861
+ # release_operand() freed the symmetric memory but kept the
1862
+ # TensorHolder wrappers alive. So here we allocate a fresh GPU buffer
1863
+ # and rebind .tensor on the existing wrappers.
1864
+ new_mirror = _alloc_and_copy_to_exespace_mirror(
1865
+ operand_wrapped,
1866
+ stream_holder,
1867
+ self.fft_abstract_type,
1868
+ self.global_extents,
1869
+ self.distribution,
1870
+ self.capacity,
1871
+ self.rank,
1872
+ self.nranks,
1873
+ )
1874
+ self.operand.tensor = new_mirror.tensor
1875
+ self.operand_backup.tensor = operand_wrapped.tensor
1876
+ else:
1877
+ # Mirror is still alive, we need to get a view of it
1878
+ # with the new shape and copy the new CPU data in.
1879
+ operand_view = _get_view(
1880
+ self.operand,
1881
+ operand_wrapped.shape,
1882
+ operand_wrapped.dtype,
1883
+ self.process_group,
1884
+ collective_error_checking=False,
1885
+ )
1886
+ operand_view.copy_(operand_wrapped, stream_holder=stream_holder)
1887
+ self.operand.tensor = operand_view.tensor
1888
+ self.operand_backup.tensor = operand_wrapped.tensor
1889
+
1890
+ # Phase 2: Recompute layouts
1891
+ # --------------------------
1892
+ # The operand's local shape may have changed, so update the layout.
1893
+ self.operand_layout = TensorLayout(shape=self.operand.shape, strides=self.operand.strides)
1894
+
1895
+ if log_info:
1896
+ self.logger.info(
1897
+ f"The reset operand shape = {self.operand_layout.shape}, and strides = {self.operand_layout.strides}."
1898
+ )
1899
+
1900
+ # Determine result layout based on how cufftMp distributes the output:
1901
+ # - Box: result follows the output box's precomputed layout.
1902
+ # - Slab without redistribution: output is on the complementary axis.
1903
+ # - Slab with redistribution: output matches the input axis.
1904
+ if isinstance(self.distribution, tuple):
1905
+ output_box = self.distribution[1]
1906
+ result_layout = self.distribution_layout[output_box]
1907
+ output_box._bind(self.global_result_extents)
1908
+ elif not self.options.redistribute:
1909
+ result_layout = self.distribution_layout[Slab.X if self.distribution == Slab.Y else Slab.Y]
1910
+ else:
1911
+ result_layout = self.operand_layout
1912
+
1913
+ self.result_shape = result_layout.shape
1914
+ self.result_strides = result_layout.strides
1915
+
1916
+ if log_info:
1917
+ self.logger.info(f"The result shape = {self.result_shape}, and strides = {self.result_strides}.")
1918
+
1919
+ # Phase 3: Rebuild result views
1920
+ # ------------------------------
1921
+ # Rebuild self.result_operand (and self.cpu_result_operand in cross-space)
1922
+ # to reflect the updated buffer and result layout.
1923
+ # We replace the wrapper (keep_wrappers=False) because self.result_operand
1924
+ # may be the same object as self.operand. A distribution change can make
1925
+ # result_shape diverge, and rebinding .tensor on a shared object would
1926
+ # corrupt self.operand. Replacing the wrapper breaks the stale alias.
1927
+ self._update_result_view_attributes(keep_wrappers=False)
1928
+
1929
+ self._operand_released = False
1930
+
1931
+ @utils.precondition(_check_valid_fft)
1932
+ def reset_operand(self, operand, *, distribution: Distribution | Sequence[Box], stream: AnyStream | None = None):
1933
+ """
1934
+ Reset the operand held by this :class:`FFT` instance to a new compatible
1935
+ operand for subsequent execution.
1936
+
1937
+ Args:
1938
+ operand: A tensor (ndarray-like object) compatible with the previous one.
1939
+ The new operand is considered compatible if all the
1940
+ following properties match with the previous one:
1941
+
1942
+ - The operand data type.
1943
+ - The package that the new operand belongs to.
1944
+ - The memory space of the new operand (CPU or GPU).
1945
+ - The device that the new operand belongs to if it is on GPU.
1946
+ - The operand shape must be consistent with the specified
1947
+ ``distribution`` (see below).
1948
+
1949
+ distribution: {distribution} This argument is required.
1950
+ The distribution must be compatible with the one used at plan time:
1951
+
1952
+ - If the FFT was planned using a Slab distribution, the reset
1953
+ distribution must also be a Slab distribution. For C2C transforms,
1954
+ both ``Slab.X`` and ``Slab.Y`` are valid regardless of the slab
1955
+ axis at plan time. For R2C and C2R transforms, the distribution
1956
+ must be the same as at plan time.
1957
+ - If the FFT was planned using a box distribution, the reset
1958
+ distribution must use the same ``(input_box, output_box)`` pair
1959
+ specified at plan time (the order may be swapped).
1960
+ - If ``redistribute=True`` was specified in the options, the distribution
1961
+ cannot be changed.
1962
+
1963
+ stream: {stream}
1964
+
1965
+ Examples:
1966
+
1967
+ >>> import cupy as cp
1968
+ >>> import nvmath.distributed
1969
+
1970
+ Get process group used to initialize nvmath.distributed (for information on
1971
+ initializing nvmath.distributed, you can refer to the documentation or to the
1972
+ FFT examples in `nvmath/examples/distributed/fft
1973
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/fft>`_):
1974
+
1975
+ >>> process_group = nvmath.distributed.get_context().process_group
1976
+ >>> nranks = process_group.nranks
1977
+
1978
+ Create a 3-D complex128 ndarray on GPU symmetric memory, distributed according
1979
+ to the Slab distribution on the X axis (the global shape is (128, 128, 128)):
1980
+
1981
+ >>> from nvmath.distributed.distribution import Slab
1982
+ >>> shape = 128 // nranks, 128, 128
1983
+ >>> dtype = cp.complex128
1984
+ >>> a = nvmath.distributed.allocate_symmetric_memory(shape, cp, dtype=dtype)
1985
+ >>> a[:] = cp.random.rand(*shape) + 1j * cp.random.rand(*shape)
1986
+
1987
+ Create an FFT object as a context manager
1988
+
1989
+ >>> with nvmath.distributed.fft.FFT(a, distribution=Slab.X) as f:
1990
+ ... # Plan the FFT
1991
+ ... f.plan()
1992
+ ...
1993
+ ... # Execute the FFT to get the first result.
1994
+ ... r1 = f.execute()
1995
+ ...
1996
+ ... # Reset the operand to a new CuPy ndarray.
1997
+ ... b = nvmath.distributed.allocate_symmetric_memory(shape, cp, dtype=dtype)
1998
+ ... b[:] = cp.random.rand(*shape) + 1j * cp.random.rand(*shape)
1999
+ ... f.reset_operand(b, distribution=Slab.X)
2000
+ ...
2001
+ ... # Execute to get the new result corresponding to the updated operand.
2002
+ ... r2 = f.execute()
2003
+
2004
+ With :meth:`reset_operand`, minimal overhead is achieved as problem
2005
+ specification and planning are only performed once.
2006
+
2007
+ For the particular example above, explicitly calling :meth:`reset_operand` is
2008
+ equivalent to updating the operand in-place, i.e, replacing
2009
+ ``f.reset_operand(b, distribution=Slab.X)`` with ``a[:]=b``.
2010
+ Note that updating the operand in-place should be adopted with
2011
+ caution as it can only yield the expected result and
2012
+ incur no additional copies under the additional constraints below:
2013
+
2014
+ - The operand's distribution is the same.
2015
+
2016
+ For more details, please refer to `inplace update example
2017
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/fft/example06_stateful_reset_inplace.py>`_.
2018
+
2019
+ .. seealso::
2020
+ :meth:`release_operand`
2021
+ """
2022
+ self.logger.info("Resetting operand...")
2023
+
2024
+ if operand is None:
2025
+ raise ValueError("reset_operand() requires a valid operand.")
2026
+
2027
+ distribution, distribution_unchanged = self._validate_reset_operand(operand, distribution, stream)
2028
+
2029
+ # When the distribution is unchanged, we can skip a lot of boilerplate
2030
+ # code and logic for re-layouting the operand and result.
2031
+ # This always applies to R2C/C2R since the distribution is fixed by design
2032
+ # and to C2C with the same distribution. Only C2C with a changed distribution
2033
+ # falls through to the full path.
2034
+ if distribution_unchanged:
2035
+ if self.execution_space == self.memory_space:
2036
+ self._reset_operand_same_distribution_same_space(operand)
2037
+ else:
2038
+ self._reset_operand_same_distribution_cross_space(operand, stream)
2039
+ else:
2040
+ self._bind_and_apply_distribution(distribution)
2041
+ self._reset_operand_new_distribution(operand, stream)
2042
+
2043
+ self.logger.info("The operand has been reset to the specified operand.")
2044
+
2045
+ def reset_operand_unchecked(
2046
+ self,
2047
+ operand,
2048
+ *,
2049
+ distribution: Distribution | Sequence[Box],
2050
+ stream: AnyStream | None = None,
2051
+ ):
2052
+ """
2053
+ {reset_operand_unchecked}
2054
+ """
2055
+ # Convert to internal form before comparing so that equivalent
2056
+ # distributions of different types are recognized as equal.
2057
+ if isinstance(distribution, Distribution):
2058
+ distribution = distribution.to(Slab, ndim=self.operand_dim, copy=True)
2059
+ else:
2060
+ distribution = tuple(cast(Box, box.copy()) for box in distribution)
2061
+
2062
+ # When the distribution is unchanged, we can skip a lot of boilerplate
2063
+ # code and logic for re-layouting the operand and result.
2064
+ # This always applies to R2C/C2R since the distribution is fixed by design
2065
+ # and to C2C with the same distribution. Only C2C with a changed distribution
2066
+ # falls through to the full path.
2067
+ if self.distribution == distribution:
2068
+ if self.execution_space == self.memory_space:
2069
+ self._reset_operand_same_distribution_same_space(operand, enable_logging=False)
2070
+ else:
2071
+ self._reset_operand_same_distribution_cross_space(operand, stream, enable_logging=False)
2072
+ else:
2073
+ self._bind_and_apply_distribution(distribution)
2074
+ self._reset_operand_new_distribution(operand, stream, enable_logging=False)
2075
+
2076
+ @utils.precondition(_check_valid_fft)
2077
+ def release_operand(self):
2078
+ """
2079
+ {release_operand}
2080
+ """
2081
+ if self._operand_released:
2082
+ self.logger.info("Operand has already been released; nothing to do.")
2083
+ return
2084
+
2085
+ # Note that if/when possible, we keep the TensorHolder wrappers alive
2086
+ # and only release the internal tensor reference. This is useful when
2087
+ # reset_operand_unchecked is called subsequently because it can reuse
2088
+ # the existing wrappers, saving overhead.
2089
+ if self.execution_space == self.memory_space:
2090
+ # Same-space: self.operand is the user's tensor, and
2091
+ # self.result_operand is a view of it (or the same object
2092
+ # for C2C when shape/dtype match). Both must be released.
2093
+ self.operand.tensor = None
2094
+ self.result_operand.tensor = None
2095
+ else:
2096
+ # Cross space:
2097
+ # self.operand_backup = user's tensor
2098
+ # self.cpu_result_operand = view of self.operand_backup (for result copy-back)
2099
+ # self.operand = internal nvshmem device mirror
2100
+ # self.result_operand = view of self.operand (shares nvshmem buffer)
2101
+ # Release user references and free the nvshmem mirror.
2102
+ # Cross-space execution is always blocking, so no synchronization
2103
+ # is needed before freeing the operand.
2104
+ self.operand_backup.tensor = None
2105
+ self.cpu_result_operand.tensor = None
2106
+ # Free nvshmem before clearing self.operand.tensor — self.result_operand
2107
+ # may alias self.operand (C2C with matching shape/dtype), so clearing
2108
+ # result_operand.tensor first would also null self.operand.tensor.
2109
+ with utils.device_ctx(self.device_id):
2110
+ self.operand.free_symmetric()
2111
+ self.operand.tensor = None
2112
+ if self.result_operand is not self.operand:
2113
+ self.result_operand.tensor = None
2114
+
2115
+ self._operand_released = True
2116
+ self.logger.info("User-provided operand has been released.")
2117
+
2118
+ def _check_planned(self, *args, **kwargs):
2119
+ """ """
2120
+ what = kwargs["what"]
2121
+ if not self.fft_planned:
2122
+ raise RuntimeError(f"{what} cannot be performed before plan() has been called.")
2123
+
2124
+ def _check_valid_operand(self, *args, **kwargs):
2125
+ """ """
2126
+ what = kwargs["what"]
2127
+ if self._operand_released:
2128
+ raise RuntimeError(
2129
+ f"{what} cannot be performed after the operand has been released. Use reset_operand() to provide a new "
2130
+ f"operand before performing the {what.lower()}."
2131
+ )
2132
+
2133
+ def _free_workspace_memory(self, exception: Exception | None = None) -> bool:
2134
+ """
2135
+ Free workspace by releasing the MemoryPointer object and reshape operand.
2136
+ """
2137
+ if self.workspace_ptr is None:
2138
+ return True
2139
+
2140
+ with utils.device_ctx(self.device_id):
2141
+ # Calling nvshmem_free on memory that's still in use is not safe
2142
+ # (nvshmem_free is not stream-ordered), so we need to wait for the
2143
+ # computation to finish.
2144
+ if self.workspace_stream is not None:
2145
+ self.workspace_stream.sync()
2146
+ self.workspace_ptr.free()
2147
+ if self.reshaped_operand is not None:
2148
+ self.reshaped_operand.free_symmetric()
2149
+ self.workspace_ptr = None
2150
+ self.reshaped_operand = None
2151
+ self.logger.debug("[_free_workspace_memory] The workspace has been released.")
2152
+
2153
+ return True
2154
+
2155
+ @utils.precondition(_check_valid_fft)
2156
+ @utils.precondition(_check_planned, "Workspace memory allocation")
2157
+ @utils.atomic(_free_workspace_memory, method=True)
2158
+ def _allocate_workspace_memory(self, stream_holder: StreamHolder):
2159
+ """
2160
+ Allocate workspace memory using the specified allocator.
2161
+ """
2162
+
2163
+ assert self._workspace_allocated_here is False, "Internal Error."
2164
+
2165
+ self.logger.debug("Allocating workspace for performing the FFT...")
2166
+ with utils.device_ctx(self.device_id), stream_holder.ctx:
2167
+ try:
2168
+ self.workspace_ptr = self.allocator.memalloc(self.workspace_size) # type: ignore[union-attr]
2169
+ if self.options.redistribute:
2170
+ self.reshaped_operand = self._allocate_reshape_operand(
2171
+ stream_holder, self.logger.isEnabledFor(logging.DEBUG)
2172
+ )
2173
+ self._workspace_allocated_here = True
2174
+ except TypeError as e:
2175
+ message = (
2176
+ "The method 'memalloc' in the allocator object must conform to the interface in the "
2177
+ "'BaseCUDAMemoryManager' protocol."
2178
+ )
2179
+ raise TypeError(message) from e
2180
+ raw_workspace_ptr = utils.get_ptr_from_memory_pointer(self.workspace_ptr)
2181
+ cufft.set_work_area(self.handle, raw_workspace_ptr)
2182
+
2183
+ self.workspace_stream = stream_holder.obj
2184
+ self.logger.debug(
2185
+ f"Finished allocating device workspace of size {formatters.MemoryStr(self.workspace_size)} in the context "
2186
+ f"of stream {self.workspace_stream}."
2187
+ )
2188
+
2189
+ def _allocate_workspace_memory_perhaps(self, stream_holder: StreamHolder):
2190
+ """
2191
+ Allocate workspace memory using the specified allocator, if it hasn't already been
2192
+ done.
2193
+ """
2194
+ if self.execution_space != "cuda" or self.workspace_ptr is not None:
2195
+ return
2196
+
2197
+ return self._allocate_workspace_memory(stream_holder)
2198
+
2199
+ @utils.precondition(_check_valid_fft)
2200
+ def _free_workspace_memory_perhaps(self, release_workspace):
2201
+ """
2202
+ Free workspace memory if if 'release_workspace' is True.
2203
+ """
2204
+ if not release_workspace:
2205
+ return
2206
+
2207
+ # Establish ordering wrt the computation and free workspace if it's more than the
2208
+ # specified cache limit.
2209
+ if self.last_compute_event is not None:
2210
+ with utils.device_ctx(self.device_id):
2211
+ self.workspace_stream.wait(self.last_compute_event)
2212
+ self.logger.debug("Established ordering with respect to the computation before releasing the workspace.")
2213
+ self.last_compute_event = None
2214
+
2215
+ self.logger.debug("[_free_workspace_memory_perhaps] The workspace memory will be released.")
2216
+ self._free_workspace_memory()
2217
+
2218
+ return True
2219
+
2220
+ def _release_workspace_memory_perhaps(self, exception: Exception | None = None) -> bool:
2221
+ """
2222
+ Free workspace memory if it was allocated in this call
2223
+ (self._workspace_allocated_here == True) when an exception occurs.
2224
+ """
2225
+ release_workspace = self._workspace_allocated_here
2226
+ self.logger.debug(
2227
+ f"[_release_workspace_memory_perhaps] The release_workspace flag is set to {release_workspace} based upon "
2228
+ "the value of 'workspace_allocated_here'."
2229
+ )
2230
+ self._free_workspace_memory_perhaps(release_workspace)
2231
+ self._workspace_allocated_here = False
2232
+ return True
2233
+
2234
+ @utils.precondition(_check_valid_fft)
2235
+ @utils.precondition(_check_planned, "Execution")
2236
+ @utils.precondition(_check_valid_operand, "Execution")
2237
+ @utils.atomic(_release_workspace_memory_perhaps, method=True)
2238
+ def execute(
2239
+ self,
2240
+ *,
2241
+ direction: FFTDirection | None = None,
2242
+ stream: AnyStream | None = None,
2243
+ release_workspace: bool = False,
2244
+ sync_symmetric_memory: bool = True,
2245
+ ):
2246
+ """
2247
+ Execute the FFT operation.
2248
+
2249
+ Args:
2250
+ direction: {direction}
2251
+
2252
+ stream: {stream}
2253
+
2254
+ release_workspace: A value of `True` specifies that the FFT object
2255
+ should release workspace memory back to the symmetric memory pool on
2256
+ function return, while a value of `False` specifies that the object
2257
+ should retain the memory. This option may be set to `True` if the
2258
+ application performs other operations that consume a lot of memory between
2259
+ successive calls to the (same or different) :meth:`execute` API, but incurs
2260
+ an overhead due to obtaining and releasing workspace memory from and
2261
+ to the symmetric memory pool on every call. The default is `False`.
2262
+ **NOTE: All processes must use the same value or the application can
2263
+ deadlock.**
2264
+
2265
+ sync_symmetric_memory: {sync_symmetric_memory}
2266
+
2267
+ Returns:
2268
+ The transformed operand, which remains on the same device and utilizes the same
2269
+ package as the input operand. The data type and shape of the transformed operand
2270
+ depend on the type of input operand, and choice of distribution and redistribute
2271
+ option:
2272
+
2273
+ - For C2C FFT, the data type remains identical to the input.
2274
+ - For R2C and C2R FFT, the data type differs from the input. The global output
2275
+ shape differs from the global input shape, which affects the shape of the
2276
+ result on every process.
2277
+ - For slab distribution with redistribute=True, the shape on this process is the
2278
+ slab shape according to the same distribution as the input operand.
2279
+ - For slab distribution with redistribute=False, the shape on this process is
2280
+ the complementary slab shape.
2281
+ - For custom box distribution, the shape will depend on the output box of
2282
+ each process.
2283
+
2284
+ For GPU operands, the result will be in symmetric memory and the user is
2285
+ responsible for explicitly deallocating it (for example, using
2286
+ ``nvmath.distributed.free_symmetric_memory(tensor)``).
2287
+ """
2288
+
2289
+ log_info = self.logger.isEnabledFor(logging.INFO)
2290
+ log_debug = self.logger.isEnabledFor(logging.DEBUG)
2291
+
2292
+ if direction is None:
2293
+ direction = _get_fft_default_direction(self.fft_abstract_type)
2294
+ else:
2295
+ direction = _get_validate_direction(direction, self.fft_abstract_type)
2296
+
2297
+ stream_holder: StreamHolder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
2298
+
2299
+ # Set stream for the FFT.
2300
+ with utils.device_ctx(self.device_id):
2301
+ cufft.set_stream(self.handle, stream_holder.ptr)
2302
+
2303
+ # Allocate workspace if needed.
2304
+ self._allocate_workspace_memory_perhaps(stream_holder)
2305
+ # cuFFTMp only supports inplace transform.
2306
+ result_ptr = self.operand.data_ptr
2307
+
2308
+ if log_info:
2309
+ self.logger.info(
2310
+ f"Starting distributed FFT {self.fft_abstract_type} calculation in the {direction.name} direction..." # type: ignore[union-attr]
2311
+ )
2312
+ self.logger.info(f"{self.call_prologue}")
2313
+
2314
+ with utils.cuda_call_ctx(stream_holder, self.blocking, timing=log_info) as (
2315
+ self.last_compute_event,
2316
+ elapsed,
2317
+ ):
2318
+ if log_debug:
2319
+ self.logger.debug("The cuFFTMp execution function is 'xt_exec_descriptor'.")
2320
+ if sync_symmetric_memory:
2321
+ nvshmem.sync_all_on_stream(stream_holder.ptr)
2322
+ if log_info:
2323
+ self.logger.info(
2324
+ "sync_symmetric_memory is enabled (this may incur redundant multi-GPU "
2325
+ "synchronization, please refer to the documentation for more information)"
2326
+ )
2327
+ elif log_info:
2328
+ self.logger.info("sync_symmetric_memory is disabled")
2329
+ cufft.set_descriptor_data(self.memory_desc, result_ptr, self.subformat)
2330
+ cufft.xt_exec_descriptor(self.handle, self.memory_desc, self.memory_desc, direction)
2331
+ if self.options.redistribute:
2332
+ raw_workspace_ptr = utils.get_ptr_from_memory_pointer(self.workspace_ptr)
2333
+ assert self.reshaped_operand is not None
2334
+ cufft.exec_reshape_async(
2335
+ self.reshape_handle, self.reshaped_operand.data_ptr, result_ptr, raw_workspace_ptr, stream_holder.ptr
2336
+ )
2337
+ # Copy back to original GPU operand.
2338
+ self.result_operand.copy_(self.reshaped_operand, stream_holder=stream_holder)
2339
+
2340
+ if log_info and elapsed.data is not None:
2341
+ reshape_addendum = "along with output redistribution" if self.options.redistribute else ""
2342
+ self.logger.info(f"The distributed FFT calculation {reshape_addendum} took {elapsed.data:.3f} ms to complete.")
2343
+
2344
+ # Establish ordering wrt the computation and free workspace if it's more than the
2345
+ # specified cache limit.
2346
+ self._free_workspace_memory_perhaps(release_workspace)
2347
+
2348
+ # reset workspace allocation tracking to False at the end of the methods where
2349
+ # workspace memory is potentially allocated. This is necessary to prevent any
2350
+ # exceptions raised before method entry from using stale tracking values.
2351
+ self._workspace_allocated_here = False
2352
+
2353
+ # Return the result.
2354
+ if self.memory_space == self.execution_space:
2355
+ out = self.result_operand
2356
+ else:
2357
+ self.cpu_result_operand.copy_(self.result_operand, stream_holder=stream_holder)
2358
+ out = self.cpu_result_operand
2359
+ return out.tensor
2360
+
2361
+ def free(self):
2362
+ """Free FFT resources.
2363
+
2364
+ It is recommended that the :class:`FFT` object be used within a context, but if it
2365
+ is not possible then this method must be called explicitly to ensure that the FFT
2366
+ resources (especially internal library objects) are properly cleaned up.
2367
+ """
2368
+
2369
+ if not self.valid_state:
2370
+ return
2371
+
2372
+ try:
2373
+ # Future operations on the workspace stream should be ordered after the
2374
+ # computation.
2375
+ if self.last_compute_event is not None and self.workspace_stream is not None:
2376
+ with utils.device_ctx(self.device_id):
2377
+ self.workspace_stream.wait(self.last_compute_event)
2378
+ self.last_compute_event = None
2379
+
2380
+ self._free_workspace_memory()
2381
+
2382
+ with utils.device_ctx(self.device_id):
2383
+ if self.memory_desc is not None:
2384
+ if self.dummy_desc_data_ptr is not None:
2385
+ cufft.set_descriptor_data(self.memory_desc, self.dummy_desc_data_ptr, self.subformat)
2386
+ cufft.xt_free(self.memory_desc)
2387
+ self.memory_desc = None
2388
+
2389
+ if self.handle is not None:
2390
+ cufft.destroy(self.handle)
2391
+ if self.options.redistribute:
2392
+ cufft.destroy_reshape(self.reshape_handle)
2393
+ self.handle = None
2394
+ self.reshape_handle = None
2395
+
2396
+ if self.memory_desc_handle is not None:
2397
+ cufft.destroy(self.memory_desc_handle)
2398
+ self.memory_desc_handle = None
2399
+
2400
+ if self.memory_space == "cpu" and not self._operand_released:
2401
+ # In this case, self.operand is an internal GPU operand owned by FFT.
2402
+ # Since the execution when user passes CPU operands is blocking, it's
2403
+ # safe to call nvshmem_free here without additional synchronization.
2404
+ # If _operand_released is True, release_operand() already freed it.
2405
+ self.operand.free_symmetric()
2406
+
2407
+ # Set all attributes to None except for logger and valid_state
2408
+ _keep = {"logger", "valid_state"}
2409
+ for attr in list(vars(self)):
2410
+ if attr not in _keep:
2411
+ setattr(self, attr, None)
2412
+
2413
+ except Exception as e:
2414
+ self.logger.critical("Internal error: only part of the FFT object's resources have been released.")
2415
+ self.logger.critical(str(e))
2416
+ raise e
2417
+ finally:
2418
+ self.valid_state = False
2419
+
2420
+ self.logger.info("The FFT object's resources have been released.")
2421
+
2422
+
2423
+ def _fft(
2424
+ operand,
2425
+ /,
2426
+ *,
2427
+ distribution: Distribution | Sequence[Box],
2428
+ direction: FFTDirection | None = None,
2429
+ sync_symmetric_memory: bool = True,
2430
+ options: FFTOptions | dict[str, Any] | None = None,
2431
+ stream: AnyStream | None = None,
2432
+ check_dtype: str | None = None,
2433
+ ):
2434
+ r"""
2435
+ fft({function_signature})
2436
+
2437
+ Perform an N-D *complex-to-complex* (C2C) distributed FFT on the provided complex
2438
+ operand.
2439
+
2440
+ Args:
2441
+ operand: {operand}
2442
+ {operand_admonitions}
2443
+
2444
+ distribution: {distribution}
2445
+
2446
+ sync_symmetric_memory: {sync_symmetric_memory}
2447
+
2448
+ options: {options}
2449
+
2450
+ stream: {stream}
2451
+
2452
+ Returns:
2453
+ A transformed operand that retains the same data type as the input. The resulting
2454
+ shape will depend on the choice of distribution and redistribute option. The operand
2455
+ remains on the same device and uses the same package as the input operand.
2456
+
2457
+ .. seealso::
2458
+ :func:`ifft`, :func:`irfft`, :func:`rfft`, :class:`FFT`
2459
+
2460
+ Examples:
2461
+
2462
+ >>> import cupy as cp
2463
+ >>> import nvmath.distributed
2464
+
2465
+ Get process group used to initialize nvmath.distributed (for information on
2466
+ initializing nvmath.distributed, you can refer to the documentation or to the
2467
+ FFT examples in `nvmath/examples/distributed/fft
2468
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/fft>`_):
2469
+
2470
+ >>> process_group = nvmath.distributed.get_context().process_group
2471
+ >>> nranks = process_group.nranks
2472
+
2473
+ Create a 3-D complex128 ndarray on GPU symmetric memory, distributed according to
2474
+ the Slab distribution on the Y axis (the global shape is (256, 256, 256)):
2475
+
2476
+ >>> from nvmath.distributed.distribution import Slab
2477
+ >>> shape = 256, 256 // nranks, 256
2478
+ >>> dtype = cp.complex128
2479
+ >>> a = nvmath.distributed.allocate_symmetric_memory(shape, cp, dtype=dtype)
2480
+ >>> a[:] = cp.random.rand(*shape, dtype=cp.float64) + 1j * cp.random.rand(
2481
+ ... *shape, dtype=cp.float64
2482
+ ... )
2483
+
2484
+ Perform a 3-D C2C FFT using :func:`fft`. The result `r` is also a CuPy complex128
2485
+ ndarray:
2486
+
2487
+ >>> r = nvmath.distributed.fft.fft(a, distribution=Slab.Y)
2488
+
2489
+ See :class:`FFTOptions` for the complete list of available options.
2490
+
2491
+ The package current stream is used by default, but a stream can be explicitly
2492
+ provided to the FFT operation. This can be done if the FFT operand is computed on a
2493
+ different stream, for example:
2494
+
2495
+ >>> s = cp.cuda.Stream()
2496
+ >>> with s:
2497
+ ... a = nvmath.distributed.allocate_symmetric_memory(shape, cp, dtype=dtype)
2498
+ ... a[:] = cp.random.rand(*shape) + 1j * cp.random.rand(*shape)
2499
+ >>> r = nvmath.distributed.fft.fft(a, stream=s)
2500
+
2501
+ The operation above runs on stream `s` and is ordered with respect to the input
2502
+ computation.
2503
+
2504
+ Create a NumPy ndarray on the CPU.
2505
+
2506
+ >>> import numpy as np
2507
+ >>> b = np.random.rand(*shape) + 1j * np.random.rand(*shape)
2508
+
2509
+ Provide the NumPy ndarray to :func:`fft`, with the result also being a NumPy
2510
+ ndarray:
2511
+
2512
+ >>> r = nvmath.distributed.fft.fft(b, distribution=Slab.Y)
2513
+
2514
+ Notes:
2515
+ - This function only takes complex operand for C2C transformation. If the user
2516
+ wishes to perform full FFT transformation on real input, please cast the input to
2517
+ the corresponding complex data type.
2518
+ - This function is a convenience wrapper around :class:`FFT` and is specifically
2519
+ meant for *single* use. The same computation can be performed with the stateful
2520
+ API using the default `direction` argument in :meth:`FFT.execute`.
2521
+
2522
+ Further examples can be found in the `nvmath/examples/distributed/fft
2523
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/fft>`_
2524
+ directory.
2525
+ """
2526
+ if check_dtype is not None:
2527
+ assert check_dtype in {"real", "complex"}, "internal error"
2528
+ wrapped = tensor_wrapper.wrap_operand(operand)
2529
+ if ("complex" in wrapped.dtype) != (check_dtype == "complex"):
2530
+ raise ValueError(f"This function expects {check_dtype} operand, found {wrapped.dtype}")
2531
+
2532
+ with FFT(operand, distribution=distribution, options=options, stream=stream) as fftobj:
2533
+ # Plan the FFT.
2534
+ fftobj.plan(stream=stream)
2535
+
2536
+ # Execute the FFT.
2537
+ result = fftobj.execute(direction=direction, stream=stream, sync_symmetric_memory=sync_symmetric_memory)
2538
+
2539
+ return result
2540
+
2541
+
2542
+ # Forward C2C FFT Function.
2543
+ fft = functools.wraps(_fft)(functools.partial(_fft, direction=FFTDirection.FORWARD, check_dtype="complex"))
2544
+ if fft.__doc__ is not None:
2545
+ fft.__doc__ = fft.__doc__.format(**SHARED_FFT_DOCUMENTATION) # type: ignore
2546
+ fft.__name__ = "fft"
2547
+
2548
+
2549
+ # Forward R2C FFT Function
2550
+ @utils.docstring_decorator(SHARED_FFT_DOCUMENTATION, skip_missing=False)
2551
+ def rfft(
2552
+ operand,
2553
+ /,
2554
+ *,
2555
+ distribution: Distribution | Sequence[Box],
2556
+ sync_symmetric_memory: bool = True,
2557
+ options: FFTOptions | dict[str, Any] | None = None,
2558
+ stream: AnyStream | None = None,
2559
+ ):
2560
+ r"""
2561
+ rfft({function_signature})
2562
+
2563
+ Perform an N-D *real-to-complex* (R2C) distributed FFT on the provided real operand.
2564
+
2565
+ Args:
2566
+ operand: {operand}
2567
+ {operand_admonitions}
2568
+
2569
+ distribution: {distribution}
2570
+
2571
+ sync_symmetric_memory: {sync_symmetric_memory}
2572
+
2573
+ options: {options}
2574
+
2575
+ stream: {stream}
2576
+
2577
+ Returns:
2578
+ A complex tensor whose shape will depend on the choice of distribution and
2579
+ redistribute option. The operand remains on the same device and belongs to the
2580
+ same package as the input operand. The global extent of the last transformed axis
2581
+ in the result will be ``global_extent[-1] // 2 + 1``.
2582
+
2583
+ .. seealso::
2584
+ :func:`fft`, :func:`irfft`, :class:`FFT`.
2585
+ """
2586
+ wrapped_operand = tensor_wrapper.wrap_operand(operand)
2587
+ # check if input operand if real type
2588
+ if "complex" in wrapped_operand.dtype:
2589
+ raise RuntimeError(f"rfft expects a real input, but got {wrapped_operand.dtype}. Please use fft for complex input.")
2590
+
2591
+ return _fft(
2592
+ operand,
2593
+ distribution=distribution,
2594
+ sync_symmetric_memory=sync_symmetric_memory,
2595
+ options=options,
2596
+ stream=stream,
2597
+ check_dtype="real",
2598
+ )
2599
+
2600
+
2601
+ # Inverse C2C FFT Function.
2602
+ ifft = functools.wraps(_fft)(functools.partial(_fft, direction=FFTDirection.INVERSE, check_dtype="complex"))
2603
+ ifft.__doc__ = """
2604
+ ifft({function_signature})
2605
+
2606
+ Perform an N-D *complex-to-complex* (C2C) inverse FFT on the provided complex operand.
2607
+ The direction is implicitly inverse.
2608
+
2609
+ Args:
2610
+ operand: {operand}
2611
+ {operand_admonitions}
2612
+
2613
+ distribution: {distribution}
2614
+
2615
+ sync_symmetric_memory: {sync_symmetric_memory}
2616
+
2617
+ options: {options}
2618
+
2619
+ stream: {stream}
2620
+
2621
+ Returns:
2622
+ A transformed operand that retains the same data type as the input. The resulting
2623
+ shape will depend on the choice of distribution and redistribute option. The operand
2624
+ remains on the same device and uses the same package as the input operand.
2625
+
2626
+ .. seealso::
2627
+ :func:`fft`, :func:`irfft`, :class:`FFT`.
2628
+
2629
+ Notes:
2630
+ - This function only takes complex operand for C2C transformation. If the user wishes
2631
+ to perform full FFT transformation on real input, please cast the input to the
2632
+ corresponding complex data type.
2633
+ - This function is a convenience wrapper around :class:`FFT` and is specifically
2634
+ meant for *single* use. The same computation can be performed with the stateful
2635
+ API by passing the argument ``direction='inverse'`` when calling
2636
+ :meth:`FFT.execute`.
2637
+ """.format(**SHARED_FFT_DOCUMENTATION)
2638
+ ifft.__name__ = "ifft"
2639
+
2640
+
2641
+ # Inverse C2R FFT Function.
2642
+ @utils.docstring_decorator(SHARED_FFT_DOCUMENTATION, skip_missing=False)
2643
+ def irfft(
2644
+ operand,
2645
+ /,
2646
+ *,
2647
+ distribution: Distribution | Sequence[Box],
2648
+ sync_symmetric_memory: bool = True,
2649
+ options: FFTOptions | dict[str, Any] | None = None,
2650
+ stream: AnyStream | None = None,
2651
+ ):
2652
+ """
2653
+ irfft({function_signature})
2654
+
2655
+ Perform an N-D *complex-to-real* (C2R) distributed FFT on the provided complex operand.
2656
+ The direction is implicitly inverse.
2657
+
2658
+ Args:
2659
+ operand: {operand}
2660
+ {operand_admonitions}
2661
+
2662
+ distribution: {distribution}
2663
+
2664
+ sync_symmetric_memory: {sync_symmetric_memory}
2665
+
2666
+ options: {options}
2667
+
2668
+ stream: {stream}
2669
+
2670
+ Returns:
2671
+ A real tensor whose shape will depend on the choice of distribution and redistribute
2672
+ option. The operand remains on the same device and belongs to the same package as
2673
+ the input operand. The global extent of the last transformed axis in the result
2674
+ will be ``(global_extent[-1] - 1) * 2`` if :attr:`FFTOptions.last_axis_parity` is
2675
+ ``even``, or ``global_extent[-1] * 2 - 1`` if :attr:`FFTOptions.last_axis_parity`
2676
+ is ``odd``.
2677
+
2678
+ .. seealso::
2679
+ :func:`fft`, :func:`ifft`, :class:`FFT`.
2680
+
2681
+ Example:
2682
+
2683
+ >>> import cupy as cp
2684
+ >>> import nvmath.distributed
2685
+
2686
+ Get process group used to initialize nvmath.distributed (for information on
2687
+ initializing nvmath.distributed, you can refer to the documentation or to the
2688
+ FFT examples in `nvmath/examples/distributed/fft
2689
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/fft>`_):
2690
+
2691
+ >>> process_group = nvmath.distributed.get_context().process_group
2692
+ >>> nranks = process_group.nranks
2693
+ >>> from nvmath.distributed.fft import Slab
2694
+
2695
+ Create a 3-D symmetric complex128 ndarray on GPU symmetric memory:
2696
+
2697
+ >>> shape = 512 // nranks, 768, 256
2698
+ >>> a = nvmath.distributed.allocate_operand(
2699
+ ... shape, cp, input_dtype=cp.float64, distribution=Slab.X, fft_type="R2C"
2700
+ ... )
2701
+ >>> a[:] = cp.random.rand(*shape, dtype=cp.float64)
2702
+ >>> b = nvmath.distributed.fft.rfft(a, distribution=Slab.X)
2703
+
2704
+ Perform a 3-D C2R FFT using the :func:`irfft` wrapper. The result `r` is a CuPy
2705
+ float64 ndarray:
2706
+
2707
+ >>> r = nvmath.distributed.fft.irfft(b, distribution=Slab.X)
2708
+ >>> r.dtype
2709
+ dtype('float64')
2710
+
2711
+ Notes:
2712
+
2713
+ - This function performs an inverse C2R N-D FFT, which is similar to `irfftn` but
2714
+ different from `irfft` in various numerical packages.
2715
+ - This function is a convenience wrapper around :class:`FFT` and is specifically
2716
+ meant for *single* use. The same computation can be performed with the stateful
2717
+ API by setting :attr:`FFTOptions.fft_type` to ``'C2R'`` and passing the argument
2718
+ ``direction='inverse'`` when calling :meth:`FFT.execute`.
2719
+ - **The input to this function must be Hermitian-symmetric, otherwise the result is
2720
+ undefined.** While the symmetry requirement is partially captured by the different
2721
+ global extents in the last transformed dimension between the input and result,
2722
+ there are additional `constraints
2723
+ <https://docs.nvidia.com/cuda/cufft/#fourier-transform-types>`_. In addition,
2724
+ if the input to `irfft` was generated using an R2C FFT with an odd global last
2725
+ axis size, :attr:`FFTOptions.last_axis_parity` must be set to ``odd`` to recover
2726
+ the original signal.
2727
+ - For more details, please refer to `R2C/C2R example
2728
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/fft/example01_cupy_r2c_c2r.py>`_
2729
+ and `odd C2R example
2730
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/fft/example01_torch_r2c_c2r.py>`_.
2731
+ """
2732
+ options = cast(FFTOptions, utils.check_or_create_options(FFTOptions, options, "Distributed FFT options"))
2733
+ options.fft_type = "C2R"
2734
+ return _fft(
2735
+ operand,
2736
+ distribution=distribution,
2737
+ direction=FFTDirection.INVERSE,
2738
+ sync_symmetric_memory=sync_symmetric_memory,
2739
+ options=options,
2740
+ stream=stream,
2741
+ check_dtype="complex",
2742
+ )