nvmath-python 0.5.0__cp313-cp313-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 (174) hide show
  1. nvmath/__init__.pxd +0 -0
  2. nvmath/__init__.py +37 -0
  3. nvmath/_internal/__init__.py +0 -0
  4. nvmath/_internal/layout.py +58 -0
  5. nvmath/_utils.py +363 -0
  6. nvmath/bindings/__init__.pxd +0 -0
  7. nvmath/bindings/__init__.py +44 -0
  8. nvmath/bindings/_internal/__init__.pxd +0 -0
  9. nvmath/bindings/_internal/__init__.py +0 -0
  10. nvmath/bindings/_internal/cublas.cp313-win_amd64.pyd +0 -0
  11. nvmath/bindings/_internal/cublas.pxd +518 -0
  12. nvmath/bindings/_internal/cublasLt.cp313-win_amd64.pyd +0 -0
  13. nvmath/bindings/_internal/cublasLt.pxd +54 -0
  14. nvmath/bindings/_internal/cudss.cp313-win_amd64.pyd +0 -0
  15. nvmath/bindings/_internal/cudss.pxd +44 -0
  16. nvmath/bindings/_internal/cufft.cp313-win_amd64.pyd +0 -0
  17. nvmath/bindings/_internal/cufft.pxd +69 -0
  18. nvmath/bindings/_internal/cufftMp.pxd +77 -0
  19. nvmath/bindings/_internal/curand.cp313-win_amd64.pyd +0 -0
  20. nvmath/bindings/_internal/curand.pxd +42 -0
  21. nvmath/bindings/_internal/cusolver.cp313-win_amd64.pyd +0 -0
  22. nvmath/bindings/_internal/cusolver.pxd +15 -0
  23. nvmath/bindings/_internal/cusolverDn.cp313-win_amd64.pyd +0 -0
  24. nvmath/bindings/_internal/cusolverDn.pxd +386 -0
  25. nvmath/bindings/_internal/cusparse.cp313-win_amd64.pyd +0 -0
  26. nvmath/bindings/_internal/cusparse.pxd +270 -0
  27. nvmath/bindings/_internal/mathdx.cp313-win_amd64.pyd +0 -0
  28. nvmath/bindings/_internal/mathdx.pxd +79 -0
  29. nvmath/bindings/_internal/nvshmem.pxd +29 -0
  30. nvmath/bindings/_internal/utils.cp313-win_amd64.pyd +0 -0
  31. nvmath/bindings/_internal/utils.pxd +186 -0
  32. nvmath/bindings/cublas.cp313-win_amd64.pyd +0 -0
  33. nvmath/bindings/cublas.pxd +545 -0
  34. nvmath/bindings/cublas.pyi +18 -0
  35. nvmath/bindings/cublasLt.cp313-win_amd64.pyd +0 -0
  36. nvmath/bindings/cublasLt.pxd +99 -0
  37. nvmath/bindings/cudss.cp313-win_amd64.pyd +0 -0
  38. nvmath/bindings/cudss.pxd +83 -0
  39. nvmath/bindings/cudss.pyi +249 -0
  40. nvmath/bindings/cufft.cp313-win_amd64.pyd +0 -0
  41. nvmath/bindings/cufft.pxd +114 -0
  42. nvmath/bindings/cufftMp.pxd +124 -0
  43. nvmath/bindings/curand.cp313-win_amd64.pyd +0 -0
  44. nvmath/bindings/curand.pxd +71 -0
  45. nvmath/bindings/cusolver.cp313-win_amd64.pyd +0 -0
  46. nvmath/bindings/cusolver.pxd +60 -0
  47. nvmath/bindings/cusolverDn.cp313-win_amd64.pyd +0 -0
  48. nvmath/bindings/cusolverDn.pxd +410 -0
  49. nvmath/bindings/cusparse.cp313-win_amd64.pyd +0 -0
  50. nvmath/bindings/cusparse.pxd +329 -0
  51. nvmath/bindings/cycublas.cp313-win_amd64.pyd +0 -0
  52. nvmath/bindings/cycublas.pxd +657 -0
  53. nvmath/bindings/cycublasLt.cp313-win_amd64.pyd +0 -0
  54. nvmath/bindings/cycublasLt.pxd +1010 -0
  55. nvmath/bindings/cycudss.cp313-win_amd64.pyd +0 -0
  56. nvmath/bindings/cycudss.pxd +191 -0
  57. nvmath/bindings/cycufft.cp313-win_amd64.pyd +0 -0
  58. nvmath/bindings/cycufft.pxd +320 -0
  59. nvmath/bindings/cycufftMp.pxd +333 -0
  60. nvmath/bindings/cycurand.cp313-win_amd64.pyd +0 -0
  61. nvmath/bindings/cycurand.pxd +146 -0
  62. nvmath/bindings/cycusolver.cp313-win_amd64.pyd +0 -0
  63. nvmath/bindings/cycusolver.pxd +143 -0
  64. nvmath/bindings/cycusolverDn.cp313-win_amd64.pyd +0 -0
  65. nvmath/bindings/cycusolverDn.pxd +414 -0
  66. nvmath/bindings/cycusparse.cp313-win_amd64.pyd +0 -0
  67. nvmath/bindings/cycusparse.pxd +476 -0
  68. nvmath/bindings/cymathdx.cp313-win_amd64.pyd +0 -0
  69. nvmath/bindings/cymathdx.pxd +363 -0
  70. nvmath/bindings/cynvshmem.pxd +126 -0
  71. nvmath/bindings/mathdx.cp313-win_amd64.pyd +0 -0
  72. nvmath/bindings/mathdx.pxd +131 -0
  73. nvmath/bindings/mathdx.pyi +686 -0
  74. nvmath/bindings/nvpl/__init__.pxd +0 -0
  75. nvmath/bindings/nvpl/__init__.py +11 -0
  76. nvmath/bindings/nvpl/_internal/__init__.pxd +0 -0
  77. nvmath/bindings/nvpl/_internal/__init__.py +0 -0
  78. nvmath/bindings/nvpl/_internal/fft.pxd +36 -0
  79. nvmath/bindings/nvpl/cyfft.pxd +87 -0
  80. nvmath/bindings/nvpl/fft.pxd +100 -0
  81. nvmath/bindings/nvshmem.pxd +54 -0
  82. nvmath/device/__init__.py +23 -0
  83. nvmath/device/_deprecated.py +33 -0
  84. nvmath/device/caching.py +51 -0
  85. nvmath/device/common.py +131 -0
  86. nvmath/device/common_backend.py +125 -0
  87. nvmath/device/common_cuda.py +156 -0
  88. nvmath/device/common_mathdx.py +97 -0
  89. nvmath/device/common_numba.py +159 -0
  90. nvmath/device/common_opaque_tensor.py +128 -0
  91. nvmath/device/cublasdx.py +1085 -0
  92. nvmath/device/cublasdx_backend.py +552 -0
  93. nvmath/device/cublasdx_numba.py +528 -0
  94. nvmath/device/cufftdx.py +551 -0
  95. nvmath/device/cufftdx_backend.py +194 -0
  96. nvmath/device/cufftdx_numba.py +123 -0
  97. nvmath/device/curand_kernel.py +9147 -0
  98. nvmath/device/nvrtc.py +95 -0
  99. nvmath/device/patch.py +35 -0
  100. nvmath/device/random.py +417 -0
  101. nvmath/device/random_helpers.py +23 -0
  102. nvmath/device/random_states.py +189 -0
  103. nvmath/device/types.py +11 -0
  104. nvmath/device/vector_types_numba.py +203 -0
  105. nvmath/distributed/__init__.py +117 -0
  106. nvmath/distributed/_internal/__init__.py +0 -0
  107. nvmath/distributed/_internal/nvshmem.py +243 -0
  108. nvmath/distributed/_internal/tensor_ifc_cupy.py +111 -0
  109. nvmath/distributed/_internal/tensor_ifc_numpy.py +53 -0
  110. nvmath/distributed/_internal/tensor_ifc_torch.py +108 -0
  111. nvmath/distributed/_internal/tensor_wrapper.py +59 -0
  112. nvmath/distributed/_utils.py +152 -0
  113. nvmath/distributed/fft/__init__.py +6 -0
  114. nvmath/distributed/fft/_configuration.py +86 -0
  115. nvmath/distributed/fft/fft.py +1737 -0
  116. nvmath/distributed/reshape/__init__.py +6 -0
  117. nvmath/distributed/reshape/_configuration.py +39 -0
  118. nvmath/distributed/reshape/reshape.py +1249 -0
  119. nvmath/fft/__init__.py +7 -0
  120. nvmath/fft/_configuration.py +208 -0
  121. nvmath/fft/_exec_utils.py +116 -0
  122. nvmath/fft/_helpers.py +260 -0
  123. nvmath/fft/fft.py +2346 -0
  124. nvmath/internal/__init__.py +5 -0
  125. nvmath/internal/enum_utils.py +142 -0
  126. nvmath/internal/formatters.py +87 -0
  127. nvmath/internal/mem_limit.py +51 -0
  128. nvmath/internal/package_ifc.py +119 -0
  129. nvmath/internal/package_ifc_cuda.py +43 -0
  130. nvmath/internal/package_ifc_cupy.py +49 -0
  131. nvmath/internal/package_ifc_torch.py +31 -0
  132. nvmath/internal/package_wrapper.py +14 -0
  133. nvmath/internal/tensor_ifc.py +143 -0
  134. nvmath/internal/tensor_ifc_cupy.py +185 -0
  135. nvmath/internal/tensor_ifc_numpy.py +157 -0
  136. nvmath/internal/tensor_ifc_torch.py +115 -0
  137. nvmath/internal/tensor_wrapper.py +160 -0
  138. nvmath/internal/typemaps.py +154 -0
  139. nvmath/internal/utils.py +634 -0
  140. nvmath/linalg/__init__.py +11 -0
  141. nvmath/linalg/_internal/__init__.py +3 -0
  142. nvmath/linalg/_internal/algo_cap_ifc.py +86 -0
  143. nvmath/linalg/_internal/algo_config_ifc.py +87 -0
  144. nvmath/linalg/_internal/enum_to_tuples.py +64 -0
  145. nvmath/linalg/_internal/epilog_protocol.py +738 -0
  146. nvmath/linalg/_internal/matmul_desc_ifc.py +72 -0
  147. nvmath/linalg/_internal/matmul_pref_ifc.py +65 -0
  148. nvmath/linalg/_internal/matrix_layout_ifc.py +59 -0
  149. nvmath/linalg/_internal/typemaps.py +135 -0
  150. nvmath/linalg/_internal/utils.py +96 -0
  151. nvmath/linalg/advanced/__init__.py +8 -0
  152. nvmath/linalg/advanced/_algorithmmod.py +142 -0
  153. nvmath/linalg/advanced/_configuration.py +320 -0
  154. nvmath/linalg/advanced/helpers/__init__.py +5 -0
  155. nvmath/linalg/advanced/helpers/matmul.py +173 -0
  156. nvmath/linalg/advanced/matmulmod.py +2855 -0
  157. nvmath/memory.py +266 -0
  158. nvmath/sparse/__init__.py +18 -0
  159. nvmath/sparse/_internal/__init__.py +21 -0
  160. nvmath/sparse/_internal/common_utils.py +81 -0
  161. nvmath/sparse/_internal/cudss_config_ifc.py +584 -0
  162. nvmath/sparse/_internal/cudss_data_ifc.py +291 -0
  163. nvmath/sparse/_internal/cudss_utils.py +398 -0
  164. nvmath/sparse/_internal/sparse_csr_ifc.py +154 -0
  165. nvmath/sparse/_internal/sparse_format_helpers.py +88 -0
  166. nvmath/sparse/_internal/sparse_tensor_ifc.py +80 -0
  167. nvmath/sparse/advanced/__init__.py +7 -0
  168. nvmath/sparse/advanced/_configuration.py +150 -0
  169. nvmath/sparse/advanced/direct_solver.py +1705 -0
  170. nvmath_python-0.5.0.dist-info/METADATA +122 -0
  171. nvmath_python-0.5.0.dist-info/RECORD +174 -0
  172. nvmath_python-0.5.0.dist-info/WHEEL +5 -0
  173. nvmath_python-0.5.0.dist-info/licenses/LICENSE +177 -0
  174. nvmath_python-0.5.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1737 @@
