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