1
+ # Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ __all__ = ["FFT", "fft", "ifft"]
6
+
7
+ from typing import Literal, cast, TYPE_CHECKING
8
+ from collections.abc import Sequence
9
+ from dataclasses import dataclass
10
+ import functools
11
+ import logging
12
+ import math
13
+ import numpy as np
14
+
15
+ from ._configuration import FFTOptions, Slab, FFTDirection
16
+
17
+ import nvmath.distributed
18
+ from nvmath.bindings import cufftMp as cufft # type: ignore
19
+ from nvmath.bindings import nvshmem # type: ignore
20
+ from nvmath import memory
21
+ from nvmath.distributed._internal.nvshmem import NvshmemMemoryManager
22
+ from nvmath.distributed._internal.nvshmem import free as nvshmem_free_wrapper
23
+
24
+ if TYPE_CHECKING:
25
+ from nvmath.distributed._internal.tensor_ifc_cupy import CupyDistributedTensor
26
+ from nvmath.distributed._internal.tensor_ifc_torch import TorchDistributedTensor
27
+
28
+ from nvmath.internal import formatters
29
+ from nvmath.distributed._internal import tensor_wrapper
30
+ from nvmath.internal.typemaps import (
31
+ NAME_TO_DATA_TYPE,
32
+ )
33
+ from nvmath.internal import utils
34
+ from nvmath._internal.layout import is_overlapping_layout
35
+ from nvmath.internal.package_wrapper import AnyStream, StreamHolder
36
+ from nvmath.internal.tensor_wrapper import maybe_register_package
37
+
38
+
39
+ @dataclass
40
+ class TensorLayout:
41
+ """An internal data class for capturing the tensor layout."""
42
+
43
+ shape: Sequence[int]
44
+ strides: Sequence[int]
45
+
46
+
47
+ # A box contains lower and upper coordinates, so it must be of length 2 in practice.
48
+ Box = Sequence[Sequence[int]]
49
+
50
+
51
+ @dataclass
52
+ class _ProblemSpec:
53
+ """This is used in a custom MPI reduction to check that the FFT problem
54
+ specification is consistent across processes, and to infer global information
55
+ (e.g shape)."""
56
+
57
+ @dataclass
58
+ class Options:
59
+ """
60
+ This is used for _ProblemSpec instead of FFTOptions
61
+ because it's going to be serialized as part of the custom reduction of the
62
+ _ProblemSpec, and we want to control which fields are included (for example
63
+ we don't need the logger).
64
+ """
65
+
66
+ def __init__(self, options: FFTOptions):
67
+ self.fft_type = options.fft_type
68
+ self.reshape = options.reshape
69
+ self.blocking = options.blocking
70
+
71
+ fft_type: Literal["C2C"] | None
72
+ reshape: bool
73
+ blocking: Literal[True, "auto"]
74
+
75
+ shape: list[int] # operand shape
76
+ is_C: bool # Is C memory layout
77
+ operand_dtype: str # str because TensorHolder.dtype returns str
78
+ package: Literal["numpy", "cupy", "torch"] # operand package
79
+ memory_space: Literal["cuda", "cpu"] # operand memory space
80
+ distribution: Slab | Sequence[Box] # distribution of FFT input/output operands
81
+ options: Options # FFT options
82
+
83
+ # Global number of elements in the operand (calculated as part of the reduction).
84
+ global_size: int = 0
85
+ # is_leaf=True means that this is the _ProblemSpec of a process before reducing
86
+ # with that of another process.
87
+ is_leaf: bool = True
88
+
89
+
90
+ SHARED_FFT_DOCUMENTATION = utils.COMMON_SHARED_DOC_MAP.copy()
91
+ SHARED_FFT_DOCUMENTATION.update(
92
+ {
93
+ "operand": SHARED_FFT_DOCUMENTATION["operand"],
94
+ #
95
+ "operand_admonitions": """
96
+ .. important::
97
+ GPU operands must be on the symmetric heap (for example, allocated with
98
+ ``nvmath.distributed.allocate_symmetric_memory()``).
99
+ """,
100
+ #
101
+ "options": """\
102
+ Specify options for the FFT as a :class:`FFTOptions` object. Alternatively, a `dict` containing the parameters for the
103
+ ``FFTOptions`` constructor can also be provided. If not specified, the value will be set to the default-constructed
104
+ ``FFTOptions`` object.""".replace("\n", " "),
105
+ #
106
+ "distribution": """\
107
+ Specifies the distribution of input and output operands across processes, which can be: (i) according to
108
+ a Slab distribution (see :class:`Slab`), or (ii) a custom box distribution. With Slab distribution,
109
+ this indicates the distribution of the input operand (the output operand will use the complementary
110
+ Slab distribution). With box distribution, this indicates the input and output boxes.""".replace("\n", " "),
111
+ #
112
+ "direction": """\
113
+ Specify whether forward or inverse FFT is performed (:class:`FFTDirection` object, or as a string from ['forward',
114
+ 'inverse'], "or as an int from [-1, 1] denoting forward and inverse directions respectively).""".replace("\n", " "),
115
+ #
116
+ "sync_symmetric_memory": """\
117
+ Indicates whether to issue a symmetric memory synchronization operation on the execute stream
118
+ before the FFT. Note that before the FFT starts executing, it is required that the input operand
119
+ be ready on all processes. A symmetric memory synchronization ensures completion and visibility
120
+ by all processes of previously issued local stores to symmetric memory. Advanced users who choose
121
+ to manage the synchronization on their own using the appropriate NVSHMEM API, or who know that
122
+ GPUs are already synchronized on the source operand, can set this to False.""".replace("\n", " "),
123
+ #
124
+ "function_signature": """\
125
+ operand,
126
+ distribution: Slab | Sequence[Box],
127
+ sync_symmetric_memory: bool = True,
128
+ options: FFTOptions | None = None,
129
+ stream: AnyStream | None = None
130
+ """.replace("\n", " "),
131
+ }
132
+ )
133
+
134
+
135
+ def _calculate_slab_shape_strides(global_extents, partition_dim, rank, nranks):
136
+ """Calculate the local slab shape for the given rank, given the global shape
137
+ and partition dimension."""
138
+ n = nranks
139
+ S = global_extents[partition_dim]
140
+ partition_dim_local_size = (S // n + 1) if rank < S % n else S // n
141
+ slab_shape = list(global_extents)
142
+ slab_shape[partition_dim] = partition_dim_local_size
143
+ strides = calculate_strides(slab_shape, reversed(range(len(global_extents))))
144
+ return tuple(slab_shape), strides
145
+
146
+
147
+ def _calculate_local_box(global_shape, partition_dim, rank, nranks):
148
+ """Given a global shape of data that is partitioned across ranks along the
149
+ `partition_dim` dimension according to cuFFTMp's slab distribution, return
150
+ the local box of this rank (as lower and upper coordinates in the global shape).
151
+ """
152
+ lower = [0] * len(global_shape)
153
+ for i in range(rank):
154
+ shape, _ = _calculate_slab_shape_strides(global_shape, partition_dim, i, nranks)
155
+ lower[partition_dim] += shape[partition_dim]
156
+ shape, _ = _calculate_slab_shape_strides(global_shape, partition_dim, rank, nranks)
157
+ upper = list(shape)
158
+ upper[partition_dim] += lower[partition_dim]
159
+ return lower, upper
160
+
161
+
162
+ def _get_fft_concrete_type(dtype, fft_abstract_type):
163
+ FFTType = cufft.Type
164
+ if fft_abstract_type == "C2C":
165
+ if dtype == "complex64":
166
+ return FFTType["C2C"]
167
+ elif dtype == "complex128":
168
+ return FFTType["Z2Z"]
169
+ else:
170
+ raise ValueError(f"Incompatible dtype '{dtype}' for complex-to-complex transform.")
171
+ elif fft_abstract_type == "R2C":
172
+ if dtype == "float32":
173
+ return FFTType["R2C"]
174
+ elif dtype == "float64":
175
+ return FFTType["D2Z"]
176
+ else:
177
+ raise ValueError(f"Incompatible dtype '{dtype}' for real-to-complex transform.")
178
+ elif fft_abstract_type == "C2R":
179
+ if dtype == "complex64":
180
+ return FFTType["C2R"]
181
+ elif dtype == "complex128":
182
+ return FFTType["Z2R"]
183
+ else:
184
+ raise ValueError(f"Incompatible dtype '{dtype}' for complex-to-real transform.")
185
+ else:
186
+ raise ValueError(f"Unsupported FFT Type: '{fft_abstract_type}'")
187
+
188
+
189
+ def _get_validate_direction(direction, fft_abstract_type):
190
+ if isinstance(direction, str) and (d := direction.upper()) in ["FORWARD", "INVERSE"]:
191
+ direction = FFTDirection[d]
192
+ else:
193
+ direction = FFTDirection(direction)
194
+
195
+ if fft_abstract_type == "C2R":
196
+ if direction != FFTDirection.INVERSE:
197
+ raise ValueError(
198
+ f"The specified direction {direction.name} is not compatible with the FFT type '{fft_abstract_type}'."
199
+ )
200
+ elif fft_abstract_type == "R2C": # noqa: SIM102
201
+ if direction != FFTDirection.FORWARD:
202
+ raise ValueError(
203
+ f"The specified direction {direction.name} is not compatible with the FFT type '{fft_abstract_type}'."
204
+ )
205
+ return direction
206
+
207
+
208
+ def complex_to_real_equivalent(name):
209
+ assert "complex" in name, f"Internal Error ({name=})"
210
+ m = name.split("complex")
211
+ assert len(m) in (1, 2)
212
+ size = int(m[-1]) // 2
213
+ if len(m) == 1:
214
+ return f"float{size}"
215
+ else:
216
+ return f"{m[0]}float{size}"
217
+
218
+
219
+ def real_to_complex_equivalent(name):
220
+ assert "float" in name, f"Internal Error ({name=})"
221
+ m = name.split("float")
222
+ assert len(m) in (1, 2)
223
+ size = int(m[-1])
224
+ if len(m) == 1:
225
+ return f"complex{size * 2}"
226
+ else:
227
+ return f"{m[0]}complex{size * 2}"
228
+
229
+
230
+ def _get_default_fft_abstract_type(dtype, fft_type):
231
+ if fft_type is not None:
232
+ return fft_type
233
+
234
+ f, c = "float", "complex"
235
+ if dtype[: len(f)] == f:
236
+ fft_type = "R2C"
237
+ elif dtype[: len(c)] == c:
238
+ fft_type = "C2C"
239
+ else:
240
+ raise ValueError(f"Unsupported dtype '{dtype}' for FFT.")
241
+ return fft_type
242
+
243
+
244
+ def _get_fft_result_and_compute_types(dtype, fft_abstract_type):
245
+ """
246
+ Return result and compute data type given the input data type and the FFT type.
247
+ """
248
+ if fft_abstract_type == "C2C":
249
+ return dtype, dtype
250
+ elif fft_abstract_type == "C2R":
251
+ return complex_to_real_equivalent(dtype), dtype
252
+ elif fft_abstract_type == "R2C":
253
+ return real_to_complex_equivalent(dtype), dtype
254
+ else:
255
+ raise ValueError(f"Unsupported FFT Type: '{fft_abstract_type}'")
256
+
257
+
258
+ def _get_fft_default_direction(fft_abstract_type) -> FFTDirection:
259
+ """
260
+ Return the default FFT direction (as object of type configuration.FFTDirection) based on
261
+ the FFT type.
262
+ """
263
+ if fft_abstract_type in ["C2C", "R2C"]:
264
+ return FFTDirection.FORWARD
265
+ elif fft_abstract_type == "C2R":
266
+ return FFTDirection.INVERSE
267
+ else:
268
+ raise ValueError(f"Unsupported FFT Type: '{fft_abstract_type}'")
269
+
270
+
271
+ def check_inplace_overlapping_layout(operand: utils.TensorHolder):
272
+ if is_overlapping_layout(operand.shape, operand.strides):
273
+ raise ValueError(
274
+ f"In-place transform is not supported because the tensor with shape "
275
+ f"{operand.shape} and strides {operand.strides} overlaps in memory."
276
+ )
277
+
278
+
279
+ def calculate_strides(shape, axis_order):
280
+ """
281
+ Calculate the strides for the provided shape and axis order.
282
+ """
283
+ strides = [None] * len(shape)
284
+
285
+ stride = 1
286
+ for axis in axis_order:
287
+ strides[axis] = stride
288
+ stride *= shape[axis]
289
+
290
+ return tuple(strides)
291
+
292
+
293
+ def _copy_operand_perhaps(
294
+ internal_operand,
295
+ operand: utils.TensorHolder,
296
+ stream_holder,
297
+ execution_space,
298
+ memory_space,
299
+ device_id: int | Literal["cpu"],
300
+ fft_abstract_type,
301
+ logger,
302
+ ):
303
+ if execution_space == memory_space:
304
+ if fft_abstract_type != "C2R":
305
+ return operand, None
306
+ else:
307
+ # For C2R, we need to take a copy to avoid input being overwritten
308
+ logger.info("For C2R FFT with input operand on GPU, the input is copied to avoid being overwritten by cuFFT.")
309
+ operand_copy = utils.create_empty_tensor(
310
+ operand.__class__,
311
+ operand.shape,
312
+ operand.dtype,
313
+ device_id,
314
+ stream_holder,
315
+ verify_strides=True,
316
+ strides=operand.strides,
317
+ make_symmetric=True,
318
+ logger=logger,
319
+ )
320
+ operand_copy.copy_(operand, stream_holder=stream_holder)
321
+ # We don't need to keep the operand backup, because C2R precludes `inplace=True`
322
+ return operand_copy, None
323
+ else:
324
+ # Copy the `operand` to memory that matches the exec space and keep the
325
+ # original `operand` since distributed FFT has inplace behavior and the
326
+ # result will overwrite the original operand.
327
+ if internal_operand is None:
328
+ assert execution_space == "cuda"
329
+ exec_space_copy = operand.to(device_id, stream_holder)
330
+ return exec_space_copy, operand
331
+ else:
332
+ # In-place copy to existing pointer
333
+ # Since the distribution of the operand (and thus the shape) can
334
+ # change with reset_operand, we have to be able to reshape the internal
335
+ # operand.
336
+ if internal_operand.shape != operand.shape:
337
+ internal_operand = internal_operand.reshape(operand.shape, copy=False)
338
+ tensor_wrapper.copy_([operand], [internal_operand], stream_holder)
339
+ return internal_operand, operand
340
+
341
+
342
+ def _problem_spec_reducer(p1: _ProblemSpec, p2: _ProblemSpec):
343
+ try:
344
+ if isinstance(p1, Exception):
345
+ return p1 # propagate exception
346
+
347
+ if isinstance(p2, Exception):
348
+ return p2 # propagate exception
349
+
350
+ if len(p1.shape) != len(p2.shape):
351
+ return ValueError("The number of dimensions of the input operand is inconsistent across processes")
352
+
353
+ # Check if rank is 2-D or 3-D.
354
+ if len(p1.shape) not in (2, 3):
355
+ return ValueError(
356
+ "Distributed FFT is currently supported only for 2-D and 3-D tensors."
357
+ f" The number of dimensions of the operand is {len(p1.shape)}."
358
+ )
359
+
360
+ if p1.operand_dtype != p2.operand_dtype:
361
+ return ValueError("The operand dtype is inconsistent across processes")
362
+
363
+ if p1.package != p2.package:
364
+ return ValueError("operand doesn't belong to the same package on all processes")
365
+
366
+ if p1.memory_space != p2.memory_space:
367
+ return ValueError('operand is not on the same memory space ("cpu", "cuda") on all processes')
368
+
369
+ if p1.options != p2.options:
370
+ return ValueError(f"options are inconsistent across processes: {p1.options} != {p2.options}")
371
+
372
+ # Determine the memory layout shared by all processes.
373
+ p1.is_C &= p2.is_C
374
+ if not p1.is_C:
375
+ return ValueError("The input memory layout is not C on every process")
376
+
377
+ is_box_1 = not isinstance(p1.distribution, Slab)
378
+ is_box_2 = not isinstance(p2.distribution, Slab)
379
+ if is_box_1 != is_box_2:
380
+ return ValueError("distribution must be either Slab or box on all processes, not a mix of both")
381
+
382
+ if not is_box_1:
383
+ if p1.distribution != p2.distribution:
384
+ raise ValueError("The slab distribution is inconsistent across processes")
385
+
386
+ # Using cuFFTMp slab distribution.
387
+ partitioned_dim = 0 if p1.distribution == Slab.X else 1
388
+
389
+ if any(p1.shape[i] != p2.shape[i] for i in range(len(p1.shape)) if i != partitioned_dim):
390
+ return ValueError("The problem size is inconsistent across processes")
391
+
392
+ if p1 is not p2: # with nranks=1 p1 is p2
393
+ # Reduce the partitioned dimension to get the global size.
394
+ p1.shape[partitioned_dim] += p2.shape[partitioned_dim]
395
+ else:
396
+ # Custom distribution given by input and output boxes on each process.
397
+ if len(p1.distribution) != 2 or len(p2.distribution) != 2: # type: ignore
398
+ return ValueError("Must provide input and output boxes on all processes")
399
+ input_box1, output_box1 = p1.distribution # type: ignore
400
+ input_box2, output_box2 = p2.distribution # type: ignore
401
+ for box in (input_box1, output_box1, input_box2, output_box2):
402
+ if len(box) != 2:
403
+ return ValueError(f"Box {box} must have lower and upper coordinates")
404
+ lower, upper = box
405
+ if len(lower) != len(p1.shape) or len(upper) != len(p1.shape):
406
+ return ValueError(
407
+ f"The number of coordinates in each coordinate pair of box {box} must "
408
+ f"match the number of operand dimensions {len(p1.shape)}."
409
+ )
410
+ if not all(upper[i] > lower[i] for i in range(len(p1.shape))):
411
+ return ValueError(
412
+ f"The upper coordinates must be larger than the lower coordinates, but got lower={lower} upper={upper}"
413
+ )
414
+
415
+ for p_spec in (p1, p2):
416
+ if p_spec.is_leaf:
417
+ # Check that the input box shape of this process matches the shape of
418
+ # the input operand.
419
+ input_lower, input_upper = p_spec.distribution[0] # type: ignore
420
+ input_box_shape = tuple(input_upper[i] - input_lower[i] for i in range(len(p_spec.shape)))
421
+ if input_box_shape != tuple(p_spec.shape):
422
+ return ValueError(
423
+ f"The operand shape {p_spec.shape} does not match the input box shape {input_box_shape}"
424
+ )
425
+
426
+ if p1 is not p2: # with nranks=1 p1 is p2
427
+ p1.global_size += p2.global_size
428
+
429
+ def reduce_boxes(box1, box2):
430
+ """This function returns the smallest box that encompasses `box1`
431
+ and `box2`"""
432
+ lower1, upper1 = box1
433
+ lower2, upper2 = box2
434
+ lower = np.minimum(np.array(lower1), np.array(lower2)).tolist()
435
+ upper = np.maximum(np.array(upper1), np.array(upper2)).tolist()
436
+ return lower, upper
437
+
438
+ # Merge the boxes to get the global operand shape. Note that this is applied
439
+ # progressively throughout the MPI reduction, starting with the local boxes.
440
+ p1.distribution = (reduce_boxes(input_box1, input_box2), reduce_boxes(output_box1, output_box2))
441
+
442
+ except Exception as e:
443
+ return e
444
+ p1.is_leaf = False
445
+ return p1
446
+
447
+
448
+ def _reduce_exception(e1, e2):
449
+ if e1 is not None:
450
+ return e1
451
+ return e2
452
+
453
+
454
+ class InvalidFFTState(Exception):
455
+ pass
456
+
457
+
458
+ @utils.docstring_decorator(SHARED_FFT_DOCUMENTATION, skip_missing=False)
459
+ class FFT:
460
+ """
461
+ Create a stateful object that encapsulates the specified distributed FFT computations
462
+ and required resources. This object ensures the validity of resources during use and
463
+ releases them when they are no longer needed to prevent misuse.
464
+
465
+ This object encompasses all functionalities of function-form APIs :func:`fft` and
466
+ :func:`ifft`, which are convenience wrappers around it.
467
+ The stateful object also allows for the amortization of preparatory costs when the same
468
+ FFT operation is to be performed on multiple operands with the same problem
469
+ specification (see :meth:`reset_operand` for more details).
470
+
471
+ Using the stateful object typically involves the following steps:
472
+
473
+ 1. **Problem Specification**: Initialize the object with a defined operation and
474
+ options.
475
+ 2. **Preparation**: Use :meth:`plan` to determine the best algorithmic implementation
476
+ for this specific FFT operation.
477
+ 3. **Execution**: Perform the FFT computation with :meth:`execute`, which can be either
478
+ forward or inverse FFT transformation.
479
+ 4. **Resource Management**: Ensure all resources are released either by explicitly
480
+ calling :meth:`free` or by managing the stateful object within a context manager.
481
+
482
+ Detailed information on each step described above can be obtained by passing in a
483
+ :class:`logging.Logger` object to :class:`FFTOptions` or by setting the appropriate
484
+ options in the root logger object, which is used by default:
485
+
486
+ >>> import logging
487
+ >>> logging.basicConfig(
488
+ ... level=logging.INFO,
489
+ ... format="%(asctime)s %(levelname)-8s %(message)s",
490
+ ... datefmt="%m-%d %H:%M:%S",
491
+ ... )
492
+
493
+ Args:
494
+ operand: {operand}
495
+ {operand_admonitions}
496
+
497
+ distribution: {distribution}
498
+
499
+ options: {options}
500
+
501
+ stream: {stream}
502
+
503
+ See Also:
504
+ :meth:`plan`, :meth:`reset_operand`, :meth:`execute`
505
+
506
+ Examples:
507
+
508
+ >>> import cupy as cp
509
+ >>> import nvmath.distributed
510
+
511
+ Get MPI communicator used to initialize nvmath.distributed (for information on
512
+ initializing nvmath.distributed, you can refer to the documentation or to the
513
+ FFT examples in `nvmath/examples/distributed/fft
514
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/fft>`_):
515
+
516
+ >>> comm = nvmath.distributed.get_context().communicator
517
+
518
+ Get the number of processes:
519
+
520
+ >>> nranks = comm.Get_size()
521
+
522
+ Create a 3-D complex128 ndarray on GPU symmetric memory, distributed according to
523
+ the Slab distribution on the X axis (the global shape is (128, 128, 128)):
524
+
525
+ >>> shape = 128 // nranks, 128, 128
526
+
527
+ cuFFTMp uses the NVSHMEM PGAS model for distributed computation, which requires GPU
528
+ operands to be on the symmetric heap:
529
+
530
+ >>> a = nvmath.distributed.allocate_symmetric_memory(shape, cp, dtype=cp.complex128)
531
+
532
+ After allocating, we initialize the CuPy ndarray's memory:
533
+
534
+ >>> a[:] = cp.random.rand(*shape) + 1j * cp.random.rand(*shape)
535
+
536
+ We will define a 3-D C2C FFT operation, creating an FFT object encapsulating the
537
+ above problem specification. Each process provides their own local operand (which
538
+ is part of the PGAS space, but otherwise can be operated on as any other CuPy
539
+ ndarray for local operations) and specifies how the operand is distributed across
540
+ processes:
541
+
542
+ >>> f = nvmath.distributed.fft.FFT(a, distribution=nvmath.distributed.fft.Slab.X)
543
+
544
+ More information on distribution of operands can be found in the documentation:
545
+ (TODO: link to docs).
546
+
547
+ Options can be provided above to control the behavior of the operation using the
548
+ `options` argument (see :class:`FFTOptions`).
549
+
550
+ Next, plan the FFT:
551
+
552
+ >>> f.plan()
553
+
554
+ Now execute the FFT, and obtain the result `r1` as a CuPy ndarray. Note that
555
+ distributed FFT computations are inplace, so operands a and r1 share the same
556
+ symmetric memory buffer:
557
+
558
+ >>> r1 = f.execute()
559
+
560
+ Finally, free the FFT object's resources. To avoid this explicit call, it's
561
+ recommended to use the FFT object as a context manager as shown below, if possible.
562
+
563
+ >>> f.free()
564
+
565
+ Any symmetric memory that is owned by the user must be deleted explicitly (this is
566
+ a collective call and must be called by all processes). Note that because operands
567
+ a and r1 share the same buffer, only one of them must be freed:
568
+
569
+ >>> nvmath.distributed.free_symmetric_memory(a)
570
+
571
+ Note that all :class:`FFT` methods execute on the current stream by default.
572
+ Alternatively, the `stream` argument can be used to run a method on a specified
573
+ stream.
574
+
575
+ Let's now look at the same problem with NumPy ndarrays on the CPU.
576
+
577
+ Create a 3-D complex128 NumPy ndarray on the CPU:
578
+
579
+ >>> import numpy as np
580
+ >>> shape = 128 // nranks, 128, 128
581
+ >>> a = np.random.rand(*shape) + 1j * np.random.rand(*shape)
582
+
583
+ Create an FFT object encapsulating the problem specification described earlier and
584
+ use it as a context manager.
585
+
586
+ >>> with nvmath.distributed.fft.FFT(a, distribution=Slab.X) as f:
587
+ ... f.plan()
588
+ ...
589
+ ... # Execute the FFT to get the first result.
590
+ ... r1 = f.execute()
591
+
592
+ All the resources used by the object are released at the end of the block.
593
+
594
+ The operation was performed on the GPU, with the NumPy array temporarily copied to
595
+ GPU symmetric memory and transformed on the GPU.
596
+
597
+ Further examples can be found in the `nvmath/examples/distributed/fft
598
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/fft>`_
599
+ directory.
600
+ """
601
+
602
+ def _free_internal_sheap(self, exception: Exception | None = None) -> bool:
603
+ # This is a fail-safe to free NVSHMEM internal memory in case of invalid
604
+ # state (FFT constructor fails). Since we might call nvshmem_free here, we're
605
+ # assuming that all processes equally failed in the ctor, which might not be true,
606
+ # but if it weren't true they would end up in deadlock most likely anyway.
607
+ if (
608
+ hasattr(self, "memory_space")
609
+ and self.memory_space == "cpu"
610
+ and self.operand is not None
611
+ and self.operand.device == "cuda"
612
+ ):
613
+ with utils.device_ctx(self.device_id):
614
+ nvshmem_free_wrapper(self.operand.data_ptr)
615
+ return True
616
+
617
+ @utils.atomic(_free_internal_sheap, method=True)
618
+ def __init__(
619
+ self,
620
+ operand,
621
+ distribution: Slab | Sequence[Box],
622
+ *,
623
+ options: FFTOptions | None = None,
624
+ stream: AnyStream | None = None,
625
+ ):
626
+ distributed_ctx = nvmath.distributed.get_context()
627
+ if distributed_ctx is None:
628
+ # TODO: add a link to the docs section that will discuss initialization
629
+ # and finalization of the distributed operations.
630
+ raise RuntimeError("nvmath.distributed has not been initialized")
631
+ communicator = distributed_ctx.communicator
632
+ self.rank = rank = communicator.Get_rank()
633
+ self.nranks = nranks = communicator.Get_size()
634
+
635
+ # For GPU operands, the distributed tensor wrappers check that the memory is in the
636
+ # symmetric heap by calling nvshmem.ptr().
637
+ self.operand = operand = tensor_wrapper.wrap_operand(operand)
638
+ self.options = options = cast(FFTOptions, utils.check_or_create_options(FFTOptions, options, "Distributed FFT options"))
639
+ self.package = operand.name
640
+
641
+ is_C = sorted(operand.strides, reverse=True) == list(operand.strides)
642
+
643
+ # Merge the problem specification across processes to make sure that there are no
644
+ # inconsistencies and to calculate the global shape. Importantly, this also does
645
+ # collective error checking of the FFT input parameters, to ensure that all
646
+ # processes fail on error of any one process, thus preventing deadlock.
647
+ problem_spec = _ProblemSpec(
648
+ distribution=distribution,
649
+ shape=list(operand.shape),
650
+ is_C=is_C,
651
+ operand_dtype=operand.dtype,
652
+ options=_ProblemSpec.Options(options),
653
+ package=self.package,
654
+ memory_space=operand.device,
655
+ global_size=math.prod(operand.shape),
656
+ )
657
+ if nranks > 1:
658
+ problem_spec = communicator.allreduce(problem_spec, op=_problem_spec_reducer)
659
+ else:
660
+ # Ensure we error-check with one rank.
661
+ problem_spec = _problem_spec_reducer(problem_spec, problem_spec)
662
+ if isinstance(problem_spec, Exception):
663
+ # There is an error or inconsistency in the problem spec across processes.
664
+ # Note that since this comes from an allreduce, all processes will have
665
+ # received the same exception.
666
+ raise problem_spec
667
+
668
+ self.operand_dim = len(operand.shape)
669
+
670
+ self.operand_data_type = operand.dtype
671
+ self.fft_abstract_type = _get_default_fft_abstract_type(self.operand_data_type, options.fft_type)
672
+
673
+ # TODO: R2C and C2R
674
+ if self.fft_abstract_type in ("R2C", "C2R"):
675
+ raise ValueError("Only complex-to-complex distributed FFT is currently supported.")
676
+
677
+ self.result_data_type, self.compute_data_type = _get_fft_result_and_compute_types(operand.dtype, self.fft_abstract_type)
678
+
679
+ self.logger = options.logger if options.logger is not None else logging.getLogger()
680
+ self.logger.info(f"The FFT type is {self.fft_abstract_type}.")
681
+ self.logger.info(
682
+ f"The input data type is {self.operand_data_type}, and the result data type is {self.result_data_type}."
683
+ )
684
+
685
+ # cuFFTMp doesn't support complex32.
686
+ if self.result_data_type == "complex32":
687
+ raise TypeError(f"The result data type {self.result_data_type} is not supported.")
688
+
689
+ # Infer operand package, execution space, and memory space.
690
+ execution_device_id: int = distributed_ctx.device_id
691
+ if operand.device_id != "cpu": # exec space matches the mem space
692
+ self.memory_space = "cuda"
693
+ self.device_id = operand.device_id
694
+ assert operand.device_id == execution_device_id
695
+ else: # we need to move inputs cpu -> gpu and outputs gpu -> cpu
696
+ self.memory_space = "cpu"
697
+ self.device_id = execution_device_id
698
+ self.execution_space = "cuda"
699
+ self.operand_device_id = operand.device_id
700
+ self.internal_op_package = self._internal_operand_package(self.package)
701
+ stream_holder: StreamHolder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
702
+
703
+ self.logger.info(
704
+ f"The input tensor's memory space is {self.memory_space}, and the execution space "
705
+ f"is {self.execution_space}, with device {self.device_id}."
706
+ )
707
+
708
+ self.logger.info(f"The specified stream for the FFT ctor is {stream_holder and stream_holder.obj}")
709
+
710
+ # Copy the operand to execution_space's device if needed.
711
+ self.operand, self.operand_backup = _copy_operand_perhaps(
712
+ None,
713
+ operand,
714
+ stream_holder,
715
+ self.execution_space,
716
+ self.memory_space,
717
+ self.device_id,
718
+ self.fft_abstract_type,
719
+ self.logger,
720
+ )
721
+
722
+ operand = self.operand
723
+ # Capture operand layout for consistency checks when resetting operands.
724
+ self.operand_layout = TensorLayout(shape=operand.shape, strides=operand.strides)
725
+
726
+ self.logger.info("The FFT will be performed in-place, with the result overwriting the input.")
727
+
728
+ # The result's package and device.
729
+ self.result_class: CupyDistributedTensor | TorchDistributedTensor = operand.__class__
730
+
731
+ # Set blocking or non-blocking behavior.
732
+ self.blocking = self.options.blocking is True or self.memory_space == "cpu"
733
+ if self.blocking:
734
+ self.call_prologue = "This call is blocking and will return only after the operation is complete."
735
+ else:
736
+ self.call_prologue = (
737
+ "This call is non-blocking and will return immediately after the operation is launched on the device."
738
+ )
739
+
740
+ if not isinstance(distribution, Slab):
741
+ # Reshape only applies to cuFFTMp's default slab distribution.
742
+ self.options.reshape = False
743
+ self.logger.info("Reshape option is ignored when using box distribution.")
744
+
745
+ # Set memory allocator.
746
+ self.allocator = NvshmemMemoryManager(self.device_id, self.logger)
747
+
748
+ self.distribution: Slab | Sequence[Box] = distribution
749
+ # Map possible distributions to the corresponding operand TensorLayout.
750
+ self.distribution_layout: dict[Slab | Box, TensorLayout] = {}
751
+ # The subformat is an identifier that cuFFTMp uses to refer to an operand
752
+ # distribution. It can be one of:
753
+ # - cufftMp.XtSubFormat.FORMAT_INPLACE (refers to Slab.X)
754
+ # - cufftMp.XtSubFormat.FORMAT_INPLACE_SHUFFLED (refers to Slab.Y)
755
+ # - cufft.XtSubFormat.FORMAT_DISTRIBUTED_INPUT (the input box at FFT plan time)
756
+ # - cufft.XtSubFormat.FORMAT_DISTRIBUTED_OUTPUT (the output box at FFT plan time)
757
+ self.subformat: int = -1
758
+ if isinstance(distribution, Slab):
759
+ self.global_extents = tuple(problem_spec.shape)
760
+
761
+ # TODO: fully support X and Y not divisible by number of ranks.
762
+ if self.global_extents[0] % nranks != 0:
763
+ raise ValueError("X not divisible by # ranks is not supported yet")
764
+ if self.global_extents[1] % nranks != 0:
765
+ raise ValueError("Y not divisible by # ranks is not supported yet")
766
+
767
+ # Check that this process has the correct slab shape.
768
+ partitioned_dim = 0 if distribution == Slab.X else 1
769
+ shape, _ = _calculate_slab_shape_strides(self.global_extents, partitioned_dim, rank, nranks)
770
+ error = None
771
+ if self.operand.shape != shape:
772
+ error = ValueError(
773
+ f"The operand shape is {self.operand.shape}, but the expected slab shape is {shape} ({distribution})"
774
+ )
775
+ error = communicator.allreduce(error, _reduce_exception)
776
+ if error:
777
+ raise error
778
+
779
+ self.distribution_layout[distribution] = self.operand_layout
780
+
781
+ if self.options.reshape:
782
+ from_axis, to_axis = ("X", "X") if distribution == Slab.X else ("Y", "Y")
783
+ else:
784
+ from_axis, to_axis = ("X", "Y") if distribution == Slab.X else ("Y", "X")
785
+ self.logger.info(
786
+ f"The operand distribution is Slab, with input partitioned on {from_axis} axis "
787
+ f"and output on {to_axis} (reshape={self.options.reshape})."
788
+ )
789
+ else:
790
+ # The merged (global) input and output box must be the same.
791
+ if problem_spec.distribution[0] != problem_spec.distribution[1]: # type: ignore
792
+ raise ValueError(
793
+ "The global box derived from the input and output boxes doesn't match: "
794
+ f"{problem_spec.distribution[0]} != {problem_spec.distribution[1]}" # type: ignore
795
+ )
796
+
797
+ # Infer the global shape from the global input box. Note that cuFFTMp does not
798
+ # disallow lower coordinates for the merged (global) boxes that are not 0.
799
+ lower, upper = problem_spec.distribution[0] # type: ignore
800
+ self.global_extents = tuple(int(upper[i] - lower[i]) for i in range(self.operand_dim))
801
+
802
+ # The global number of elements must be compatible with the global shape.
803
+ if problem_spec.global_size != math.prod(self.global_extents):
804
+ raise ValueError(
805
+ f"The global number of elements is incompatible with the inferred global shape {self.global_extents}"
806
+ )
807
+
808
+ input_box, output_box = distribution
809
+ input_box = (tuple(input_box[0]), tuple(input_box[1]))
810
+ output_box = (tuple(output_box[0]), tuple(output_box[1]))
811
+ self.distribution_layout[input_box] = self.operand_layout
812
+ self.distribution = distribution = (input_box, output_box)
813
+
814
+ self.logger.info(f"The operand distribution is based on custom input box {input_box} and output box {output_box}.")
815
+
816
+ self.logger.info(f"The global FFT extents are {self.global_extents}.")
817
+
818
+ # Infer result shape and strides.
819
+ # TODO: adjust for R2C and C2R.
820
+ if self.options.reshape:
821
+ self.result_shape = operand.shape
822
+ self.result_strides = operand.strides
823
+ elif not isinstance(self.distribution, Slab):
824
+ output_lower, output_upper = distribution[1] # type: ignore
825
+ self.result_shape = tuple(output_upper[i] - output_lower[i] for i in range(self.operand_dim))
826
+ self.result_strides = calculate_strides(self.result_shape, reversed(range(self.operand_dim)))
827
+ self.distribution_layout[output_box] = TensorLayout(shape=self.result_shape, strides=self.result_strides)
828
+ else:
829
+ result_partition_dim = 1 if distribution == Slab.X else 0
830
+ self.result_shape, self.result_strides = _calculate_slab_shape_strides(
831
+ self.global_extents, result_partition_dim, rank, nranks
832
+ )
833
+ self.distribution_layout[Slab.X if distribution == Slab.Y else Slab.Y] = TensorLayout(
834
+ shape=self.result_shape, strides=self.result_strides
835
+ )
836
+
837
+ # Create handle.
838
+ with utils.device_ctx(self.device_id):
839
+ self.handle = cufft.create()
840
+ # Dummy handle to create a cufft descriptor with initial tiny data buffer.
841
+ # We'll reuse this descriptor to call cufft.xt_exec_descriptor, by
842
+ # setting the data pointer and subformat in the descriptor.
843
+ self.memory_desc_handle = cufft.create()
844
+ if self.options.reshape:
845
+ self.reshape_handle = cufft.create_reshape()
846
+
847
+ # Set stream for the FFT.
848
+ with utils.device_ctx(self.device_id):
849
+ cufft.set_stream(self.handle, stream_holder.ptr) # type: ignore[union-attr]
850
+
851
+ # Plan attributes.
852
+ cufft.set_auto_allocation(self.handle, 0)
853
+
854
+ self.fft_planned = False
855
+ # Descriptor to call cufft.xt_exec_descriptor (by setting the
856
+ # data pointer and subformat in the descriptor before execute).
857
+ self.memory_desc = None
858
+ # Pointer to tiny data buffer of descriptor when first created.
859
+ self.dummy_desc_data_ptr = None
860
+
861
+ # Workspace attributes.
862
+ self.workspace_ptr: None | memory.MemoryPointer = None
863
+ self.workspace_size = 0
864
+ self._workspace_allocated_here = False
865
+ self.reshaped_operand = None
866
+
867
+ # Attributes to establish stream ordering.
868
+ self.workspace_stream = None
869
+ self.last_compute_event = None
870
+
871
+ self.valid_state = True
872
+ self.logger.info("The distributed FFT operation has been created.")
873
+
874
+ def __enter__(self):
875
+ return self
876
+
877
+ def __exit__(self, exc_type, exc_value, traceback):
878
+ self.free()
879
+
880
+ def _check_valid_fft(self, *args, **kwargs):
881
+ """
882
+ Check if FFT object is alive and well.
883
+ """
884
+ if not self.valid_state:
885
+ raise InvalidFFTState("The FFT object cannot be used after resources are free'd")
886
+
887
+ def _free_plan_resources(self, exception: Exception | None = None) -> bool:
888
+ """
889
+ Free resources allocated in planning.
890
+ """
891
+ if self.memory_desc is not None:
892
+ with utils.device_ctx(self.device_id):
893
+ cufft.xt_free(self.memory_desc)
894
+ self.memory_desc = None
895
+
896
+ self.fft_planned = False
897
+ return True
898
+
899
+ def _internal_operand_package(self, package_name):
900
+ if self.execution_space == "cuda":
901
+ if package_name == "numpy":
902
+ # TODO: remove this call after cupy is dropped
903
+ maybe_register_package("cupy")
904
+ return package_name if package_name != "numpy" else "cupy"
905
+ else:
906
+ return package_name if package_name != "cupy" else "numpy"
907
+
908
+ def _allocate_reshape_operand(self, exec_stream_holder: StreamHolder | None, log_debug):
909
+ if log_debug:
910
+ self.logger.debug("Beginning empty tensor creation to hold reshape value...")
911
+ self.logger.debug(
912
+ f"The reshape tensor shape = {self.result_shape} with strides = "
913
+ f"{self.result_strides} and data type '{self.result_data_type}'."
914
+ )
915
+ result = utils.create_empty_tensor(
916
+ self.result_class, # type: ignore
917
+ self.result_shape,
918
+ self.result_data_type,
919
+ self.device_id,
920
+ exec_stream_holder,
921
+ verify_strides=False, # the strides are computed so that they are contiguous
922
+ strides=self.result_strides,
923
+ make_symmetric=True,
924
+ logger=self.logger,
925
+ )
926
+ if log_debug:
927
+ self.logger.debug("The reshape output (empty) tensor has been created.")
928
+ return result
929
+
930
+ @utils.precondition(_check_valid_fft)
931
+ @utils.atomic(_free_plan_resources, method=True)
932
+ def plan(self, *, stream: AnyStream | None = None):
933
+ """Plan the FFT.
934
+
935
+ Args:
936
+ stream: {stream}
937
+ """
938
+ log_info = self.logger.isEnabledFor(logging.INFO)
939
+ log_debug = self.logger.isEnabledFor(logging.DEBUG)
940
+
941
+ if self.fft_planned:
942
+ self.logger.debug("The FFT has already been planned, and redoing the plan is not supported.")
943
+ return
944
+
945
+ stream_holder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
946
+ self.workspace_stream = stream_holder.obj
947
+
948
+ # Set stream for the FFT.
949
+ with utils.device_ctx(self.device_id):
950
+ cufft.set_stream(self.handle, stream_holder.ptr)
951
+
952
+ check_inplace_overlapping_layout(self.operand)
953
+ if self.operand_backup is not None:
954
+ check_inplace_overlapping_layout(self.operand_backup)
955
+
956
+ if log_debug:
957
+ self.logger.debug(
958
+ f"The operand CUDA type is {NAME_TO_DATA_TYPE[self.operand_data_type].name}, and the result CUDA type is "
959
+ f"{NAME_TO_DATA_TYPE[self.result_data_type].name}."
960
+ )
961
+ self.logger.debug(f"The CUDA type used for compute is {NAME_TO_DATA_TYPE[self.compute_data_type].name}.")
962
+ if log_info:
963
+ self.logger.info("Starting distributed FFT planning...")
964
+
965
+ planner = None
966
+ if self.operand_dim == 2:
967
+ planner = cufft.make_plan2d
968
+ elif self.operand_dim == 3:
969
+ planner = cufft.make_plan3d
970
+ else:
971
+ raise AssertionError("Internal error: unsupported dimensionality for distributed FFT in plan().")
972
+
973
+ if self.options.reshape:
974
+ # Plan a reshape of the FFT output back to the original slab distribution of the
975
+ # FFT input.
976
+ from_partition_dim, to_partition_dim = (1, 0) if self.distribution == Slab.X else (0, 1)
977
+ # cuFFTMP reshape API only supports 3D, so we broadcast 2D operands.
978
+ X, Y = self.global_extents[:2]
979
+ Z = self.global_extents[2] if self.operand_dim == 3 else 1
980
+ global_shape = (X, Y, Z)
981
+ reshape_input_box = _calculate_local_box(global_shape, from_partition_dim, self.rank, self.nranks)
982
+ reshape_output_box = _calculate_local_box(global_shape, to_partition_dim, self.rank, self.nranks)
983
+ lower, upper = reshape_input_box
984
+ input_local_shape = (X, upper[1] - lower[1], Z) if from_partition_dim == 1 else (upper[0] - lower[0], Y, Z)
985
+ reshape_input_strides = calculate_strides(input_local_shape, reversed(range(3)))
986
+ reshape_output_strides = self.result_strides if self.operand_dim == 3 else tuple(self.result_strides) + (1,)
987
+
988
+ with utils.cuda_call_ctx(stream_holder, blocking=True, timing=log_info) as (
989
+ self.last_compute_event,
990
+ elapsed,
991
+ ):
992
+ if isinstance(self.distribution, Slab):
993
+ self.subformat = self.distribution
994
+ else:
995
+ lower_input, upper_input = self.distribution[0]
996
+ lower_output, upper_output = self.distribution[1]
997
+ strides_input = self.operand_layout.strides
998
+ strides_output = self.result_strides
999
+ cufft.xt_set_distribution(
1000
+ self.handle,
1001
+ self.operand_dim,
1002
+ lower_input,
1003
+ upper_input,
1004
+ lower_output,
1005
+ upper_output,
1006
+ strides_input,
1007
+ strides_output,
1008
+ )
1009
+ self.box_to_subformat = {}
1010
+ self.box_to_subformat[(tuple(lower_input), tuple(upper_input))] = cufft.XtSubFormat.FORMAT_DISTRIBUTED_INPUT
1011
+ self.box_to_subformat[(tuple(lower_output), tuple(upper_output))] = cufft.XtSubFormat.FORMAT_DISTRIBUTED_OUTPUT
1012
+ self.subformat = cufft.XtSubFormat.FORMAT_DISTRIBUTED_INPUT
1013
+
1014
+ fft_concrete_type = _get_fft_concrete_type(self.operand_data_type, self.fft_abstract_type)
1015
+ self.logger.debug(f"The FFT concrete type is {fft_concrete_type.name}.")
1016
+ # NVSHMEM is already initialized (no need to pass MPI comm to the library).
1017
+ cufft.attach_comm(self.handle, cufft.MpCommType.COMM_NONE, 0)
1018
+ self.workspace_size = planner(self.handle, *self.global_extents, fft_concrete_type)
1019
+
1020
+ # Create memory descriptor using dummy handle.
1021
+ _ = planner(self.memory_desc_handle, *[1] * self.operand_dim, fft_concrete_type)
1022
+ self.memory_desc = cufft.xt_malloc(self.memory_desc_handle, cufft.XtSubFormat.FORMAT_INPLACE)
1023
+
1024
+ if self.options.reshape:
1025
+ nullptr = 0
1026
+ cufft.make_reshape(
1027
+ self.reshape_handle,
1028
+ # TODO: change to `operand.dtype.itemsize` once operand is
1029
+ # StridedMemoryView.
1030
+ self.operand.tensor.dtype.itemsize,
1031
+ 3,
1032
+ reshape_input_box[0],
1033
+ reshape_input_box[1],
1034
+ reshape_input_strides,
1035
+ reshape_output_box[0],
1036
+ reshape_output_box[1],
1037
+ reshape_output_strides,
1038
+ nullptr,
1039
+ cufft.MpCommType.COMM_NONE,
1040
+ )
1041
+ reshape_workspace_size = cufft.get_reshape_size(self.reshape_handle)
1042
+ self.workspace_size = max(self.workspace_size, reshape_workspace_size)
1043
+
1044
+ self.logger.debug(
1045
+ f"The workspace required on process {self.rank} for the distributed"
1046
+ f" FFT operation is {formatters.MemoryStr(self.workspace_size)}."
1047
+ )
1048
+
1049
+ # Store memory descriptor's buffer pointer, to be able to free it later.
1050
+ self.dummy_desc_data_ptr = cufft.set_descriptor_data(self.memory_desc, 0, self.subformat)
1051
+
1052
+ self.fft_planned = True
1053
+
1054
+ if log_info and elapsed.data is not None:
1055
+ self.logger.info(f"The FFT planning phase took {elapsed.data:.3f} ms to complete.")
1056
+
1057
+ @utils.precondition(_check_valid_fft)
1058
+ def reset_operand(self, operand=None, distribution: Slab | Sequence[Box] | None = None, *, stream: AnyStream | None = None):
1059
+ """
1060
+ Reset the operand held by this :class:`FFT` instance. This method has two use cases:
1061
+
1062
+ (1) it can be used to provide a new operand for execution
1063
+ (2) it can be used to release the internal reference to the previous operand and
1064
+ potentially make its memory available for other use by passing
1065
+ ``operand=None``.
1066
+
1067
+ Args:
1068
+ operand: A tensor (ndarray-like object) compatible with the previous one or
1069
+ `None` (default). A value of `None` will release the internal reference to
1070
+ the previous operand and user is expected to set a new operand before again
1071
+ calling :meth:`execute`. The new operand is considered compatible if all the
1072
+ following properties match with the previous one:
1073
+
1074
+ - The operand distribution: (a) if the FFT was planned using a Slab
1075
+ distribution, the reset operand must also use a Slab distribution
1076
+ (both X and Y axes are valid regardless of the slab axis at
1077
+ plan time), (b) if the FFT was planned using a box distribution, the
1078
+ distribution of the reset operand must be (input_box, output_box)
1079
+ or (output_box, input_box) where input_box and output_box are the
1080
+ boxes specified at plan time.
1081
+ - The operand data type.
1082
+ - The package that the new operand belongs to.
1083
+ - The memory space of the new operand (CPU or GPU).
1084
+ - The device that new operand belongs to if it is on GPU.
1085
+
1086
+ distribution: {distribution}
1087
+
1088
+ stream: {stream}.
1089
+
1090
+ Examples:
1091
+
1092
+ >>> import cupy as cp
1093
+ >>> import nvmath.distributed
1094
+
1095
+ Get MPI communicator used to initialize nvmath.distributed (for information on
1096
+ initializing nvmath.distributed, you can refer to the documentation or to the
1097
+ FFT examples in `nvmath/examples/distributed/fft
1098
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/fft>`_):
1099
+
1100
+ >>> comm = nvmath.distributed.get_context().communicator
1101
+ >>> nranks = comm.Get_size()
1102
+
1103
+ Create a 3-D complex128 ndarray on GPU symmetric memory, distributed according
1104
+ to the Slab distribution on the X axis (the global shape is (128, 128, 128)):
1105
+
1106
+ >>> shape = 128 // nranks, 128, 128
1107
+ >>> dtype = cp.complex128
1108
+ >>> a = nvmath.distributed.allocate_symmetric_memory(shape, cp, dtype=dtype)
1109
+ >>> a[:] = cp.random.rand(*shape) + 1j * cp.random.rand(*shape)
1110
+
1111
+ Create an FFT object as a context manager
1112
+
1113
+ >>> with nvmath.distributed.fft.FFT(a, nvmath.distributed.fft.Slab.X) as f:
1114
+ ... # Plan the FFT
1115
+ ... f.plan()
1116
+ ...
1117
+ ... # Execute the FFT to get the first result.
1118
+ ... r1 = f.execute()
1119
+ ...
1120
+ ... # Reset the operand to a new CuPy ndarray.
1121
+ ... b = nvmath.distributed.allocate_symmetric_memory(shape, cp, dtype=dtype)
1122
+ ... b[:] = cp.random.rand(*shape) + 1j * cp.random.rand(*shape)
1123
+ ... f.reset_operand(b)
1124
+ ...
1125
+ ... # Execute to get the new result corresponding to the updated operand.
1126
+ ... r2 = f.execute()
1127
+
1128
+ With :meth:`reset_operand`, minimal overhead is achieved as problem
1129
+ specification and planning are only performed once.
1130
+
1131
+ For the particular example above, explicitly calling :meth:`reset_operand` is
1132
+ equivalent to updating the operand in-place, i.e, replacing
1133
+ ``f.reset_operand(b)`` with ``a[:]=b``. Note that updating the operand in-place
1134
+ should be adopted with caution as it can only yield the expected result and
1135
+ incur no additional copies under the additional constraints below:
1136
+
1137
+ - The operand's distribution is the same.
1138
+
1139
+ For more details, please refer to `inplace update example
1140
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/fft/example06_stateful_reset_inplace.py>`_.
1141
+ """
1142
+
1143
+ log_info = self.logger.isEnabledFor(logging.INFO)
1144
+
1145
+ if operand is None:
1146
+ if self.memory_space == "cpu" and self.operand is not None:
1147
+ with utils.device_ctx(self.device_id):
1148
+ nvshmem_free_wrapper(self.operand.data_ptr)
1149
+ self.operand = None # type: ignore
1150
+ self.operand_backup = None
1151
+ self.logger.info("The operand has been reset to None.")
1152
+ return
1153
+
1154
+ self.logger.info("Resetting operand...")
1155
+ # First wrap operand.
1156
+ operand = tensor_wrapper.wrap_operand(operand)
1157
+
1158
+ # Check package match.
1159
+ if self.package != operand.name:
1160
+ raise TypeError(f"Library package mismatch: '{self.package}' => '{operand.name}'")
1161
+
1162
+ utils.check_attribute_match(self.operand_data_type, operand.dtype, "data type")
1163
+
1164
+ if len(operand.shape) != self.operand_dim:
1165
+ raise ValueError(
1166
+ f"The reset operand number of dimensions ({len(operand.shape)}) does not "
1167
+ f"match the FFT number of dimensions ({self.operand_dim})"
1168
+ )
1169
+
1170
+ stream_holder: StreamHolder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
1171
+ self.logger.info(f"The specified stream for reset_operand() is {stream_holder and stream_holder.obj}.")
1172
+
1173
+ # In principle, we could support memory_space change,
1174
+ # but to handle it properly we need to update self.memory_space and
1175
+ # some dependent properties, like self.blocking, which may be error-prone
1176
+ # from the user perspective. It would prevent inplace optimizations as well.
1177
+ operand_device_id = operand.device_id
1178
+ if operand_device_id != self.operand_device_id:
1179
+
1180
+ def device_str(device_id: int | Literal["cpu"]) -> str:
1181
+ return f"cuda:{device_id}" if isinstance(device_id, int) else f"{device_id}"
1182
+
1183
+ raise ValueError(
1184
+ f"The new operand must be on the same device as the original one. "
1185
+ f"The new operand's device is {device_str(operand_device_id)}, "
1186
+ f"the original device is {device_str(self.operand_device_id)}"
1187
+ )
1188
+
1189
+ # Check for C memory layout.
1190
+ if sorted(operand.strides, reverse=True) != list(operand.strides):
1191
+ raise ValueError("The reset operand memory layout is not C")
1192
+
1193
+ # Check that the distribution of the reset operand is compatible.
1194
+ if distribution is None:
1195
+ raise ValueError("Please specify the distribution of the operand for reset_operand")
1196
+
1197
+ distribution_type_old = "slab" if isinstance(self.distribution, Slab) else "box"
1198
+ distribution_type_new = "slab" if isinstance(distribution, Slab) else "box"
1199
+ if distribution_type_old != distribution_type_new:
1200
+ raise ValueError(
1201
+ f"This FFT uses {distribution_type_old} distribution, but got "
1202
+ f"{distribution_type_new} distribution in reset_operand."
1203
+ )
1204
+
1205
+ if distribution_type_old == "slab":
1206
+ if self.options.reshape and self.distribution != distribution:
1207
+ raise ValueError("Can't change distribution when using reshape=True")
1208
+
1209
+ distribution = cast(Slab, distribution) # for type checker
1210
+ operand_layout = self.distribution_layout[distribution]
1211
+ if operand.shape != operand_layout.shape:
1212
+ raise ValueError(
1213
+ f"The operand shape is {operand.shape}, but the expected slab shape "
1214
+ f"is {operand_layout.shape} ({distribution})"
1215
+ )
1216
+
1217
+ self.subformat = self.distribution = distribution
1218
+
1219
+ # Log distribution.
1220
+ if log_info:
1221
+ if self.options.reshape:
1222
+ from_axis, to_axis = ("X", "X") if distribution == Slab.X else ("Y", "Y")
1223
+ else:
1224
+ from_axis, to_axis = ("X", "Y") if distribution == Slab.X else ("Y", "X")
1225
+ self.logger.info(
1226
+ f"The operand distribution is Slab, with input partitioned on {from_axis} axis "
1227
+ f"and output on {to_axis} (reshape={self.options.reshape})."
1228
+ )
1229
+ else:
1230
+ distribution = cast(Sequence[Box], distribution) # for type checker
1231
+ input_box, output_box = distribution
1232
+ input_box = (tuple(input_box[0]), tuple(input_box[1]))
1233
+ output_box = (tuple(output_box[0]), tuple(output_box[1]))
1234
+ if input_box not in self.box_to_subformat or output_box not in self.box_to_subformat:
1235
+ raise ValueError("The reset operand distribution must use the original boxes (in any order)")
1236
+
1237
+ operand_layout = self.distribution_layout[input_box]
1238
+ if operand.shape != operand_layout.shape:
1239
+ return ValueError(
1240
+ f"The operand shape {operand.shape} does not match the input box shape {operand_layout.shape}"
1241
+ )
1242
+
1243
+ self.subformat = self.box_to_subformat[input_box]
1244
+ self.distribution = distribution
1245
+
1246
+ # Log distribution.
1247
+ self.logger.info("The operand distribution is based on custom input and output boxes given on each process.")
1248
+
1249
+ # Set stream for the FFT.
1250
+ with utils.device_ctx(self.device_id):
1251
+ cufft.set_stream(self.handle, stream_holder.ptr)
1252
+
1253
+ self.operand, self.operand_backup = _copy_operand_perhaps(
1254
+ self.operand,
1255
+ operand,
1256
+ stream_holder,
1257
+ self.execution_space,
1258
+ self.memory_space,
1259
+ self.device_id,
1260
+ self.fft_abstract_type,
1261
+ self.logger,
1262
+ )
1263
+ operand = self.operand
1264
+
1265
+ # Update operand layout and plan traits.
1266
+ self.operand_layout = TensorLayout(shape=operand.shape, strides=operand.strides)
1267
+ self.logger.info(f"The reset operand shape = {self.operand_layout.shape}, and strides = {self.operand_layout.strides}.")
1268
+
1269
+ if distribution_type_old == "box":
1270
+ result_layout = self.distribution_layout[output_box]
1271
+ elif not self.options.reshape:
1272
+ result_layout = self.distribution_layout[Slab.X if distribution == Slab.Y else Slab.Y]
1273
+ else:
1274
+ result_layout = self.operand_layout
1275
+ self.result_shape = result_layout.shape
1276
+ self.result_strides = result_layout.strides
1277
+
1278
+ self.logger.info(f"The result shape = {self.result_shape}, and strides = {self.result_strides}.")
1279
+
1280
+ self.logger.info("The operand has been reset to the specified operand.")
1281
+
1282
+ def _check_planned(self, *args, **kwargs):
1283
+ """ """
1284
+ what = kwargs["what"]
1285
+ if not self.fft_planned:
1286
+ raise RuntimeError(f"{what} cannot be performed before plan() has been called.")
1287
+
1288
+ def _check_valid_operand(self, *args, **kwargs):
1289
+ """ """
1290
+ what = kwargs["what"]
1291
+ if self.operand is None:
1292
+ raise RuntimeError(
1293
+ f"{what} cannot be performed if the input operand has been set to None. Use reset_operand() to set the "
1294
+ f"desired input before using performing the {what.lower()}."
1295
+ )
1296
+
1297
+ def _free_workspace_memory(self, exception: Exception | None = None) -> bool:
1298
+ """
1299
+ Free workspace by releasing the MemoryPointer object and reshape operand.
1300
+ """
1301
+ if self.workspace_ptr is None:
1302
+ return True
1303
+
1304
+ with utils.device_ctx(self.device_id):
1305
+ self.workspace_ptr.free()
1306
+ if self.reshaped_operand is not None:
1307
+ nvshmem_free_wrapper(self.reshaped_operand.data_ptr)
1308
+ self.workspace_ptr = None
1309
+ self.reshaped_operand = None
1310
+ self.logger.debug("[_free_workspace_memory] The workspace has been released.")
1311
+
1312
+ return True
1313
+
1314
+ @utils.precondition(_check_valid_fft)
1315
+ @utils.precondition(_check_planned, "Workspace memory allocation")
1316
+ @utils.atomic(_free_workspace_memory, method=True)
1317
+ def _allocate_workspace_memory(self, stream_holder: StreamHolder):
1318
+ """
1319
+ Allocate workspace memory using the specified allocator.
1320
+ """
1321
+
1322
+ assert self._workspace_allocated_here is False, "Internal Error."
1323
+
1324
+ self.logger.debug("Allocating workspace for performing the FFT...")
1325
+ with utils.device_ctx(self.device_id), stream_holder.ctx:
1326
+ try:
1327
+ self.workspace_ptr = self.allocator.memalloc(self.workspace_size) # type: ignore[union-attr]
1328
+ if self.options.reshape:
1329
+ self.reshaped_operand = self._allocate_reshape_operand(
1330
+ stream_holder, self.logger.isEnabledFor(logging.DEBUG)
1331
+ )
1332
+ self._workspace_allocated_here = True
1333
+ except TypeError as e:
1334
+ message = (
1335
+ "The method 'memalloc' in the allocator object must conform to the interface in the "
1336
+ "'BaseCUDAMemoryManager' protocol."
1337
+ )
1338
+ raise TypeError(message) from e
1339
+ raw_workspace_ptr = utils.get_ptr_from_memory_pointer(self.workspace_ptr)
1340
+ cufft.set_work_area(self.handle, raw_workspace_ptr)
1341
+
1342
+ self.workspace_stream = stream_holder.obj
1343
+ self.logger.debug(
1344
+ f"Finished allocating device workspace of size {formatters.MemoryStr(self.workspace_size)} in the context "
1345
+ f"of stream {self.workspace_stream}."
1346
+ )
1347
+
1348
+ def _allocate_workspace_memory_perhaps(self, stream_holder: StreamHolder):
1349
+ """
1350
+ Allocate workspace memory using the specified allocator, if it hasn't already been
1351
+ done.
1352
+ """
1353
+ if self.execution_space != "cuda" or self.workspace_ptr is not None:
1354
+ return
1355
+
1356
+ return self._allocate_workspace_memory(stream_holder)
1357
+
1358
+ @utils.precondition(_check_valid_fft)
1359
+ def _free_workspace_memory_perhaps(self, release_workspace):
1360
+ """
1361
+ Free workspace memory if if 'release_workspace' is True.
1362
+ """
1363
+ if not release_workspace:
1364
+ return
1365
+
1366
+ # Establish ordering wrt the computation and free workspace if it's more than the
1367
+ # specified cache limit.
1368
+ if self.last_compute_event is not None:
1369
+ with utils.device_ctx(self.device_id):
1370
+ self.workspace_stream.wait(self.last_compute_event)
1371
+ self.logger.debug("Established ordering with respect to the computation before releasing the workspace.")
1372
+ self.last_compute_event = None
1373
+
1374
+ self.logger.debug("[_free_workspace_memory_perhaps] The workspace memory will be released.")
1375
+ self._free_workspace_memory()
1376
+
1377
+ return True
1378
+
1379
+ def _release_workspace_memory_perhaps(self, exception: Exception | None = None) -> bool:
1380
+ """
1381
+ Free workspace memory if it was allocated in this call
1382
+ (self._workspace_allocated_here == True) when an exception occurs.
1383
+ """
1384
+ release_workspace = self._workspace_allocated_here
1385
+ self.logger.debug(
1386
+ f"[_release_workspace_memory_perhaps] The release_workspace flag is set to {release_workspace} based upon "
1387
+ "the value of 'workspace_allocated_here'."
1388
+ )
1389
+ self._free_workspace_memory_perhaps(release_workspace)
1390
+ self._workspace_allocated_here = False
1391
+ return True
1392
+
1393
+ @utils.precondition(_check_valid_fft)
1394
+ @utils.precondition(_check_planned, "Execution")
1395
+ @utils.precondition(_check_valid_operand, "Execution")
1396
+ @utils.atomic(_release_workspace_memory_perhaps, method=True)
1397
+ def execute(
1398
+ self,
1399
+ *,
1400
+ direction: FFTDirection | None = None,
1401
+ stream: AnyStream | None = None,
1402
+ release_workspace: bool = False,
1403
+ sync_symmetric_memory: bool = True,
1404
+ ):
1405
+ """
1406
+ Execute the FFT operation.
1407
+
1408
+ Args:
1409
+ direction: {direction}
1410
+
1411
+ stream: {stream}
1412
+
1413
+ release_workspace: A value of `True` specifies that the FFT object
1414
+ should release workspace memory back to the symmetric memory pool on
1415
+ function return, while a value of `False` specifies that the object
1416
+ should retain the memory. This option may be set to `True` if the
1417
+ application performs other operations that consume a lot of memory between
1418
+ successive calls to the (same or different) :meth:`execute` API, but incurs
1419
+ an overhead due to obtaining and releasing workspace memory from and
1420
+ to the symmetric memory pool on every call. The default is `False`.
1421
+ **NOTE: All processes must use the same value or the application can
1422
+ deadlock.**
1423
+
1424
+ sync_symmetric_memory: {sync_symmetric_memory}
1425
+
1426
+ Returns:
1427
+ The transformed operand, which remains on the same device and utilizes the same
1428
+ package as the input operand. The data type and shape of the transformed operand
1429
+ depend on the type of input operand, and choice of distribution and reshape
1430
+ option:
1431
+
1432
+ - For C2C FFT, the data type remains identical to the input.
1433
+ - For slab distribution with reshape=True, the shape will remain identical.
1434
+ - For slab distribution with reshape=False, the shape will be the converse
1435
+ slab shape.
1436
+ - For custom box distribution, the shape will depend on the output box of
1437
+ each process.
1438
+
1439
+ For GPU operands, the result will be in symmetric memory and the user is
1440
+ responsible for explicitly deallocating it (for example, using
1441
+ ``nvmath.distributed.free_symmetric_memory(tensor)``).
1442
+ """
1443
+
1444
+ log_info = self.logger.isEnabledFor(logging.INFO)
1445
+ log_debug = self.logger.isEnabledFor(logging.DEBUG)
1446
+
1447
+ if direction is None:
1448
+ direction = _get_fft_default_direction(self.fft_abstract_type)
1449
+ else:
1450
+ direction = _get_validate_direction(direction, self.fft_abstract_type)
1451
+
1452
+ stream_holder: StreamHolder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
1453
+
1454
+ # Set stream for the FFT.
1455
+ with utils.device_ctx(self.device_id):
1456
+ cufft.set_stream(self.handle, stream_holder.ptr)
1457
+
1458
+ # Allocate workspace if needed.
1459
+ self._allocate_workspace_memory_perhaps(stream_holder)
1460
+ # cuFFTMp only supports inplace transform.
1461
+ result_ptr = self.operand.data_ptr
1462
+
1463
+ if log_info:
1464
+ self.logger.info(
1465
+ f"Starting distributed FFT {self.fft_abstract_type} calculation in the {direction.name} direction..." # type: ignore[union-attr]
1466
+ )
1467
+ self.logger.info(f"{self.call_prologue}")
1468
+
1469
+ with utils.cuda_call_ctx(stream_holder, self.blocking, timing=log_info) as (
1470
+ self.last_compute_event,
1471
+ elapsed,
1472
+ ):
1473
+ if log_debug:
1474
+ self.logger.debug("The cuFFTMp execution function is 'xt_exec_descriptor'.")
1475
+ if sync_symmetric_memory:
1476
+ nvshmem.sync_all_on_stream(stream_holder.ptr)
1477
+ if log_info:
1478
+ self.logger.info(
1479
+ "sync_symmetric_memory is enabled (this may incur redundant multi-GPU "
1480
+ "synchronization, please refer to the documentation for more information)"
1481
+ )
1482
+ elif log_info:
1483
+ self.logger.info("sync_symmetric_memory is disabled")
1484
+ cufft.set_descriptor_data(self.memory_desc, result_ptr, self.subformat)
1485
+ cufft.xt_exec_descriptor(self.handle, self.memory_desc, self.memory_desc, direction)
1486
+ if self.options.reshape:
1487
+ raw_workspace_ptr = utils.get_ptr_from_memory_pointer(self.workspace_ptr)
1488
+ assert self.reshaped_operand is not None
1489
+ cufft.exec_reshape_async(
1490
+ self.reshape_handle, self.reshaped_operand.data_ptr, result_ptr, raw_workspace_ptr, stream_holder.ptr
1491
+ )
1492
+ # Copy back to original GPU operand.
1493
+ self.operand.copy_(self.reshaped_operand, stream_holder=stream_holder)
1494
+
1495
+ if log_info and elapsed.data is not None:
1496
+ reshape_addendum = "along with output reshaping" if self.options.reshape else ""
1497
+ self.logger.info(f"The distributed FFT calculation {reshape_addendum} took {elapsed.data:.3f} ms to complete.")
1498
+
1499
+ # Establish ordering wrt the computation and free workspace if it's more than the
1500
+ # specified cache limit.
1501
+ self._free_workspace_memory_perhaps(release_workspace)
1502
+
1503
+ # reset workspace allocation tracking to False at the end of the methods where
1504
+ # workspace memory is potentially allocated. This is necessary to prevent any
1505
+ # exceptions raised before method entry from using stale tracking values.
1506
+ self._workspace_allocated_here = False
1507
+
1508
+ # Return the result.
1509
+ result = self.operand
1510
+
1511
+ if self.memory_space == self.execution_space:
1512
+ out = result
1513
+ else:
1514
+ self.operand_backup.copy_(result, stream_holder=stream_holder)
1515
+ out = self.operand_backup
1516
+
1517
+ if tuple(out.shape) != tuple(self.result_shape):
1518
+ # For cases where the output operand has a different shape than the input
1519
+ # operand (e.g. reshape=False, or shape(output_box) != shape(input_box)) and
1520
+ # since the output operand is a reference to the input operand (FFT transform
1521
+ # is inplace), we need to reshape the tensor (without copying) before returning
1522
+ # it to the user.
1523
+ out = out.reshape(self.result_shape, copy=False)
1524
+
1525
+ return out.tensor
1526
+
1527
+ def free(self):
1528
+ """Free FFT resources.
1529
+
1530
+ It is recommended that the :class:`FFT` object be used within a context, but if it
1531
+ is not possible then this method must be called explicitly to ensure that the FFT
1532
+ resources (especially internal library objects) are properly cleaned up.
1533
+ """
1534
+
1535
+ if not self.valid_state:
1536
+ return
1537
+
1538
+ try:
1539
+ # Future operations on the workspace stream should be ordered after the
1540
+ # computation.
1541
+ if self.last_compute_event is not None:
1542
+ with utils.device_ctx(self.device_id):
1543
+ self.workspace_stream.wait(self.last_compute_event)
1544
+ self.last_compute_event = None
1545
+
1546
+ self._free_workspace_memory()
1547
+
1548
+ with utils.device_ctx(self.device_id):
1549
+ if self.memory_desc is not None:
1550
+ if self.dummy_desc_data_ptr is not None:
1551
+ cufft.set_descriptor_data(self.memory_desc, self.dummy_desc_data_ptr, self.subformat)
1552
+ cufft.xt_free(self.memory_desc)
1553
+ self.memory_desc = None
1554
+
1555
+ if self.handle is not None:
1556
+ cufft.destroy(self.handle)
1557
+ if self.options.reshape:
1558
+ cufft.destroy_reshape(self.reshape_handle)
1559
+ self.handle = None
1560
+ self.reshape_handle = None
1561
+
1562
+ if self.memory_desc_handle is not None:
1563
+ cufft.destroy(self.memory_desc_handle)
1564
+ self.memory_desc_handle = None
1565
+
1566
+ if self.memory_space == "cpu" and self.operand is not None:
1567
+ # In this case, self.operand is an internal GPU operand owned by FFT.
1568
+ nvshmem_free_wrapper(self.operand.data_ptr)
1569
+ self.operand = None
1570
+ self.operand_backup = None
1571
+
1572
+ except Exception as e:
1573
+ self.logger.critical("Internal error: only part of the FFT object's resources have been released.")
1574
+ self.logger.critical(str(e))
1575
+ raise e
1576
+ finally:
1577
+ self.valid_state = False
1578
+
1579
+ self.logger.info("The FFT object's resources have been released.")
1580
+
1581
+
1582
+ def _fft(
1583
+ x,
1584
+ /,
1585
+ distribution: Slab | Sequence[Box],
1586
+ *,
1587
+ direction: FFTDirection | None = None,
1588
+ sync_symmetric_memory: bool = True,
1589
+ options: FFTOptions | None = None,
1590
+ stream: AnyStream | None = None,
1591
+ check_dtype: str | None = None,
1592
+ ):
1593
+ r"""
1594
+ fft({function_signature})
1595
+
1596
+ Perform an N-D *complex-to-complex* (C2C) distributed FFT on the provided complex
1597
+ operand.
1598
+
1599
+ Args:
1600
+ operand: {operand}
1601
+ {operand_admonitions}
1602
+
1603
+ distribution: {distribution}
1604
+
1605
+ sync_symmetric_memory: {sync_symmetric_memory}
1606
+
1607
+ options: {options}
1608
+
1609
+ stream: {stream}
1610
+
1611
+ Returns:
1612
+ A transformed operand that retains the same data type as the input. The resulting
1613
+ shape will depend on the choice of distribution and reshape option. The operand
1614
+ remains on the same device and uses the same package as the input operand.
1615
+
1616
+ See Also:
1617
+ :func:`ifft`, :class:`FFT`
1618
+
1619
+ Examples:
1620
+
1621
+ >>> import cupy as cp
1622
+ >>> import nvmath.distributed
1623
+
1624
+ Get MPI communicator used to initialize nvmath.distributed (for information on
1625
+ initializing nvmath.distributed, you can refer to the documentation or to the
1626
+ FFT examples in `nvmath/examples/distributed/fft
1627
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/fft>`_):
1628
+
1629
+ >>> comm = nvmath.distributed.get_context().communicator
1630
+ >>> nranks = comm.Get_size()
1631
+
1632
+ Create a 3-D complex128 ndarray on GPU symmetric memory, distributed according to
1633
+ the Slab distribution on the Y axis (the global shape is (256, 256, 256)):
1634
+
1635
+ >>> shape = 256, 256 // nranks, 256
1636
+ >>> dtype = cp.complex128
1637
+ >>> a = nvmath.distributed.allocate_symmetric_memory(shape, cp, dtype=dtype)
1638
+ >>> a[:] = cp.random.rand(*shape, dtype=cp.float64) + 1j * cp.random.rand(
1639
+ ... *shape, dtype=cp.float64
1640
+ ... )
1641
+
1642
+ Perform a 3-D C2C FFT using :func:`fft`. The result `r` is also a CuPy complex128
1643
+ ndarray:
1644
+
1645
+ >>> r = nvmath.distributed.fft.fft(a, distribution=nvmath.distributed.fft.Slab.Y)
1646
+
1647
+ See :class:`FFTOptions` for the complete list of available options.
1648
+
1649
+ The package current stream is used by default, but a stream can be explicitly
1650
+ provided to the FFT operation. This can be done if the FFT operand is computed on a
1651
+ different stream, for example:
1652
+
1653
+ >>> s = cp.cuda.Stream()
1654
+ >>> with s:
1655
+ ... a = nvmath.distributed.allocate_symmetric_memory(shape, cp, dtype=dtype)
1656
+ ... a[:] = cp.random.rand(*shape) + 1j * cp.random.rand(*shape)
1657
+ >>> r = nvmath.distributed.fft.fft(a, stream=s)
1658
+
1659
+ The operation above runs on stream `s` and is ordered with respect to the input
1660
+ computation.
1661
+
1662
+ Create a NumPy ndarray on the CPU.
1663
+
1664
+ >>> import numpy as np
1665
+ >>> b = np.random.rand(*shape) + 1j * np.random.rand(*shape)
1666
+
1667
+ Provide the NumPy ndarray to :func:`fft`, with the result also being a NumPy
1668
+ ndarray:
1669
+
1670
+ >>> r = nvmath.distributed.fft.fft(b, nvmath.distributed.fft.Slab.Y)
1671
+
1672
+ Notes:
1673
+ - This function is a convenience wrapper around :class:`FFT` and and is specifically
1674
+ meant for *single* use. The same computation can be performed with the stateful
1675
+ API using the default `direction` argument in :meth:`FFT.execute`.
1676
+
1677
+ Further examples can be found in the `nvmath/examples/distributed/fft
1678
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/fft>`_
1679
+ directory.
1680
+ """
1681
+ if check_dtype is not None:
1682
+ assert check_dtype in {"real", "complex"}, "internal error"
1683
+ operand = tensor_wrapper.wrap_operand(x)
1684
+ if ("complex" in operand.dtype) != (check_dtype == "complex"):
1685
+ raise ValueError(f"This function expects {check_dtype} operand, found {operand.dtype}")
1686
+
1687
+ with FFT(x, distribution, options=options, stream=stream) as fftobj:
1688
+ # Plan the FFT.
1689
+ fftobj.plan(stream=stream)
1690
+
1691
+ # Execute the FFT.
1692
+ result = fftobj.execute(direction=direction, stream=stream, sync_symmetric_memory=sync_symmetric_memory)
1693
+
1694
+ return result
1695
+
1696
+
1697
+ # Forward C2C FFT Function.
1698
+ fft = functools.wraps(_fft)(functools.partial(_fft, direction=FFTDirection.FORWARD, check_dtype="complex"))
1699
+ fft.__doc__ = fft.__doc__.format(**SHARED_FFT_DOCUMENTATION) # type: ignore
1700
+ fft.__name__ = "fft"
1701
+
1702
+
1703
+ # Inverse C2C FFT Function.
1704
+ ifft = functools.wraps(_fft)(functools.partial(_fft, direction=FFTDirection.INVERSE, check_dtype="complex"))
1705
+ ifft.__doc__ = """
1706
+ ifft({function_signature})
1707
+
1708
+ Perform an N-D *complex-to-complex* (C2C) inverse FFT on the provided complex operand.
1709
+ The direction is implicitly inverse.
1710
+
1711
+ Args:
1712
+ operand: {operand}
1713
+ {operand_admonitions}
1714
+
1715
+ distribution: {distribution}
1716
+
1717
+ sync_symmetric_memory: {sync_symmetric_memory}
1718
+
1719
+ options: {options}
1720
+
1721
+ stream: {stream}
1722
+
1723
+ Returns:
1724
+ A transformed operand that retains the same data type as the input. The resulting
1725
+ shape will depend on the choice of distribution and reshape option. The operand
1726
+ remains on the same device and uses the same package as the input operand.
1727
+
1728
+ See Also:
1729
+ :func:`fft`, :class:`FFT`.
1730
+
1731
+ Notes:
1732
+ - This function is a convenience wrapper around :class:`FFT` and and is specifically
1733
+ meant for *single* use. The same computation can be performed with the stateful
1734
+ API by passing the argument ``direction='inverse'`` when calling
1735
+ :meth:`FFT.execute`.
1736
+ """.format(**SHARED_FFT_DOCUMENTATION)
1737
+ ifft.__name__ = "ifft"