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
nvmath/fft/fft.py ADDED
@@ -0,0 +1,2346 @@
1
+ # Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ __all__ = ["FFT", "fft", "ifft", "rfft", "irfft", "UnsupportedLayoutError"]
6
+
7
+ from typing import Literal
8
+ from collections.abc import Sequence
9
+ from dataclasses import dataclass, astuple as data_cls_astuple
10
+ import enum
11
+ import functools
12
+ import logging
13
+ import math
14
+ import operator
15
+
16
+ from ._configuration import ExecutionCPU, ExecutionCUDA, FFTOptions, FFTDirection, DeviceCallable
17
+
18
+ from nvmath.bindings import cufft # type: ignore
19
+
20
+ try:
21
+ from nvmath.bindings.nvpl import fft as fftw # type: ignore
22
+ except ImportError:
23
+ fftw = None # type: ignore
24
+ from nvmath.bindings._internal import utils as _bindings_utils # type: ignore
25
+ from nvmath.fft._exec_utils import _cross_setup_execution_and_options
26
+ from nvmath import memory
27
+
28
+ from nvmath.internal import formatters
29
+ from nvmath.internal import tensor_wrapper
30
+ from nvmath.internal.typemaps import (
31
+ NAME_TO_DATA_TYPE,
32
+ DATA_TYPE_TO_NAME,
33
+ FFTW_SUPPORTED_SINGLE,
34
+ FFTW_SUPPORTED_DOUBLE,
35
+ FFTW_SUPPORTED_TYPES,
36
+ FFTW_SUPPORTED_FLOAT,
37
+ FFTW_SUPPORTED_COMPLEX,
38
+ )
39
+ from nvmath.internal import utils
40
+ from nvmath._internal.layout import is_contiguous_layout, is_contiguous_in_memory, is_overlapping_layout
41
+ from nvmath.internal.package_wrapper import AnyStream, StreamHolder
42
+
43
+
44
+ class UnsupportedLayoutError(Exception):
45
+ """
46
+ Error type for layouts not supported by the library.
47
+
48
+ Args:
49
+ message: The error message.
50
+
51
+ permutation: The permutation needed to convert the input layout to a supported
52
+ layout to the FFT operation. The same permutation needs to be applied to the
53
+ result to obtain the axis sequence corresponding to the non-permuted input.
54
+
55
+ axes: The dimensions along which the FFT is performed corresponding to the permuted
56
+ operand layout.
57
+ """
58
+
59
+ def __init__(self, message, permutation, axes):
60
+ self.message = message
61
+ self.permutation = permutation
62
+ self.axes = axes
63
+
64
+ def __str__(self):
65
+ return self.message
66
+
67
+
68
+ @dataclass
69
+ class TensorLayout:
70
+ """An internal data class for capturing the tensor layout."""
71
+
72
+ shape: Sequence[int]
73
+ strides: Sequence[int]
74
+
75
+
76
+ @dataclass
77
+ class PlanTraits:
78
+ """An internal data class for capturing FFT plan traits."""
79
+
80
+ result_shape: Sequence[int]
81
+ result_strides: Sequence[int]
82
+ ordered_axes: Sequence[int]
83
+ ordered_fft_in_shape: Sequence[int]
84
+ ordered_fft_in_embedding_shape: Sequence[int]
85
+ ordered_fft_out_shape: Sequence[int]
86
+ fft_batch_size: int
87
+ istride: int
88
+ idistance: int
89
+ ostride: int
90
+ odistance: int
91
+
92
+
93
+ class CBLoadType(enum.IntEnum):
94
+ COMPLEX64 = (0x0,)
95
+ COMPLEX128 = (0x1,)
96
+ FLOAT32 = (0x2,)
97
+ FLOAT64 = (0x3,)
98
+ UNDEFINED = 0x8
99
+
100
+
101
+ class CBStoreType(enum.IntEnum):
102
+ COMPLEX64 = (0x4,)
103
+ COMPLEX128 = (0x5,)
104
+ FLOAT32 = (0x6,)
105
+ FLOAT64 = (0x7,)
106
+ UNDEFINED = 0x8
107
+
108
+
109
+ SHARED_FFT_DOCUMENTATION = utils.COMMON_SHARED_DOC_MAP.copy()
110
+ SHARED_FFT_DOCUMENTATION.update(
111
+ {
112
+ "axes": """\
113
+ The dimensions along which the FFT is performed. ``axes[-1]`` is the 'last transformed' axis for rffts. Currently, it is
114
+ required that the axes are contiguous and include the first or the last dimension. Only up to 3D FFTs are
115
+ supported.""".replace("\n", " "),
116
+ #
117
+ "options": """\
118
+ Specify options for the FFT as a :class:`FFTOptions` object. Alternatively, a `dict` containing the parameters for the
119
+ ``FFTOptions`` constructor can also be provided. If not specified, the value will be set to the default-constructed
120
+ ``FFTOptions`` object.""".replace("\n", " "),
121
+ #
122
+ "execution": """\
123
+ Specify execution space options for the FFT as a :class:`ExecutionCUDA` or :class:`ExecutionCPU` object. Alternatively,
124
+ a string ('cuda' or 'cpu'), or a `dict` with the 'name' key set to 'cpu' or 'cuda' and optional parameters relevant to
125
+ the given execution space. If not specified, the execution space will be selected to match operand's storage (in GPU or
126
+ host memory), and the corresponding :class:`ExecutionCUDA` or :class:`ExecutionCPU` object will be
127
+ default-constructed.""".replace("\n", " "),
128
+ #
129
+ "prolog": """\
130
+ Provide device-callable function in LTO-IR format to use as load-callback as an object of type :class:`DeviceCallable`.
131
+ Alternatively, a `dict` containing the parameters for the ``DeviceCallable`` constructor can also be provided. The
132
+ default is no prolog. Currently, callbacks are supported only with CUDA execution.""".replace("\n", " "),
133
+ #
134
+ "epilog": """\
135
+ Provide device-callable function in LTO-IR format to use as store-callback as an object of type :class:`DeviceCallable`.
136
+ Alternatively, a `dict` containing the parameters for the ``DeviceCallable`` constructor can also be provided. The
137
+ default is no epilog. Currently, callbacks are supported only with CUDA execution.""".replace("\n", " "),
138
+ #
139
+ "direction": """\
140
+ Specify whether forward or inverse FFT is performed (:class:`FFTDirection` object, or as a string from ['forward',
141
+ 'inverse'], "or as an int from [-1, 1] denoting forward and inverse directions respectively).""".replace("\n", " "),
142
+ #
143
+ "fft_key": """\
144
+ A tuple as the key to represent the input FFT problem.""".replace("\n", " "),
145
+ #
146
+ "function_signature": """\
147
+ operand,
148
+ axes: Sequence[int] | None = None,
149
+ options: FFTOptions | None = None,
150
+ execution: ExecutionCPU | ExecutionCUDA | None = None,
151
+ prolog: DeviceCallable | None = None,
152
+ epilog: DeviceCallable | None = None,
153
+ stream: AnyStream | None = None
154
+ """.replace("\n", " "),
155
+ }
156
+ )
157
+
158
+
159
+ def complex_to_real_equivalent(name):
160
+ assert "complex" in name, f"Internal Error ({name=})"
161
+ m = name.split("complex")
162
+ assert len(m) in (1, 2)
163
+ size = int(m[-1]) // 2
164
+ if len(m) == 1:
165
+ return f"float{size}"
166
+ else:
167
+ return f"{m[0]}float{size}"
168
+
169
+
170
+ def real_to_complex_equivalent(name):
171
+ assert "float" in name, f"Internal Error ({name=})"
172
+ m = name.split("float")
173
+ assert len(m) in (1, 2)
174
+ size = int(m[-1])
175
+ if len(m) == 1:
176
+ return f"complex{size * 2}"
177
+ else:
178
+ return f"{m[0]}complex{size * 2}"
179
+
180
+
181
+ def _get_default_fft_abstract_type(dtype, fft_type):
182
+ if fft_type is not None:
183
+ return fft_type
184
+
185
+ f, c = "float", "complex"
186
+ if dtype[: len(f)] == f:
187
+ fft_type = "R2C"
188
+ elif dtype[: len(c)] == c:
189
+ fft_type = "C2C"
190
+ else:
191
+ raise ValueError(f"Unsupported dtype '{dtype}' for FFT.")
192
+ return fft_type
193
+
194
+
195
+ def _get_fft_result_and_compute_types(dtype, fft_abstract_type):
196
+ """
197
+ Return result and compute data type given the input data type and the FFT type.
198
+ """
199
+ if fft_abstract_type == "C2C":
200
+ return dtype, dtype
201
+ elif fft_abstract_type == "C2R":
202
+ return complex_to_real_equivalent(dtype), dtype
203
+ elif fft_abstract_type == "R2C":
204
+ return real_to_complex_equivalent(dtype), dtype
205
+ else:
206
+ raise ValueError(f"Unsupported FFT Type: '{fft_abstract_type}'")
207
+
208
+
209
+ def _get_fft_default_direction(fft_abstract_type):
210
+ """
211
+ Return the default FFT direction (as object of type configuration.FFTDirection) based on
212
+ the FFT type.
213
+ """
214
+ if fft_abstract_type in ["C2C", "R2C"]:
215
+ return FFTDirection.FORWARD
216
+ elif fft_abstract_type == "C2R":
217
+ return FFTDirection.INVERSE
218
+ else:
219
+ raise ValueError(f"Unsupported FFT Type: '{fft_abstract_type}'")
220
+
221
+
222
+ def _get_size(shape):
223
+ return functools.reduce(operator.mul, shape, 1)
224
+
225
+
226
+ def _get_last_axis_id_and_size(
227
+ axes: Sequence[int],
228
+ operand_shape: Sequence[int],
229
+ fft_abstract_type: Literal["C2C", "C2R", "R2C"],
230
+ last_axis_parity: Literal["even", "odd"],
231
+ ) -> tuple[int, int]:
232
+ """
233
+ Args:
234
+ axes: The user-specified or default FFT axes.
235
+
236
+ operand_shape: The input operand shape.
237
+
238
+ fft_abstract_type: The "abstract" type of the FFT ('C2C', 'C2R', 'R2C').
239
+
240
+ last_axis_parity: For 'C2R' FFTs, specify whether the last axis size is even or odd.
241
+
242
+ Returns the last axis ID and the corresponding axis size required for the result.
243
+ """
244
+ last_axis_id = axes[-1]
245
+
246
+ if fft_abstract_type == "C2C":
247
+ return last_axis_id, operand_shape[last_axis_id]
248
+
249
+ if fft_abstract_type == "C2R":
250
+ if last_axis_parity == "even":
251
+ return last_axis_id, 2 * (operand_shape[last_axis_id] - 1)
252
+ elif last_axis_parity == "odd":
253
+ return last_axis_id, 2 * operand_shape[last_axis_id] - 1
254
+ else:
255
+ raise AssertionError("Unreachable.")
256
+
257
+ if fft_abstract_type == "R2C":
258
+ return last_axis_id, operand_shape[last_axis_id] // 2 + 1
259
+
260
+
261
+ def check_inplace_overlapping_layout(operand: utils.TensorHolder):
262
+ if is_overlapping_layout(operand.shape, operand.strides):
263
+ raise ValueError(
264
+ f"In-place transform is not supported because the tensor with shape "
265
+ f"{operand.shape} and strides {operand.strides} overlaps in memory."
266
+ )
267
+
268
+
269
+ def check_embedding_possible(strides, presorted=False):
270
+ """
271
+ Check if the strides allow for calculating an embedding dimension.
272
+ """
273
+ if not presorted:
274
+ strides = sorted(strides)
275
+ # with a broadcasted view, stride can be 0
276
+ if any(strides[i - 1] == 0 for i in range(1, len(strides))):
277
+ return False
278
+ return all(strides[i] % strides[i - 1] == 0 for i in range(1, len(strides)))
279
+
280
+
281
+ def check_batch_tileable(sorted_batch_shape, sorted_batch_strides):
282
+ """
283
+ Check if FFT layout is tileable across the specified batch layout.
284
+ """
285
+ return is_contiguous_layout(sorted_batch_shape, sorted_batch_strides)
286
+
287
+
288
+ def check_contiguous_layout(axes, strides, shape):
289
+ if not axes:
290
+ return True
291
+ sorted_batch_strides, sorted_batch_shape = zip(*sorted((strides[a], shape[a]) for a in axes), strict=True)
292
+ return is_contiguous_layout(sorted_batch_shape, sorted_batch_strides)
293
+
294
+
295
+ def calculate_embedding_shape(shape: Sequence[int], strides: Sequence[int]):
296
+ """
297
+ Calculate the embedding shape for the given shape and strides.
298
+ """
299
+ n = len(strides)
300
+ # The shape is used to resolve cases like (1, 2, 1) : (2, 1, 1) in CuTe notation.
301
+ ordered_strides, _, order = zip(*sorted(zip(strides, shape, range(n), strict=True)), strict=True)
302
+
303
+ ordered_shape = [ordered_strides[i] // ordered_strides[i - 1] for i in range(1, len(ordered_strides))] + [shape[order[-1]]]
304
+
305
+ embedding_shape = [0] * n
306
+ for o in range(n):
307
+ embedding_shape[order[o]] = ordered_shape[o]
308
+
309
+ return embedding_shape, order
310
+
311
+
312
+ def axis_order_in_memory(shape, strides):
313
+ """
314
+ Compute the order in which the axes appear in memory.
315
+ """
316
+ # The shape is used to resolve cases like (1, 2, 1) : (2, 1, 1) in CuTe notation.
317
+ _, _, axis_order = zip(*sorted(zip(strides, shape, range(len(strides)), strict=True)), strict=True)
318
+
319
+ return axis_order
320
+
321
+
322
+ def calculate_strides(shape, axis_order):
323
+ """
324
+ Calculate the strides for the provided shape and axis order.
325
+ """
326
+ strides = [None] * len(shape)
327
+
328
+ stride = 1
329
+ for axis in axis_order:
330
+ strides[axis] = stride
331
+ stride *= shape[axis]
332
+
333
+ return strides
334
+
335
+
336
+ def unsupported_layout_exception(operand_dim, axes, message, logger):
337
+ logger.error(message)
338
+
339
+ permutation = tuple(a for a in range(operand_dim) if a not in axes) + tuple(axes)
340
+ fft_dim = len(axes)
341
+ axes = tuple(range(operand_dim - fft_dim, operand_dim))
342
+
343
+ message = (
344
+ f"To convert to a supported layout, create a transposed view using transpose{permutation} and copy the "
345
+ f"view into a new tensor, using view.copy() for instance, and use axes={axes}."
346
+ )
347
+ logger.error(message)
348
+
349
+ raise UnsupportedLayoutError(message, permutation, axes)
350
+
351
+
352
+ def get_null_logger(name):
353
+ logger = logging.getLogger(name)
354
+ logger.addHandler(logging.NullHandler())
355
+ logger.propagate = False
356
+
357
+ return logger
358
+
359
+
360
+ def get_fft_plan_traits(
361
+ operand_shape: Sequence[int],
362
+ operand_strides: Sequence[int],
363
+ operand_dtype,
364
+ axes: Sequence[int],
365
+ execution: ExecutionCUDA | ExecutionCPU,
366
+ *,
367
+ fft_abstract_type: Literal["C2C", "C2R", "R2C"] = "C2C",
368
+ last_axis_parity: Literal["even", "odd"] = "even",
369
+ result_layout: Literal["optimized", "natural"] = "optimized",
370
+ logger: logging.Logger | None = None,
371
+ ) -> PlanTraits:
372
+ """
373
+ Extract the FFT shape from the operand shape, compute the ordered axes so that the data
374
+ is C-contiguous in memory, and compute the result shape and strides.
375
+
376
+ Args:
377
+ operand_shape: The operand shape
378
+
379
+ operand_strides: The operand strides
380
+
381
+ axes: The axes over which the FFT is performed. For R2C and C2R transforms, the size
382
+ of the last axis in `axes` will change.
383
+
384
+ execution: The execution options, an instance of either ExecutionCUDA or
385
+ ExecutionCPU class.
386
+
387
+ fft_abstract_type: The "abstract" type of the FFT ('C2C', 'C2R', 'R2C').
388
+
389
+ last_axis_parity: For 'C2R' FFTs, specify whether the last axis size is even or odd.
390
+
391
+ The data needed for creating a cuFFT plan is returned in the following order:
392
+ (result_shape, result_strides), ordered_axes, ordered_fft_in_shape,
393
+ ordered_fft_out_shape, (istride, idistance), (ostride, odistance)
394
+ """
395
+ logger = logger if logger is not None else get_null_logger("get_fft_plan_traits_null")
396
+
397
+ if len(axes) > 3:
398
+ raise ValueError(
399
+ "Only up to 3D FFTs are currently supported. You can use the 'axes' option to specify up to three axes "
400
+ f"along which to perform the FFT. The current number of dimensions is {len(axes)} corresponding to the "
401
+ f"axes {axes}."
402
+ )
403
+
404
+ # Check for duplicate axis IDs.
405
+ if len(axes) != len(set(axes)):
406
+ raise ValueError(f"The specified FFT axes = {axes} contains duplicate axis IDs, which is not supported.")
407
+
408
+ operand_dim = len(operand_shape)
409
+ batch_axes = [axis for axis in range(operand_dim) if axis not in axes]
410
+
411
+ # Check if an embedding is possible for the provided operand layout.
412
+ if not check_embedding_possible(operand_strides):
413
+ message = (
414
+ f"The operand layout corresponding to shape = {operand_shape} and strides = {operand_strides} is "
415
+ "not currently supported because it does not have a suitable embedding dimension."
416
+ )
417
+ unsupported_layout_exception(operand_dim, axes, message, logger)
418
+
419
+ # Compute the embedding shape for the operand.
420
+ operand_embedding_shape, axis_order = calculate_embedding_shape(operand_shape, operand_strides)
421
+ logger.debug(f"The operand embedding shape = {operand_embedding_shape}.")
422
+
423
+ # The first or the last *ordered* axis must be present in the specified axes to be able
424
+ # to use the "advanced" layout.
425
+ first, last = axis_order[-1], axis_order[0]
426
+ if first not in axes and last not in axes:
427
+ raise ValueError(
428
+ f"The first ({first}) or the last ({last}) tensor axis in stride order {axis_order} must be present in the "
429
+ f"specified FFT axes {axes}."
430
+ )
431
+
432
+ # Compute the embedding input shape for the FFT.
433
+ fft_in_embedding_shape = [operand_embedding_shape[a] for a in axes]
434
+
435
+ # Compute the input shape for the FFT.
436
+ fft_in_shape, fft_in_strides = zip(*[(operand_shape[a], operand_strides[a]) for a in axes], strict=True)
437
+ if not is_contiguous_in_memory(fft_in_embedding_shape, fft_in_strides):
438
+ message = (
439
+ f"The FFT axes {axes} cannot be reordered so that the data is contiguous in memory for "
440
+ f"operand shape = {operand_shape} and operand strides = {operand_strides}."
441
+ )
442
+ unsupported_layout_exception(operand_dim, axes, message, logger)
443
+
444
+ # Reorder the FFT axes and input shape so that they are contiguous or separated by
445
+ # constant stride in memory.
446
+ quadruple = sorted(
447
+ zip(fft_in_strides, fft_in_shape, fft_in_embedding_shape, axes, strict=True), key=lambda v: v[:2], reverse=True
448
+ )
449
+
450
+ ordered_in_strides, ordered_fft_in_shape, ordered_fft_in_embedding_shape, ordered_axes = zip(*quadruple, strict=True)
451
+
452
+ # Check if R2C and C2R can be supported without copying.
453
+ if fft_abstract_type in ["R2C", "C2R"] and ordered_axes[-1] != axes[-1]:
454
+ message = (
455
+ f"The last FFT axis specified ({axes[-1]}) must have the smallest stride of all the FFT axes' "
456
+ f"strides {fft_in_strides} for FFT type '{fft_abstract_type}'."
457
+ )
458
+ unsupported_layout_exception(operand_dim, axes, message, logger)
459
+
460
+ # Input FFT size and batch size.
461
+ fft_in_size = _get_size(fft_in_shape)
462
+ if fft_in_size == 0:
463
+ raise ValueError("Invalid number of FFT data points (0) specified.")
464
+ fft_batch_size = _get_size(operand_shape) // fft_in_size
465
+
466
+ # Output FFT (ordered) shape and size.
467
+ last_axis_id, last_axis_size = _get_last_axis_id_and_size(axes, operand_shape, fft_abstract_type, last_axis_parity)
468
+ if last_axis_size == 0:
469
+ raise ValueError(
470
+ f"The size of the last FFT axis in the result for FFT type '{fft_abstract_type}' is 0 for operand shape = "
471
+ f"{operand_shape} and axes = {axes}. To fix this, provide 'last_axis_parity' = 'odd' to the FFT options."
472
+ )
473
+ ordered_fft_out_shape = list(ordered_fft_in_shape)
474
+ index = ordered_axes.index(last_axis_id)
475
+ ordered_fft_out_shape[index] = last_axis_size
476
+ fft_out_size = _get_size(ordered_fft_out_shape)
477
+
478
+ # Check that batch dimensions are tileable, as required by the "advanced" layout.
479
+ sorted_batch_shape: Sequence[int] = []
480
+ sorted_batch_strides: Sequence[int] = []
481
+ if batch_axes:
482
+ sorted_batch_strides, sorted_batch_shape = zip(
483
+ *sorted((operand_strides[a], operand_shape[a]) for a in batch_axes), strict=True
484
+ )
485
+ if not check_embedding_possible(sorted_batch_strides, presorted=True):
486
+ raise ValueError(
487
+ f"The operand layout corresponding to shape = {operand_shape} and strides = {operand_strides} "
488
+ f"together with the specified axes = {axes} is currently not supported because it is not tileable."
489
+ )
490
+ logger.debug(f"The sorted batch shape is {sorted_batch_shape}.")
491
+ logger.debug(f"The sorted batch strides are {sorted_batch_strides}.")
492
+ if not check_batch_tileable(sorted_batch_shape, sorted_batch_strides):
493
+ message = (
494
+ f"The operand layout corresponding to shape = {operand_shape} and strides = {operand_strides} "
495
+ f"together with the specified axes = {axes} is currently not supported because it is not tileable."
496
+ )
497
+ unsupported_layout_exception(operand_dim, axes, message, logger)
498
+ logger.debug(
499
+ f"The operand layout corresponding to shape = {operand_shape} and strides = {operand_strides} together with "
500
+ f"the specified axes = {axes} IS tileable."
501
+ )
502
+
503
+ # The result tensor has updated shape for R2C and C2R transforms.
504
+ result_shape = list(operand_shape)
505
+ result_shape[last_axis_id] = last_axis_size
506
+
507
+ # The result tensor layout is either natural or chosen for optimal cuFFT performance,
508
+ # based on the operand layout and user-provided option.
509
+
510
+ # We can keep the input's layout (i.e. operand's extents order of increasing strides)
511
+ # without performance hit, if the samples do not interleave.
512
+ # Otherwise, we try to keep it only when explicitly asked (result_layout=natural)
513
+ is_sample_interleaved = sorted_batch_strides and sorted_batch_strides[0] <= ordered_in_strides[0]
514
+ logger.debug(f"Are the samples interleaved? {bool(is_sample_interleaved)}.")
515
+
516
+ if not is_sample_interleaved or result_layout == "natural": # Natural (== operand) layout.
517
+ axis_order = axis_order_in_memory(operand_shape, operand_strides)
518
+ result_strides = calculate_strides(result_shape, axis_order)
519
+ # If the resulting output operand is not tilable, keeping the original layout is not
520
+ # possible. If `not is_sample_interleaved` the batch must be tilable, because the
521
+ # min batch stride is bigger than max fft stride
522
+ if is_sample_interleaved:
523
+ if not check_contiguous_layout(batch_axes, result_strides, result_shape):
524
+ message = (
525
+ f"The operand layout corresponding to shape = {operand_shape} and strides = {operand_strides} "
526
+ f"together with the specified axes = {axes} is currently not supported with "
527
+ "result_layout='natural', because the output batch would not be tileable."
528
+ )
529
+ unsupported_layout_exception(operand_dim, axes, message, logger)
530
+ if not check_contiguous_layout(axes, result_strides, result_shape):
531
+ message = (
532
+ f"The operand layout corresponding to shape = {operand_shape} and strides = {operand_strides} "
533
+ f"together with the specified axes = {axes} is currently not supported with "
534
+ "result_layout='natural', because the output sample would be non-contiguous."
535
+ )
536
+ unsupported_layout_exception(operand_dim, axes, message, logger)
537
+ else: # Optimized layout.
538
+ axis_order = tuple(
539
+ list(reversed(ordered_axes)) + sorted((a for a in batch_axes), key=lambda v: (operand_strides[v], operand_shape[v]))
540
+ )
541
+ result_strides = calculate_strides(result_shape, axis_order)
542
+ logger.debug(f"The result layout is '{result_layout}' with the result_strides {result_strides}.")
543
+
544
+ # Compute the operand linear stride and distance needed for the cuFFT plan.
545
+ last_ordered_in_stride = ordered_in_strides[-1]
546
+ min_in_stride = min(operand_strides)
547
+
548
+ if last_ordered_in_stride == min_in_stride:
549
+ istride, idistance = (
550
+ min_in_stride,
551
+ _get_size(ordered_fft_in_embedding_shape) if not sorted_batch_strides else sorted_batch_strides[0],
552
+ )
553
+ else:
554
+ istride, idistance = (
555
+ last_ordered_in_stride,
556
+ min_in_stride if not sorted_batch_strides else sorted_batch_strides[0],
557
+ )
558
+
559
+ # Compute the result linear stride and distance needed for the cuFFT plan.
560
+ ostride = result_strides[ordered_axes[-1]] # minimal output fft stride
561
+ odistance = fft_out_size if not batch_axes else min(result_strides[axis] for axis in batch_axes)
562
+
563
+ if execution.name == "cpu":
564
+ if fft_out_size == 1:
565
+ istride = ostride = 1
566
+ else:
567
+ assert execution.name == "cuda"
568
+ if operand_dtype in ("float16", "complex32"):
569
+ if fft_abstract_type == "R2C" and istride != 1:
570
+ raise ValueError(
571
+ f"The {fft_abstract_type} FFT of half-precision tensor ({operand_dtype}) "
572
+ f"is currently not supported for strided inputs "
573
+ f"(got input stride {istride})."
574
+ )
575
+ if fft_abstract_type == "C2R" and ostride != 1:
576
+ raise ValueError(
577
+ f"The {fft_abstract_type} FFT of half-precision tensor ({operand_dtype}) "
578
+ f"is currently not supported for strided outputs "
579
+ f"(got output stride {ostride})."
580
+ )
581
+ if fft_out_size == 1:
582
+ if cufft.get_version() < 10702: # 10702 is shipped with CTK 11.7
583
+ raise ValueError(
584
+ f"The FFT of sample size 1 and half-precision type ({operand_dtype}) "
585
+ f"of size 1 is not supported by the installed cuFFT version. "
586
+ )
587
+ # There is a bug that leads to invalid memory access (CTK 12.1) for
588
+ # one-element, strided C2C complex32 tensors (either in the input or output)
589
+ # or results in CUFFT_INVALID_SIZE (CTK 12.3). This workaround relies on the
590
+ # fact that the [i|o]stride effectively does not matter in a one-element
591
+ # sample.
592
+ elif fft_abstract_type == "C2C":
593
+ istride = ostride = 1
594
+
595
+ # There's a bug in cuFFT in CTKs prior to 11.4U2
596
+ if len(axes) == 3 and fft_batch_size > 1 and cufft.get_version() < 10502:
597
+ raise ValueError(
598
+ "The 3D batched FFT is not supported by the installed cuFFT version. "
599
+ "Please update your CUDA Toolkit (to 11.4.2 or newer)"
600
+ )
601
+
602
+ plan_traits = PlanTraits(
603
+ result_shape=tuple(result_shape),
604
+ result_strides=tuple(result_strides),
605
+ ordered_axes=tuple(ordered_axes),
606
+ ordered_fft_in_shape=tuple(ordered_fft_in_shape),
607
+ ordered_fft_in_embedding_shape=tuple(ordered_fft_in_embedding_shape),
608
+ ordered_fft_out_shape=tuple(ordered_fft_out_shape),
609
+ fft_batch_size=fft_batch_size,
610
+ istride=istride,
611
+ idistance=idistance,
612
+ ostride=ostride,
613
+ odistance=odistance,
614
+ )
615
+ return plan_traits
616
+
617
+
618
+ def _copy_operand_perhaps(
619
+ internal_operand,
620
+ operand: utils.TensorHolder,
621
+ stream_holder,
622
+ execution_space,
623
+ memory_space,
624
+ device_id: int | Literal["cpu"],
625
+ fft_abstract_type,
626
+ logger,
627
+ ):
628
+ if execution_space == memory_space:
629
+ if fft_abstract_type != "C2R":
630
+ return operand, None
631
+ else:
632
+ # For C2R, we need to take a copy to avoid input being overwritten
633
+ logger.info("For C2R FFT with input operand on GPU, the input is copied to avoid being overwritten by cuFFT.")
634
+ operand_copy = utils.create_empty_tensor(
635
+ operand.__class__,
636
+ operand.shape,
637
+ operand.dtype,
638
+ device_id,
639
+ stream_holder,
640
+ verify_strides=True,
641
+ strides=operand.strides,
642
+ )
643
+ operand_copy.copy_(operand, stream_holder=stream_holder)
644
+ # We don't need to keep the operand backup, because C2R precludes `inplace=True`
645
+ return operand_copy, None
646
+ else:
647
+ # Copy the `operand` to memory that matches the exec space
648
+ # and keep the original `operand` to handle `options.inplace=True`
649
+ if internal_operand is None:
650
+ if execution_space == "cuda":
651
+ assert isinstance(device_id, int)
652
+ to_device: int | Literal["cpu"] = device_id
653
+ else:
654
+ assert execution_space == "cpu"
655
+ to_device = "cpu"
656
+ exec_space_copy = operand.to(to_device, stream_holder)
657
+ return exec_space_copy, operand
658
+ else:
659
+ # In-place copy to existing pointer
660
+ tensor_wrapper.copy_([operand], [internal_operand], stream_holder)
661
+ return internal_operand, operand
662
+
663
+
664
+ def create_xt_plan_args(*, plan_traits=None, fft_abstract_type=None, operand_data_type=None, inplace=None):
665
+ """
666
+ Create the arguments to xt_make_plan_many() except for the handle. This is also used for
667
+ computing the FFT key.
668
+ """
669
+ assert plan_traits is not None, "Internal error."
670
+ assert fft_abstract_type is not None, "Internal error."
671
+ assert operand_data_type is not None, "Internal error."
672
+ assert inplace is not None, "Internal error."
673
+
674
+ result_data_type, compute_data_type = _get_fft_result_and_compute_types(operand_data_type, fft_abstract_type)
675
+
676
+ # The input shape to the plan should be the logical FFT shape.
677
+ ordered_plan_shape = plan_traits.ordered_fft_out_shape if fft_abstract_type == "C2R" else plan_traits.ordered_fft_in_shape
678
+
679
+ # Handle in-place transforms.
680
+ if inplace:
681
+ ordered_fft_out_shape, ostride, odistance = (
682
+ plan_traits.ordered_fft_in_embedding_shape,
683
+ plan_traits.istride,
684
+ plan_traits.idistance,
685
+ )
686
+ else:
687
+ ordered_fft_out_shape, ostride, odistance = (
688
+ plan_traits.ordered_fft_out_shape,
689
+ plan_traits.ostride,
690
+ plan_traits.odistance,
691
+ )
692
+
693
+ return (
694
+ len(ordered_plan_shape),
695
+ ordered_plan_shape,
696
+ plan_traits.ordered_fft_in_embedding_shape,
697
+ plan_traits.istride,
698
+ plan_traits.idistance,
699
+ NAME_TO_DATA_TYPE[operand_data_type],
700
+ ordered_fft_out_shape,
701
+ ostride,
702
+ odistance,
703
+ NAME_TO_DATA_TYPE[result_data_type],
704
+ plan_traits.fft_batch_size,
705
+ NAME_TO_DATA_TYPE[compute_data_type],
706
+ )
707
+
708
+
709
+ def fftw_plan_args(xt_plan_args, operand_ptr, result_ptr, fft_abstract_type, direction):
710
+ """
711
+ Create the arguments for fftw API based on the args created by create_xt_plan_args and
712
+ pointers to the input and the output tensors. Note, that while the pointers to the data
713
+ are required in planning, different pointers may be passed to the same plan in
714
+ subsequent execute call (assuming dtype, memory layout, alignment, and inplace
715
+ properties do not change).
716
+ """
717
+ (
718
+ rank,
719
+ n,
720
+ inembed,
721
+ istride,
722
+ idist,
723
+ input_t,
724
+ onembed,
725
+ ostride,
726
+ odist,
727
+ output_t,
728
+ batch,
729
+ execution_t,
730
+ ) = xt_plan_args
731
+
732
+ if input_t in FFTW_SUPPORTED_SINGLE:
733
+ assert output_t in FFTW_SUPPORTED_SINGLE, "Expected single precision output for single precision input"
734
+ precision = fftw.Precision.FLOAT
735
+ elif input_t in FFTW_SUPPORTED_DOUBLE:
736
+ assert output_t in FFTW_SUPPORTED_DOUBLE, "Expected double precision output for double precision input"
737
+ precision = fftw.Precision.DOUBLE
738
+ else:
739
+ supported_types_str = ", ".join(DATA_TYPE_TO_NAME[dtype] for dtype in FFTW_SUPPORTED_TYPES)
740
+ raise ValueError(
741
+ f"Currently, the CPU FFT supports following input types: {supported_types_str}. "
742
+ f"Got input of type {DATA_TYPE_TO_NAME[input_t]}."
743
+ )
744
+ kind = getattr(fftw.Kind, fft_abstract_type)
745
+ if kind == fftw.Kind.C2C:
746
+ supported_in_t, supported_out_t = FFTW_SUPPORTED_COMPLEX, FFTW_SUPPORTED_COMPLEX
747
+ elif kind == fftw.Kind.C2R:
748
+ supported_in_t, supported_out_t = FFTW_SUPPORTED_COMPLEX, FFTW_SUPPORTED_FLOAT
749
+ else:
750
+ assert kind == fftw.Kind.R2C
751
+ supported_in_t, supported_out_t = FFTW_SUPPORTED_FLOAT, FFTW_SUPPORTED_COMPLEX
752
+ if input_t not in supported_in_t:
753
+ supported_types_str = ", ".join(DATA_TYPE_TO_NAME[dtype] for dtype in supported_in_t)
754
+ raise ValueError(
755
+ f"Got unsupported input data type {DATA_TYPE_TO_NAME[input_t]} "
756
+ f"for the {fft_abstract_type} transform. "
757
+ f"Supported types are {supported_types_str}."
758
+ )
759
+ assert output_t in supported_out_t, "Mismatched data type and FFT transform type"
760
+ if direction is None:
761
+ sign = fftw.Sign.UNSPECIFIED
762
+ else:
763
+ sign = fftw.Sign(direction)
764
+ return (
765
+ precision,
766
+ kind,
767
+ sign,
768
+ rank,
769
+ n,
770
+ batch,
771
+ operand_ptr,
772
+ inembed,
773
+ istride,
774
+ idist,
775
+ result_ptr,
776
+ onembed,
777
+ ostride,
778
+ odist,
779
+ fftw.PlannerFlags.ESTIMATE,
780
+ )
781
+
782
+
783
+ def setup_options(operand: utils.TensorHolder, options, execution) -> tuple[FFTOptions, ExecutionCUDA | ExecutionCPU]:
784
+ default_exec_space = operand.device
785
+ execution = utils.check_or_create_one_of_options(
786
+ (ExecutionCUDA, ExecutionCPU),
787
+ execution,
788
+ "'execution' options",
789
+ default_name=default_exec_space,
790
+ )
791
+ # Process options.
792
+ options = utils.check_or_create_options(FFTOptions, options, "FFT options")
793
+ return _cross_setup_execution_and_options(options, execution)
794
+
795
+
796
+ def create_fft_key(
797
+ operand,
798
+ *,
799
+ axes: Sequence[int] | None = None,
800
+ options: FFTOptions | None = None,
801
+ execution: ExecutionCPU | ExecutionCUDA | None = None,
802
+ inplace=None,
803
+ prolog: DeviceCallable | None = None,
804
+ epilog: DeviceCallable | None = None,
805
+ plan_args=None,
806
+ ):
807
+ """
808
+ This key is not designed to be serialized and used on a different machine. It is meant
809
+ for runtime use only. We use a specific inplace argument instead of taking it from
810
+ options, because self.inplace != self.options.inplace for CPU tensors for efficiency.
811
+
812
+ It is the user's responsibility to augment this key with the stream in case they use
813
+ stream-ordered memory pools.
814
+ """
815
+ if plan_args is None:
816
+ operand = tensor_wrapper.wrap_operand(operand)
817
+ options, execution = setup_options(operand, options, execution)
818
+ fft_abstract_type = _get_default_fft_abstract_type(operand.dtype, options.fft_type)
819
+ if axes is None:
820
+ axes = range(len(operand.shape))
821
+
822
+ # Determine plan traits.
823
+ plan_traits = get_fft_plan_traits(
824
+ operand.shape,
825
+ operand.strides,
826
+ operand.dtype,
827
+ axes,
828
+ execution,
829
+ fft_abstract_type=fft_abstract_type,
830
+ last_axis_parity=options.last_axis_parity,
831
+ result_layout=options.result_layout,
832
+ logger=None,
833
+ )
834
+
835
+ # Inplace is always True when execution space is different than the operand's memory
836
+ # space (as the operand needs to be copied once anyway)
837
+ if inplace is None:
838
+ memory_space = operand.device
839
+ execution_space = execution.name
840
+ assert execution.name in ("cpu", "cuda")
841
+ inplace = memory_space != execution_space or options.inplace
842
+
843
+ if inplace:
844
+ check_inplace_overlapping_layout(operand)
845
+
846
+ # Get the arguments to xt_make_plan_many.
847
+ plan_args = create_xt_plan_args(
848
+ plan_traits=plan_traits,
849
+ fft_abstract_type=fft_abstract_type,
850
+ operand_data_type=operand.dtype,
851
+ inplace=inplace,
852
+ )
853
+
854
+ # Prolog and epilog, if used.
855
+ if prolog is not None or epilog is not None:
856
+ prolog = utils.check_or_create_options(DeviceCallable, prolog, "prolog", keep_none=True)
857
+ epilog = utils.check_or_create_options(DeviceCallable, epilog, "epilog", keep_none=True)
858
+
859
+ def get_data(device_callable):
860
+ return None if device_callable is None else (device_callable.ltoir, device_callable.data)
861
+
862
+ callable_data = get_data(prolog), get_data(epilog)
863
+ else:
864
+ callable_data = None
865
+
866
+ # The key is based on plan arguments, callback data (a callable object of type
867
+ # DeviceCallback or None) and the execution options (in "normalized" form of
868
+ # ("cpu"/"cuda", *execution_options)).
869
+ return plan_args, callable_data, data_cls_astuple(execution) # type: ignore[arg-type]
870
+
871
+
872
+ def _has_only_small_factors_extent(extent):
873
+ # fast track for powers of 2 (and zero)
874
+ if extent & (extent - 1) == 0:
875
+ return True
876
+ # Divide the `extent` by the product of all the prime factors up to
877
+ # 127 present in the `extent` until there are none left.
878
+ # Considering all the prime factors at once is faster, even though the
879
+ # first call (and only the first call) to gcd operates on ints with precision
880
+ # exceeding 64 bits. For common 2, 3, 5, 7 factors, the higher powers are included,
881
+ # to reduce number of iterations.
882
+ # math.prod(p for p in range(2, 128) if is_prime(p)) * 2**10 * 3**7 * 5**5 * 7**4
883
+ magic_prod = 67455891904760197438286248026720562156610454525830430432000000
884
+ d = math.gcd(extent, magic_prod)
885
+ while d > 1:
886
+ if extent == d:
887
+ return True
888
+ extent //= d
889
+ d = math.gcd(extent, d)
890
+ return False
891
+
892
+
893
+ def _has_only_small_factors_shape(shape):
894
+ return all(extent <= 2048 or _has_only_small_factors_extent(extent) for extent in shape)
895
+
896
+
897
+ def check_is_shape_supported_lto_ea(operand, plan_traits, fft_abstract_type):
898
+ if fft_abstract_type != "C2R":
899
+ shape = plan_traits.ordered_fft_in_shape
900
+ else:
901
+ shape = plan_traits.ordered_fft_out_shape
902
+ if not _has_only_small_factors_shape(shape):
903
+ raise ValueError(
904
+ f"cuFFT LTO EA does not support callbacks with inputs of certain shapes. "
905
+ f"Tensor with extents comprasing prime factors larger than 127 are not supported. "
906
+ f"Got a tensor of shape {operand.shape}."
907
+ )
908
+ if len(shape) == 3 and sum(e == 1 for e in shape) == 1 and shape[-1] == 1:
909
+ raise ValueError(
910
+ "cuFFT LTO EA does not support callbacks with inputs of certain shapes. "
911
+ "3D FFT with the last extent equal 1 are not supported"
912
+ )
913
+
914
+
915
+ def _check_prolog_epilog_traits(prolog, epilog, plan_traits, operand, fft_abstract_type):
916
+ # Since the version 11300, cufft does the validation itself.
917
+ # In earlier versions, it could ignore the callback silently
918
+ # for unsupported shapes
919
+ if (prolog or epilog) and cufft.get_version() < 11300:
920
+ check_is_shape_supported_lto_ea(operand, plan_traits, fft_abstract_type)
921
+
922
+
923
+ def set_prolog_and_epilog(handle, prolog, epilog, operand_dtype, result_dtype, logger):
924
+ def set_callback(cbkind, cbobj, dtype):
925
+ if cbobj is None:
926
+ return
927
+
928
+ assert cbkind in ["prolog", "epilog"], "Internal error."
929
+ CBType = CBLoadType if cbkind == "prolog" else CBStoreType
930
+
931
+ try:
932
+ cufft.xt_set_jit_callback(handle, cbobj.ltoir, cbobj.size, CBType[dtype.upper()], [cbobj.data])
933
+ except _bindings_utils.FunctionNotFoundError as e:
934
+ version = cufft.get_version()
935
+ raise RuntimeError(
936
+ f"The currently running cuFFT version {version} does not support LTO callbacks. \n"
937
+ f"The following cuFFT releases support LTO callbacks: \n"
938
+ f"1. cuFFT shipped with CUDA 12.6U2 (11.3.0) or newer \n"
939
+ f"2. the older, experimental cuFFT LTO EA (early access) preview build "
940
+ f"(https://developer.nvidia.com/cufftea).\n"
941
+ f"To use version different from the one shipped with CUDA Toolkit, please make "
942
+ f"sure the right 'libcufft.so' takes precedence for nvmath. "
943
+ f"For example, by adjusting the 'LD_LIBRARY_PATH' or 'LD_PRELOAD'."
944
+ ) from e
945
+
946
+ logger.info(f"The specified LTO-IR {cbkind} has been set.")
947
+ if isinstance(cbobj.ltoir, int):
948
+ logger.debug(f"The {cbkind} LTO-IR pointer is {cbobj.ltoir}.")
949
+ logger.debug(f"The {cbkind} LTO-IR size is {cbobj.size}, and data is {cbobj.data}.")
950
+
951
+ if prolog is not None:
952
+ set_callback("prolog", prolog, operand_dtype)
953
+ if epilog is not None:
954
+ set_callback("epilog", epilog, result_dtype)
955
+
956
+
957
+ class InvalidFFTState(Exception):
958
+ pass
959
+
960
+
961
+ @utils.docstring_decorator(SHARED_FFT_DOCUMENTATION, skip_missing=False)
962
+ class FFT:
963
+ """
964
+ Create a stateful object that encapsulates the specified FFT computations and required
965
+ resources. This object ensures the validity of resources during use and releases them
966
+ when they are no longer needed to prevent misuse.
967
+
968
+ This object encompasses all functionalities of function-form APIs :func:`fft`,
969
+ :func:`ifft`, :func:`rfft`, and :func:`irfft`, which are convenience wrappers around it.
970
+ The stateful object also allows for the amortization of preparatory costs when the same
971
+ FFT operation is to be performed on multiple operands with the same problem
972
+ specification (see :meth:`reset_operand` and :meth:`create_key` for more details).
973
+
974
+ Using the stateful object typically involves the following steps:
975
+
976
+ 1. **Problem Specification**: Initialize the object with a defined operation and
977
+ options.
978
+ 2. **Preparation**: Use :meth:`plan` to determine the best algorithmic implementation
979
+ for this specific FFT operation.
980
+ 3. **Execution**: Perform the FFT computation with :meth:`execute`, which can be either
981
+ forward or inverse FFT transformation.
982
+ 4. **Resource Management**: Ensure all resources are released either by explicitly
983
+ calling :meth:`free` or by managing the stateful object within a context manager.
984
+
985
+ Detailed information on each step described above can be obtained by passing in a
986
+ :class:`logging.Logger` object to :class:`FFTOptions` or by setting the appropriate
987
+ options in the root logger object, which is used by default:
988
+
989
+ >>> import logging
990
+ >>> logging.basicConfig(
991
+ ... level=logging.INFO,
992
+ ... format="%(asctime)s %(levelname)-8s %(message)s",
993
+ ... datefmt="%m-%d %H:%M:%S",
994
+ ... )
995
+
996
+ Args:
997
+ operand: {operand}
998
+
999
+ axes: {axes}
1000
+
1001
+ options: {options}
1002
+
1003
+ execution: {execution}
1004
+
1005
+ stream: {stream}
1006
+
1007
+ See Also:
1008
+ :meth:`plan`, :meth:`reset_operand`, :meth:`execute`, :meth:`create_key`
1009
+
1010
+ Examples:
1011
+
1012
+ >>> import cupy as cp
1013
+ >>> import nvmath
1014
+
1015
+ Create a 3-D complex128 ndarray on the GPU:
1016
+
1017
+ >>> shape = 128, 128, 128
1018
+ >>> a = cp.random.rand(*shape) + 1j * cp.random.rand(*shape)
1019
+
1020
+ We will define a 2-D C2C FFT operation along the first two dimensions, batched along
1021
+ the last dimension:
1022
+
1023
+ >>> axes = 0, 1
1024
+
1025
+ Create an FFT object encapsulating the problem specification above:
1026
+
1027
+ >>> f = nvmath.fft.FFT(a, axes=axes)
1028
+
1029
+ Options can be provided above to control the behavior of the operation using the
1030
+ `options` argument (see :class:`FFTOptions`). Similarly, the execution space (CUDA
1031
+ or CPU) and execution options can be passed using the `execution` argument (see
1032
+ :class:`ExecutionCUDA`, :class:`ExecutionCPU`).
1033
+
1034
+ Next, plan the FFT. Load and/or store callback functions can be provided to
1035
+ :meth:`plan` using the `prolog` and `epilog` option:
1036
+
1037
+ >>> f.plan()
1038
+
1039
+ Now execute the FFT, and obtain the result `r1` as a CuPy ndarray. The transform
1040
+ will be performed on GPU, because ``execution`` was not explicitly specified and
1041
+ ``a`` resides in GPU memory.
1042
+
1043
+ >>> r1 = f.execute()
1044
+
1045
+ Finally, free the FFT object's resources. To avoid this explicit call, it's
1046
+ recommended to use the FFT object as a context manager as shown below, if possible.
1047
+
1048
+ >>> f.free()
1049
+
1050
+ Note that all :class:`FFT` methods execute on the current stream by default.
1051
+ Alternatively, the `stream` argument can be used to run a method on a specified
1052
+ stream.
1053
+
1054
+ Let's now look at the same problem with NumPy ndarrays on the CPU.
1055
+
1056
+ Create a 3-D complex128 NumPy ndarray on the CPU:
1057
+
1058
+ >>> import numpy as np
1059
+ >>> shape = 128, 128, 128
1060
+ >>> a = np.random.rand(*shape) + 1j * np.random.rand(*shape)
1061
+
1062
+ Create an FFT object encapsulating the problem specification described earlier and
1063
+ use it as a context manager.
1064
+
1065
+ >>> with nvmath.fft.FFT(a, axes=axes) as f:
1066
+ ... f.plan()
1067
+ ...
1068
+ ... # Execute the FFT to get the first result.
1069
+ ... r1 = f.execute()
1070
+
1071
+ All the resources used by the object are released at the end of the block.
1072
+
1073
+ The operation was performed on the CPU because ``a`` resides in host memory. With
1074
+ ``execution`` specified to 'cuda', the NumPy array would be temporarily copied to
1075
+ device memory and transformed on the GPU:
1076
+
1077
+ >>> with nvmath.fft.FFT(a, axes=axes, execution="cuda") as f:
1078
+ ... f.plan()
1079
+ ...
1080
+ ... # Execute the FFT to get the first result.
1081
+ ... r1 = f.execute()
1082
+
1083
+ Further examples can be found in the `nvmath/examples/fft
1084
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/fft>`_ directory.
1085
+
1086
+ Notes:
1087
+
1088
+ - The input must be Hermitian-symmetric when :attr:`FFTOptions.fft_type` is
1089
+ ``'C2R'``, otherwise the result is undefined. As a specific example, if the input
1090
+ for a C2R FFT was generated using an R2C FFT with an odd last axis size, then
1091
+ :attr:`FFTOptions.last_axis_parity` must be set to `odd` to recover the original
1092
+ signal.
1093
+ """
1094
+
1095
+ def __init__(
1096
+ self,
1097
+ operand,
1098
+ *,
1099
+ axes: Sequence[int] | None = None,
1100
+ options: FFTOptions | None = None,
1101
+ execution: ExecutionCPU | ExecutionCUDA | None = None,
1102
+ stream: AnyStream | None = None,
1103
+ ):
1104
+ self.operand = operand = tensor_wrapper.wrap_operand(operand)
1105
+ options, execution = setup_options(operand, options, execution)
1106
+ self.options = options
1107
+ self.execution_options = execution
1108
+
1109
+ self.operand_dim = len(operand.shape)
1110
+
1111
+ if not axes and self.operand_dim > 3:
1112
+ raise ValueError(
1113
+ f"The tensor is {self.operand_dim}-D and FFTs in number of dimensions > 3 is not supported. The FFT "
1114
+ "axes need to be specified using the 'axes' option."
1115
+ )
1116
+
1117
+ if self.operand_dim == 0:
1118
+ raise ValueError(f"The tensor is {self.operand_dim}-D (i.e. a scalar). FFT does not support scalars.")
1119
+
1120
+ self.operand_data_type = operand.dtype
1121
+ self.fft_abstract_type = _get_default_fft_abstract_type(self.operand_data_type, options.fft_type)
1122
+
1123
+ self.result_data_type, self.compute_data_type = _get_fft_result_and_compute_types(operand.dtype, self.fft_abstract_type)
1124
+
1125
+ self.logger = options.logger if options.logger is not None else logging.getLogger()
1126
+ self.logger.info(f"The FFT type is {self.fft_abstract_type}.")
1127
+ self.logger.info(
1128
+ f"The input data type is {self.operand_data_type}, and the result data type is {self.result_data_type}."
1129
+ )
1130
+
1131
+ if axes is None:
1132
+ axes = range(self.operand_dim)
1133
+
1134
+ if any(axis >= self.operand_dim or axis < -self.operand_dim for axis in axes):
1135
+ raise ValueError(f"The specified FFT axes {axes} are out of bounds for a {self.operand_dim}-D tensor.")
1136
+
1137
+ # Handle negative axis indices.
1138
+ self.axes = tuple(axis % self.operand_dim for axis in axes)
1139
+ self.logger.info(f"The specified FFT axes are {self.axes}.")
1140
+
1141
+ self.package = utils.infer_object_package(operand.tensor)
1142
+
1143
+ # NumPy and CuPy don't support complex32 yet.
1144
+ if self.package in ["numpy", "cupy"] and self.result_data_type == "complex32":
1145
+ raise TypeError(
1146
+ f"The result data type {self.result_data_type} is not supported by the operand package '{self.package}'."
1147
+ )
1148
+
1149
+ # Infer operand package, execution space, and memory space.
1150
+ if execution.name == "cuda":
1151
+ if operand.device == "cuda": # exec space matches the mem space
1152
+ self.memory_space = "cuda"
1153
+ self.device_id = operand.device_id
1154
+ else: # we need to move inputs cpu -> gpu and outputs gpu -> cpu
1155
+ self.memory_space = "cpu"
1156
+ self.device_id = execution.device_id
1157
+ else:
1158
+ assert execution.name == "cpu"
1159
+ self.device_id = "cpu"
1160
+ if operand.device_id == "cpu": # exec space matches the mem space
1161
+ self.memory_space = "cpu"
1162
+ else: # we need to move inputs gpu -> cpu and outputs cpu -> gpu
1163
+ self.memory_space = "cuda"
1164
+ self.execution_space = execution.name
1165
+ self.operand_device_id = operand.device_id
1166
+ self.internal_op_package = self._internal_operand_package(self.package)
1167
+ exec_stream_holder, operand_stream_holder = self._get_or_create_stream_maybe(stream)
1168
+
1169
+ self.logger.info(
1170
+ f"The input tensor's memory space is {self.memory_space}, and the execution space "
1171
+ f"is {self.execution_space}, with device {self.device_id}."
1172
+ )
1173
+
1174
+ self.logger.info(
1175
+ f"The specified stream for the FFT ctor is "
1176
+ f"{(exec_stream_holder or operand_stream_holder) and getattr(exec_stream_holder or operand_stream_holder, 'obj', None)}." # noqa: E501
1177
+ )
1178
+
1179
+ # In-place FFT option, available only for C2C transforms.
1180
+ self.inplace = self.options.inplace
1181
+ if self.inplace and self.fft_abstract_type != "C2C":
1182
+ raise ValueError(
1183
+ f"The in-place option (FFTOptions.inplace=True) is only supported for complex-to-complex FFT. "
1184
+ f"The FFT type is '{self.fft_abstract_type}'."
1185
+ )
1186
+
1187
+ # Copy the operand to execution_space's device if needed.
1188
+ self.operand, self.operand_backup = _copy_operand_perhaps(
1189
+ None,
1190
+ operand,
1191
+ operand_stream_holder,
1192
+ self.execution_space,
1193
+ self.memory_space,
1194
+ self.device_id,
1195
+ self.fft_abstract_type,
1196
+ self.logger,
1197
+ )
1198
+
1199
+ operand = self.operand
1200
+ # Capture operand layout for consistency checks when resetting operands.
1201
+ self.operand_layout = TensorLayout(shape=operand.shape, strides=operand.strides)
1202
+
1203
+ self._preallocated_result: utils.TensorHolder | None = None
1204
+
1205
+ if self.options.inplace: # Don't use self.inplace here, because we always set it to True for CPU tensors.
1206
+ self.logger.info("The FFT will be performed in-place, with the result overwriting the input.")
1207
+ else:
1208
+ self.logger.info("The FFT will be performed out-of-place.")
1209
+
1210
+ # Check if FFT is supported and calculate plan traits.
1211
+ self.plan_traits = get_fft_plan_traits(
1212
+ operand.shape,
1213
+ operand.strides,
1214
+ operand.dtype,
1215
+ self.axes,
1216
+ self.execution_options,
1217
+ fft_abstract_type=self.fft_abstract_type,
1218
+ last_axis_parity=self.options.last_axis_parity,
1219
+ result_layout=self.options.result_layout,
1220
+ logger=self.logger,
1221
+ )
1222
+
1223
+ self.logger.info(
1224
+ f"The operand data type = {self.operand_data_type}, shape = {self.operand_layout.shape}, and "
1225
+ f"strides = {self.operand_layout.strides}."
1226
+ )
1227
+ result_data_type, result_shape, result_strides = (
1228
+ (self.operand_data_type, self.operand_layout.shape, self.operand_layout.strides)
1229
+ if self.inplace
1230
+ else (self.result_data_type, self.plan_traits.result_shape, self.plan_traits.result_strides)
1231
+ )
1232
+ self.logger.info(f"The result data type = {result_data_type}, shape = {result_shape}, and strides = {result_strides}.")
1233
+ self.logger.info(f"The FFT batch size is {self.plan_traits.fft_batch_size}.")
1234
+
1235
+ ordered_fft_out_shape, ostride, odistance = (
1236
+ (self.plan_traits.ordered_fft_in_shape, self.plan_traits.istride, self.plan_traits.idistance)
1237
+ if self.inplace
1238
+ else (self.plan_traits.ordered_fft_out_shape, self.plan_traits.ostride, self.plan_traits.odistance)
1239
+ )
1240
+ self.logger.debug(
1241
+ f"The plan ordered axes = {self.plan_traits.ordered_axes}, ordered input shape = "
1242
+ f"{self.plan_traits.ordered_fft_in_shape}, ordered input embedding shape = "
1243
+ f"{self.plan_traits.ordered_fft_in_embedding_shape}, ordered output shape = {ordered_fft_out_shape}."
1244
+ )
1245
+ self.logger.debug(f"The plan input stride is {self.plan_traits.istride} with distance {self.plan_traits.idistance}.")
1246
+ self.logger.debug(f"The plan output stride is {ostride} with distance {odistance}.")
1247
+
1248
+ # The result's package and device.
1249
+ self.result_class = operand.__class__
1250
+
1251
+ # Set blocking or non-blocking behavior.
1252
+ self.blocking = self.options.blocking is True or self.memory_space == "cpu" or self.execution_space == "cpu"
1253
+ if self.blocking:
1254
+ self.call_prologue = "This call is blocking and will return only after the operation is complete."
1255
+ else:
1256
+ self.call_prologue = (
1257
+ "This call is non-blocking and will return immediately after the operation is launched on the device."
1258
+ )
1259
+
1260
+ # Set memory allocator.
1261
+ if self.execution_space == "cpu":
1262
+ self.allocator = None # currently, the nvpl/fftw does not support custom workspace allocation
1263
+ else:
1264
+ self.allocator = (
1265
+ options.allocator
1266
+ if options.allocator is not None
1267
+ else memory._MEMORY_MANAGER[self.internal_op_package](self.device_id, self.logger)
1268
+ )
1269
+
1270
+ if self.execution_space == "cpu":
1271
+ # the handle is created alongside planning
1272
+ self.handle = None
1273
+ else:
1274
+ # Create handle.
1275
+ with utils.device_ctx(self.device_id):
1276
+ self.handle = cufft.create()
1277
+
1278
+ # Set stream for the FFT.
1279
+ cufft.set_stream(self.handle, exec_stream_holder.ptr) # type: ignore[union-attr]
1280
+
1281
+ # Plan attributes.
1282
+ cufft.set_auto_allocation(self.handle, 0)
1283
+
1284
+ self.fft_planned = False
1285
+
1286
+ # Workspace attributes.
1287
+ self.workspace_ptr: None | memory.MemoryPointer = None
1288
+ self.workspace_size = 0
1289
+ self._workspace_allocated_here = False
1290
+
1291
+ # Attributes to establish stream ordering.
1292
+ self.workspace_stream = None
1293
+ self.last_compute_event = None
1294
+
1295
+ # Keep track of key (sans callback) for resetting operands, once plan in available.
1296
+ self.orig_key = None
1297
+
1298
+ self.valid_state = True
1299
+ self.logger.info("The FFT operation has been created.")
1300
+
1301
+ def get_key(self, *, prolog: DeviceCallable | None = None, epilog: DeviceCallable | None = None):
1302
+ """
1303
+ Get the key for this object's data supplemented with the callbacks.
1304
+
1305
+ Args:
1306
+ prolog: {prolog}
1307
+ epilog: {epilog}
1308
+
1309
+ Returns:
1310
+ {fft_key}
1311
+
1312
+ See Also:
1313
+ :meth:`create_key`
1314
+ """
1315
+ return create_fft_key(
1316
+ self.operand.tensor,
1317
+ axes=self.axes,
1318
+ options=self.options,
1319
+ execution=self.execution_options,
1320
+ inplace=self.inplace,
1321
+ prolog=prolog,
1322
+ epilog=epilog,
1323
+ )
1324
+
1325
+ @staticmethod
1326
+ def create_key(
1327
+ operand,
1328
+ *,
1329
+ axes: Sequence[int] | None = None,
1330
+ options: FFTOptions | None = None,
1331
+ execution: ExecutionCPU | ExecutionCUDA | None = None,
1332
+ prolog: DeviceCallable | None = None,
1333
+ epilog: DeviceCallable | None = None,
1334
+ ):
1335
+ """
1336
+ Create a key as a compact representation of the FFT problem specification based on
1337
+ the given operand, axes and the FFT options. Note that different combinations of
1338
+ operand layout, axes and options can potentially correspond to the same underlying
1339
+ problem specification (key). Users may reuse the FFT objects when different input
1340
+ problems map to an identical key.
1341
+
1342
+ Args:
1343
+ operand: {operand}
1344
+
1345
+ axes: {axes}
1346
+
1347
+ options: {options}
1348
+
1349
+ execution: {execution}
1350
+
1351
+ prolog: {prolog}
1352
+
1353
+ epilog: {epilog}
1354
+
1355
+ Returns:
1356
+ {fft_key}
1357
+
1358
+ Notes:
1359
+ - Users may take advantage of this method to create cached version of
1360
+ :func:`fft` based on the stateful object APIs (see `caching.py
1361
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/fft/caching.py>`_
1362
+ for an example implementation).
1363
+ - This key is meant for runtime use only and not designed to be serialized or
1364
+ used on a different machine.
1365
+ - It is the user's responsibility to augment this key with the stream in case
1366
+ they use stream-ordered memory pools.
1367
+ """
1368
+ return create_fft_key(operand, axes=axes, options=options, execution=execution, prolog=prolog, epilog=epilog)
1369
+
1370
+ def __enter__(self):
1371
+ return self
1372
+
1373
+ def __exit__(self, exc_type, exc_value, traceback):
1374
+ self.free()
1375
+
1376
+ def _check_valid_fft(self, *args, **kwargs):
1377
+ """
1378
+ Check if FFT object is alive and well.
1379
+ """
1380
+ if not self.valid_state:
1381
+ raise InvalidFFTState("The FFT object cannot be used after resources are free'd")
1382
+
1383
+ def _free_plan_resources(self, exception: Exception | None = None) -> bool:
1384
+ """
1385
+ Free resources allocated in planning.
1386
+ """
1387
+
1388
+ self.workspace_ptr = None
1389
+ self.fft_planned = False
1390
+ return True
1391
+
1392
+ def _internal_operand_package(self, package_name):
1393
+ if self.execution_space == "cuda":
1394
+ if package_name == "numpy":
1395
+ # TODO: remove this call after cupy is dropped
1396
+ tensor_wrapper.maybe_register_package("cupy")
1397
+ return package_name if package_name != "numpy" else "cupy"
1398
+ else:
1399
+ return package_name if package_name != "cupy" else "numpy"
1400
+
1401
+ def _get_or_create_stream_maybe(self, stream: AnyStream) -> tuple[StreamHolder | None, StreamHolder | None]:
1402
+ if self.execution_space == "cuda":
1403
+ assert isinstance(self.device_id, int), self.device_id
1404
+ stream_holder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
1405
+ return stream_holder, stream_holder
1406
+ elif self.memory_space == "cuda":
1407
+ assert isinstance(self.operand_device_id, int), self.operand_device_id
1408
+ operand_device_steam = utils.get_or_create_stream(self.operand_device_id, stream, self.package)
1409
+ return None, operand_device_steam
1410
+ else:
1411
+ return None, None
1412
+
1413
+ def _allocate_result_operand(self, exec_stream_holder: StreamHolder | None, log_debug):
1414
+ if log_debug:
1415
+ self.logger.debug("Beginning output (empty) tensor creation...")
1416
+ self.logger.debug(
1417
+ f"The output tensor shape = {self.plan_traits.result_shape} with strides = "
1418
+ f"{self.plan_traits.result_strides} and data type '{self.result_data_type}'."
1419
+ )
1420
+ result = utils.create_empty_tensor(
1421
+ self.result_class,
1422
+ self.plan_traits.result_shape,
1423
+ self.result_data_type,
1424
+ self.device_id,
1425
+ exec_stream_holder,
1426
+ verify_strides=False, # the strides are computed so that they are contiguous
1427
+ strides=self.plan_traits.result_strides,
1428
+ )
1429
+ if log_debug:
1430
+ self.logger.debug("The output (empty) tensor has been created.")
1431
+ return result
1432
+
1433
+ def _get_validate_direction(self, direction):
1434
+ if isinstance(direction, str) and (d := direction.upper()) in ["FORWARD", "INVERSE"]:
1435
+ direction = FFTDirection[d]
1436
+ else:
1437
+ direction = FFTDirection(direction)
1438
+
1439
+ if self.fft_abstract_type == "C2R":
1440
+ if direction != FFTDirection.INVERSE:
1441
+ raise ValueError(
1442
+ f"The specified direction {direction.name} is not compatible with the FFT type '{self.fft_abstract_type}'."
1443
+ )
1444
+ elif self.fft_abstract_type == "R2C": # noqa: SIM102
1445
+ if direction != FFTDirection.FORWARD:
1446
+ raise ValueError(
1447
+ f"The specified direction {direction.name} is not compatible with the FFT type '{self.fft_abstract_type}'."
1448
+ )
1449
+ return direction
1450
+
1451
+ @utils.precondition(_check_valid_fft)
1452
+ @utils.atomic(_free_plan_resources, method=True)
1453
+ def plan(
1454
+ self,
1455
+ *,
1456
+ prolog: DeviceCallable | None = None,
1457
+ epilog: DeviceCallable | None = None,
1458
+ stream: AnyStream | None = None,
1459
+ direction: FFTDirection | None = None,
1460
+ ):
1461
+ """Plan the FFT.
1462
+
1463
+ Args:
1464
+ prolog: {prolog}
1465
+
1466
+ epilog: {epilog}
1467
+
1468
+ stream: {stream}
1469
+
1470
+ direction: If specified, the same direction must be passed to subsequent
1471
+ :meth:`execute` calls. It may be used as a hint to optimize C2C planning for
1472
+ CPU FFT calls.
1473
+ """
1474
+ log_info = self.logger.isEnabledFor(logging.INFO)
1475
+ log_debug = self.logger.isEnabledFor(logging.DEBUG)
1476
+
1477
+ if self.fft_planned:
1478
+ self.logger.debug("The FFT has already been planned, and redoing the plan is not supported.")
1479
+ return
1480
+
1481
+ if self.execution_space == "cpu":
1482
+ stream_holder = None
1483
+ if prolog is not None or epilog is not None:
1484
+ raise ValueError("The 'prolog' and 'epilog' are not supported with CPU 'execution'.")
1485
+ else:
1486
+ assert isinstance(self.device_id, int)
1487
+ stream_holder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
1488
+ self.workspace_stream = stream_holder.obj
1489
+
1490
+ # Set stream for the FFT.
1491
+ cufft.set_stream(self.handle, stream_holder.ptr)
1492
+
1493
+ # Set LTO-IR callbacks, if present.
1494
+ prolog = utils.check_or_create_options(DeviceCallable, prolog, "prolog", keep_none=True)
1495
+ epilog = utils.check_or_create_options(DeviceCallable, epilog, "epilog", keep_none=True)
1496
+ _check_prolog_epilog_traits(prolog, epilog, self.plan_traits, self.operand, self.fft_abstract_type)
1497
+ set_prolog_and_epilog(self.handle, prolog, epilog, self.operand_data_type, self.result_data_type, self.logger)
1498
+
1499
+ # Get all the arguments to xt_make_plan_many except for the first (the handle).
1500
+ if self.inplace:
1501
+ check_inplace_overlapping_layout(self.operand)
1502
+ if self.operand_backup is not None:
1503
+ check_inplace_overlapping_layout(self.operand_backup)
1504
+
1505
+ plan_args = create_xt_plan_args(
1506
+ plan_traits=self.plan_traits,
1507
+ fft_abstract_type=self.fft_abstract_type,
1508
+ operand_data_type=self.operand_data_type,
1509
+ inplace=self.inplace,
1510
+ )
1511
+
1512
+ # Keep track of original key (sans callback) for resetting operands. Pass in plan
1513
+ # args to avoid recomputation.
1514
+ self.orig_key = create_fft_key(
1515
+ self.operand.tensor,
1516
+ axes=self.axes,
1517
+ options=self.options,
1518
+ execution=self.execution_options,
1519
+ plan_args=plan_args,
1520
+ )
1521
+ if log_debug:
1522
+ self.logger.debug(f"The FFT key (sans callback) is {self.orig_key}.")
1523
+
1524
+ self.logger.debug(
1525
+ f"The operand CUDA type is {NAME_TO_DATA_TYPE[self.operand_data_type].name}, and the result CUDA type is "
1526
+ f"{NAME_TO_DATA_TYPE[self.result_data_type].name}."
1527
+ )
1528
+ self.logger.debug(f"The CUDA type used for compute is {NAME_TO_DATA_TYPE[self.compute_data_type].name}.")
1529
+ if log_info:
1530
+ self.logger.info("Starting FFT planning...")
1531
+
1532
+ if self.execution_space == "cpu":
1533
+ if direction is not None:
1534
+ direction = self._get_validate_direction(direction)
1535
+ if self.inplace:
1536
+ result_ptr = self.operand.data_ptr
1537
+ else:
1538
+ # FFTW3 API requires passing pointers to the input and output during
1539
+ # planning. Passing different pointers to (properly strided and aligned)
1540
+ # data in subsequent execute calls is supported, but it is not clear what
1541
+ # planning is allowed to do with the provided pointers. For one, planning
1542
+ # can compare the two pointers for equality to decide if it is inplace or
1543
+ # out-of-place operation. To avoid subtle issues, just preallocate the
1544
+ # result tensor earlier.
1545
+ self._preallocated_result = self._allocate_result_operand(None, True)
1546
+ result_ptr = self._preallocated_result.data_ptr # type: ignore[attr-defined, union-attr]
1547
+ precision, *plan_args = fftw_plan_args(
1548
+ plan_args,
1549
+ self.operand.data_ptr,
1550
+ result_ptr,
1551
+ fft_abstract_type=self.fft_abstract_type,
1552
+ direction=direction,
1553
+ )
1554
+ with utils.host_call_ctx(timing=log_info) as elapsed:
1555
+ fftw.plan_with_nthreads(precision, self.execution_options.num_threads) # type: ignore[union-attr]
1556
+ try:
1557
+ assert self.handle is None
1558
+ self.handle = fftw.plan_many(precision, *plan_args)
1559
+ except OverflowError as e:
1560
+ raise ValueError(
1561
+ "Currently, the CPU FFT only supports the problem sizes that "
1562
+ "can be expressed as 32-bit signed integer. "
1563
+ "Tensors with shape extents or stride larger than "
1564
+ "`2147483647` are not currently supported."
1565
+ ) from e
1566
+ else:
1567
+ # FIXME: Move creation of stream_holder into this block, so assertion is not
1568
+ # needed
1569
+ assert isinstance(self.device_id, int), self.device_id
1570
+ assert stream_holder is not None
1571
+ with utils.cuda_call_ctx(stream_holder, blocking=True, timing=log_info) as (
1572
+ self.last_compute_event,
1573
+ elapsed,
1574
+ ):
1575
+ self.workspace_size = cufft.xt_make_plan_many(self.handle, *plan_args)
1576
+
1577
+ self.fft_planned = True
1578
+
1579
+ if log_info and elapsed.data is not None:
1580
+ self.logger.info(f"The FFT planning phase took {elapsed.data:.3f} ms to complete.")
1581
+
1582
+ @utils.precondition(_check_valid_fft)
1583
+ def reset_operand(self, operand=None, *, stream: AnyStream | None = None):
1584
+ """
1585
+ Reset the operand held by this :class:`FFT` instance. This method has two use cases:
1586
+
1587
+ (1) it can be used to provide a new operand for execution
1588
+ (2) it can be used to release the internal reference to the previous operand and
1589
+ potentially make its memory available for other use by passing
1590
+ ``operand=None``.
1591
+
1592
+ Args:
1593
+ operand: A tensor (ndarray-like object) compatible with the previous one or
1594
+ `None` (default). A value of `None` will release the internal reference to
1595
+ the previous operand and user is expected to set a new operand before again
1596
+ calling :meth:`execute`. The new operand is considered compatible if all the
1597
+ following properties match with the previous one:
1598
+
1599
+ - The problem specification key for the new operand. Generally the keys will
1600
+ match if the operand shares the same layout (shape, strides and data
1601
+ type). The keys may still match for certain operands with different
1602
+ layout, see :meth:`create_key` for details.
1603
+ - The package that the new operand belongs to.
1604
+ - The memory space of the new operand (CPU or GPU).
1605
+ - The device that new operand belongs to if it is on GPU.
1606
+
1607
+ stream: {stream}.
1608
+
1609
+ Examples:
1610
+
1611
+ >>> import cupy as cp
1612
+ >>> import nvmath
1613
+
1614
+ Create a 3-D complex128 ndarray on the GPU:
1615
+
1616
+ >>> shape = 128, 128, 128
1617
+ >>> a = cp.random.rand(*shape) + 1j * cp.random.rand(*shape)
1618
+
1619
+ Create an FFT object as a context manager
1620
+
1621
+ >>> axes = 0, 1
1622
+ >>> with nvmath.fft.FFT(a, axes=axes) as f:
1623
+ ... # Plan the FFT
1624
+ ... f.plan()
1625
+ ...
1626
+ ... # Execute the FFT to get the first result.
1627
+ ... r1 = f.execute()
1628
+ ...
1629
+ ... # Reset the operand to a new CuPy ndarray.
1630
+ ... b = cp.random.rand(*shape) + 1j * cp.random.rand(*shape)
1631
+ ... f.reset_operand(b)
1632
+ ...
1633
+ ... # Execute to get the new result corresponding to the updated operand.
1634
+ ... r2 = f.execute()
1635
+
1636
+ With :meth:`reset_operand`, minimal overhead is achieved as problem
1637
+ specification and planning are only performed once.
1638
+
1639
+ For the particular example above, explicitly calling :meth:`reset_operand` is
1640
+ equivalent to updating the operand in-place, i.e, replacing
1641
+ ``f.reset_operand(b)`` with ``a[:]=b``. Note that updating the operand in-place
1642
+ should be adopted with caution as it can only yield the expected result and
1643
+ incur no additional copies under the additional constraints below:
1644
+
1645
+ - The operation is not a complex-to-real (C2R) FFT.
1646
+ - The operand's memory matches the FFT execution space. More precisely, the
1647
+ operand memory space should be accessible from the execution space (CPU or
1648
+ CUDA).
1649
+
1650
+ For more details, please refer to `inplace update example
1651
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/fft/example05_stateful_inplace.py>`_.
1652
+ """
1653
+
1654
+ if operand is None:
1655
+ self.operand = None # type: ignore
1656
+ self.operand_backup = None # type: ignore
1657
+ self.logger.info("The operand has been reset to None.")
1658
+ return
1659
+
1660
+ self.logger.info("Resetting operand...")
1661
+ # First wrap operand.
1662
+ operand = tensor_wrapper.wrap_operand(operand)
1663
+
1664
+ # Check package match.
1665
+ package = utils.infer_object_package(operand.tensor)
1666
+ if self.package != package:
1667
+ message = f"Library package mismatch: '{self.package}' => '{package}'"
1668
+ raise TypeError(message)
1669
+
1670
+ utils.check_attribute_match(self.operand_data_type, operand.dtype, "data type")
1671
+
1672
+ exec_stream_holder, operand_stream_holder = self._get_or_create_stream_maybe(stream)
1673
+ self.logger.info(
1674
+ "The specified stream for reset_operand() is "
1675
+ f"{(exec_stream_holder or operand_stream_holder) and (exec_stream_holder or operand_stream_holder).obj}." # type: ignore[union-attr]
1676
+ )
1677
+
1678
+ # In principle, we could support memory_space change,
1679
+ # but to handle it properly we need to update self.memory_space and
1680
+ # some dependent properties, like self.blocking, which may be error-prone
1681
+ # from the user perspective. It would prevent inplace optimizations as well.
1682
+ operand_device_id = operand.device_id
1683
+ if operand_device_id != self.operand_device_id:
1684
+
1685
+ def device_str(device_id: int | Literal["cpu"]) -> str:
1686
+ return f"cuda:{device_id}" if isinstance(device_id, int) else f"{device_id}"
1687
+
1688
+ raise ValueError(
1689
+ f"The new operand must be on the same device as the original one. "
1690
+ f"The new operand's device is {device_str(operand_device_id)}, "
1691
+ f"the original device is {device_str(self.operand_device_id)}"
1692
+ )
1693
+
1694
+ if self.orig_key is not None:
1695
+ # Compute the key corresponding to the new operand (sans callback).
1696
+ try:
1697
+ new_key = create_fft_key(
1698
+ operand.tensor,
1699
+ axes=self.axes,
1700
+ options=self.options,
1701
+ execution=self.execution_options,
1702
+ inplace=self.inplace,
1703
+ )
1704
+ except UnsupportedLayoutError:
1705
+ new_key = None
1706
+ if self.orig_key != new_key:
1707
+ self.logger.debug(f"The FFT key corresponding to the original operand is: {self.orig_key}.")
1708
+ if new_key is None:
1709
+ self.logger.debug(
1710
+ "The FFT key for the new operand cannot be computed since the layout "
1711
+ f"(shape = {operand.shape}, strides = {operand.strides}) and axes = {self.axes} combination "
1712
+ "is unsupported."
1713
+ )
1714
+ else:
1715
+ self.logger.debug(f"The FFT key corresponding to the new operand is: {new_key}.")
1716
+ raise ValueError(
1717
+ "The new operand's traits (data type, shape, or strides) are incompatible with that of the "
1718
+ "original operand."
1719
+ )
1720
+
1721
+ if self.execution_space == "cuda":
1722
+ # Set stream for the FFT.
1723
+ cufft.set_stream(self.handle, exec_stream_holder.ptr) # type: ignore[union-attr]
1724
+
1725
+ self.operand, self.operand_backup = _copy_operand_perhaps(
1726
+ self.operand,
1727
+ operand,
1728
+ operand_stream_holder,
1729
+ self.execution_space,
1730
+ self.memory_space,
1731
+ self.device_id,
1732
+ self.fft_abstract_type,
1733
+ self.logger,
1734
+ )
1735
+ operand = self.operand
1736
+
1737
+ # Update operand layout and plan traits.
1738
+ self.operand_layout = TensorLayout(shape=operand.shape, strides=operand.strides)
1739
+ self.logger.info(f"The reset operand shape = {self.operand_layout.shape}, and strides = {self.operand_layout.strides}.")
1740
+
1741
+ self.plan_traits = get_fft_plan_traits(
1742
+ operand.shape,
1743
+ operand.strides,
1744
+ operand.dtype,
1745
+ self.axes,
1746
+ self.execution_options,
1747
+ fft_abstract_type=self.fft_abstract_type,
1748
+ last_axis_parity=self.options.last_axis_parity,
1749
+ result_layout=self.options.result_layout,
1750
+ logger=self.logger,
1751
+ )
1752
+ result_shape, result_strides = (
1753
+ (self.operand_layout.shape, self.operand_layout.strides)
1754
+ if self.inplace
1755
+ else (self.plan_traits.result_shape, self.plan_traits.result_strides)
1756
+ )
1757
+ self.logger.info(f"The result shape = {result_shape}, and strides = {result_strides}.")
1758
+
1759
+ self.logger.info("The operand has been reset to the specified operand.")
1760
+
1761
+ def get_input_layout(self):
1762
+ """
1763
+ Returns a pair of tuples: shape and strides of the FFT input.
1764
+
1765
+ .. note::
1766
+ In some cases, the FFT operation requires taking a copy of the input tensor
1767
+ (e.g. C2R cuFFT, or provided tensor resides on CPU but FFT is executed on GPU).
1768
+ The copied tensor strides may differ from the input tensor passed by the user,
1769
+ if the original tensor's strides do not conform to dense C-like layout.
1770
+ """
1771
+ return self.operand_layout.shape, self.operand_layout.strides
1772
+
1773
+ def get_output_layout(self):
1774
+ """
1775
+ Returns a pair of tuples: shape and strides of the FFT output.
1776
+ """
1777
+ return (
1778
+ (self.operand_layout.shape, self.operand_layout.strides)
1779
+ if self.inplace
1780
+ else (self.plan_traits.result_shape, self.plan_traits.result_strides)
1781
+ )
1782
+
1783
+ def _check_planned(self, *args, **kwargs):
1784
+ """ """
1785
+ what = kwargs["what"]
1786
+ if not self.fft_planned:
1787
+ raise RuntimeError(f"{what} cannot be performed before plan() has been called.")
1788
+
1789
+ def _check_valid_operand(self, *args, **kwargs):
1790
+ """ """
1791
+ what = kwargs["what"]
1792
+ if self.operand is None:
1793
+ raise RuntimeError(
1794
+ f"{what} cannot be performed if the input operand has been set to None. Use reset_operand() to set the "
1795
+ f"desired input before using performing the {what.lower()}."
1796
+ )
1797
+
1798
+ def _free_workspace_memory(self, exception: Exception | None = None) -> bool:
1799
+ """
1800
+ Free workspace by releasing the MemoryPointer object.
1801
+ """
1802
+ if self.workspace_ptr is None:
1803
+ return True
1804
+
1805
+ self.workspace_ptr = None
1806
+ self.logger.debug("[_free_workspace_memory] The workspace has been released.")
1807
+
1808
+ return True
1809
+
1810
+ @utils.precondition(_check_valid_fft)
1811
+ @utils.precondition(_check_planned, "Workspace memory allocation")
1812
+ @utils.atomic(_free_workspace_memory, method=True)
1813
+ def _allocate_workspace_memory(self, stream_holder: StreamHolder):
1814
+ """
1815
+ Allocate workspace memory using the specified allocator.
1816
+ """
1817
+
1818
+ assert self._workspace_allocated_here is False, "Internal Error."
1819
+
1820
+ self.logger.debug("Allocating workspace for performing the FFT...")
1821
+ with utils.device_ctx(self.device_id), stream_holder.ctx:
1822
+ try:
1823
+ if isinstance(self.allocator, memory.BaseCUDAMemoryManagerAsync):
1824
+ self.workspace_ptr = self.allocator.memalloc_async(self.workspace_size, stream_holder.obj)
1825
+ else:
1826
+ self.workspace_ptr = self.allocator.memalloc(self.workspace_size) # type: ignore[union-attr]
1827
+ self._workspace_allocated_here = True
1828
+ except TypeError as e:
1829
+ message = (
1830
+ "The method 'memalloc' in the allocator object must conform to the interface in the "
1831
+ "'BaseCUDAMemoryManager' protocol."
1832
+ )
1833
+ raise TypeError(message) from e
1834
+ raw_workspace_ptr = utils.get_ptr_from_memory_pointer(self.workspace_ptr)
1835
+ cufft.set_work_area(self.handle, raw_workspace_ptr)
1836
+
1837
+ self.workspace_stream = stream_holder.obj
1838
+ self.logger.debug(
1839
+ f"Finished allocating device workspace of size {formatters.MemoryStr(self.workspace_size)} in the context "
1840
+ f"of stream {self.workspace_stream}."
1841
+ )
1842
+
1843
+ def _allocate_workspace_memory_perhaps(self, stream_holder: StreamHolder):
1844
+ """
1845
+ Allocate workspace memory using the specified allocator, if it hasn't already been
1846
+ done.
1847
+ """
1848
+ if self.execution_space != "cuda" or self.workspace_ptr is not None:
1849
+ return
1850
+
1851
+ return self._allocate_workspace_memory(stream_holder)
1852
+
1853
+ @utils.precondition(_check_valid_fft)
1854
+ def _free_workspace_memory_perhaps(self, release_workspace):
1855
+ """
1856
+ Free workspace memory if if 'release_workspace' is True.
1857
+ """
1858
+ if not release_workspace:
1859
+ return
1860
+
1861
+ # Establish ordering wrt the computation and free workspace if it's more than the
1862
+ # specified cache limit.
1863
+ if self.last_compute_event is not None:
1864
+ self.workspace_stream.wait(self.last_compute_event)
1865
+ self.logger.debug("Established ordering with respect to the computation before releasing the workspace.")
1866
+ self.last_compute_event = None
1867
+
1868
+ self.logger.debug("[_free_workspace_memory_perhaps] The workspace memory will be released.")
1869
+ self._free_workspace_memory()
1870
+
1871
+ return True
1872
+
1873
+ def _release_workspace_memory_perhaps(self, exception: Exception | None = None) -> bool:
1874
+ """
1875
+ Free workspace memory if it was allocated in this call
1876
+ (self._workspace_allocated_here == True) when an exception occurs.
1877
+ """
1878
+ release_workspace = self._workspace_allocated_here
1879
+ self.logger.debug(
1880
+ f"[_release_workspace_memory_perhaps] The release_workspace flag is set to {release_workspace} based upon "
1881
+ "the value of 'workspace_allocated_here'."
1882
+ )
1883
+ self._free_workspace_memory_perhaps(release_workspace)
1884
+ self._workspace_allocated_here = False
1885
+ return True
1886
+
1887
+ @utils.precondition(_check_valid_fft)
1888
+ @utils.precondition(_check_planned, "Execution")
1889
+ @utils.precondition(_check_valid_operand, "Execution")
1890
+ @utils.atomic(_release_workspace_memory_perhaps, method=True)
1891
+ def execute(self, direction: FFTDirection | None = None, stream: AnyStream | None = None, release_workspace: bool = False):
1892
+ """
1893
+ Execute the FFT operation.
1894
+
1895
+ Args:
1896
+ direction: {direction}
1897
+
1898
+ stream: {stream}
1899
+
1900
+ release_workspace: {release_workspace}
1901
+
1902
+ Returns:
1903
+ The transformed operand, which remains on the same device and utilizes the same
1904
+ package as the input operand. The data type and shape of the transformed operand
1905
+ depend on the type of input operand:
1906
+
1907
+ - For C2C FFT, the data type and shape remain identical to the input.
1908
+ - For R2C and C2R FFT, both data type and shape differ from the input.
1909
+ """
1910
+
1911
+ log_info = self.logger.isEnabledFor(logging.INFO)
1912
+ log_debug = self.logger.isEnabledFor(logging.DEBUG)
1913
+
1914
+ if direction is None:
1915
+ direction = _get_fft_default_direction(self.fft_abstract_type)
1916
+ else:
1917
+ direction = self._get_validate_direction(direction)
1918
+
1919
+ exec_stream_holder, operand_stream_holder = self._get_or_create_stream_maybe(stream)
1920
+
1921
+ if self.execution_space == "cuda":
1922
+ # Set stream for the FFT.
1923
+ cufft.set_stream(self.handle, exec_stream_holder.ptr) # type: ignore[union-attr]
1924
+
1925
+ # Allocate workspace if needed.
1926
+ self._allocate_workspace_memory_perhaps(exec_stream_holder) # type: ignore[arg-type]
1927
+ # Allocate output operand if needed
1928
+ if self.inplace:
1929
+ result_ptr = self.operand.data_ptr
1930
+ else:
1931
+ if self._preallocated_result is not None:
1932
+ assert self.execution_space == "cpu"
1933
+ self.result = self._preallocated_result
1934
+ self._preallocated_result = None
1935
+ else:
1936
+ self.result = self._allocate_result_operand(exec_stream_holder, log_debug)
1937
+ result_ptr = self.result.data_ptr
1938
+
1939
+ if log_info:
1940
+ self.logger.info(f"Starting FFT {self.fft_abstract_type} calculation in the {direction.name} direction...") # type: ignore[union-attr]
1941
+ self.logger.info(f"{self.call_prologue}")
1942
+
1943
+ if self.execution_space == "cpu":
1944
+ with utils.host_call_ctx(timing=log_info) as elapsed:
1945
+ fftw.execute(self.handle, self.operand.data_ptr, result_ptr, direction)
1946
+ else:
1947
+ assert isinstance(self.device_id, int), self.device_id
1948
+ assert exec_stream_holder is not None
1949
+ with utils.cuda_call_ctx(exec_stream_holder, self.blocking, timing=log_info) as (
1950
+ self.last_compute_event,
1951
+ elapsed,
1952
+ ):
1953
+ if log_debug:
1954
+ self.logger.debug("The cuFFT execution function is 'xt_exec'.")
1955
+ cufft.xt_exec(self.handle, self.operand.data_ptr, result_ptr, direction)
1956
+
1957
+ if log_info and elapsed.data is not None:
1958
+ self.logger.info(f"The FFT calculation took {elapsed.data:.3f} ms to complete.")
1959
+
1960
+ # Establish ordering wrt the computation and free workspace if it's more than the
1961
+ # specified cache limit.
1962
+ self._free_workspace_memory_perhaps(release_workspace)
1963
+
1964
+ # reset workspace allocation tracking to False at the end of the methods where
1965
+ # workspace memory is potentially allocated. This is necessary to prevent any
1966
+ # exceptions raised before method entry from using stale tracking values.
1967
+ self._workspace_allocated_here = False
1968
+
1969
+ # Return the result.
1970
+ result = self.operand if self.inplace else self.result
1971
+ if self.memory_space == self.execution_space:
1972
+ out = result.tensor
1973
+ else:
1974
+ if self.options.inplace: # Don't use self.inplace here, because we always set it to True for CPU tensors.
1975
+ self.operand_backup.copy_(result, stream_holder=operand_stream_holder)
1976
+ out = self.operand_backup.tensor
1977
+ else:
1978
+ target_dev = "cpu" if self.memory_space == "cpu" else self.operand_device_id
1979
+ out = result.to(target_dev, stream_holder=operand_stream_holder).tensor # type: ignore[arg-type]
1980
+
1981
+ # Release internal reference to the result to permit recycling of memory.
1982
+ self.result = None # type: ignore
1983
+
1984
+ return out
1985
+
1986
+ def free(self):
1987
+ """Free FFT resources.
1988
+
1989
+ It is recommended that the :class:`FFT` object be used within a context, but if it
1990
+ is not possible then this method must be called explicitly to ensure that the FFT
1991
+ resources (especially internal library objects) are properly cleaned up.
1992
+ """
1993
+
1994
+ if not self.valid_state:
1995
+ return
1996
+
1997
+ try:
1998
+ # Future operations on the workspace stream should be ordered after the
1999
+ # computation.
2000
+ if self.last_compute_event is not None:
2001
+ self.workspace_stream.wait(self.last_compute_event)
2002
+ self.last_compute_event = None
2003
+
2004
+ self._free_workspace_memory()
2005
+
2006
+ if self.handle is not None:
2007
+ if self.execution_space == "cuda":
2008
+ cufft.destroy(self.handle)
2009
+ else:
2010
+ fftw.destroy(self.handle)
2011
+ self.handle = None
2012
+
2013
+ except Exception as e:
2014
+ self.logger.critical("Internal error: only part of the FFT object's resources have been released.")
2015
+ self.logger.critical(str(e))
2016
+ raise e
2017
+ finally:
2018
+ self.valid_state = False
2019
+
2020
+ self.logger.info("The FFT object's resources have been released.")
2021
+
2022
+
2023
+ def _fft(
2024
+ x,
2025
+ *,
2026
+ axes: Sequence[int] | None = None,
2027
+ direction: FFTDirection | None = None,
2028
+ options: FFTOptions | None = None,
2029
+ execution: ExecutionCPU | ExecutionCUDA | None = None,
2030
+ prolog: DeviceCallable | None = None,
2031
+ epilog: DeviceCallable | None = None,
2032
+ stream: AnyStream | None = None,
2033
+ check_dtype: str | None = None,
2034
+ ):
2035
+ r"""
2036
+ fft({function_signature})
2037
+
2038
+ Perform an N-D *complex-to-complex* (C2C) FFT on the provided complex operand.
2039
+
2040
+ Args:
2041
+ operand: {operand}
2042
+
2043
+ axes: {axes}
2044
+
2045
+ options: {options}
2046
+
2047
+ execution: {execution}
2048
+
2049
+ prolog: {prolog}
2050
+
2051
+ epilog: {epilog}
2052
+
2053
+ stream: {stream}
2054
+
2055
+ Returns:
2056
+ A transformed operand that retains the same data type and shape as the input. It
2057
+ remains on the same device and uses the same package as the input operand.
2058
+
2059
+ See Also:
2060
+ :func:`ifft`, :func:`irfft`, :func:`rfft`, :class:`FFT`
2061
+
2062
+ Examples:
2063
+
2064
+ >>> import cupy as cp
2065
+ >>> import nvmath
2066
+
2067
+ Create a 3-D complex128 ndarray on the GPU:
2068
+
2069
+ >>> shape = 256, 256, 256
2070
+ >>> a = cp.random.rand(*shape, dtype=cp.float64) + 1j * cp.random.rand(
2071
+ ... *shape, dtype=cp.float64
2072
+ ... )
2073
+
2074
+ Perform a 3-D C2C FFT using :func:`fft`. The result `r` is also a CuPy complex128
2075
+ ndarray:
2076
+
2077
+ >>> r = nvmath.fft.fft(a)
2078
+
2079
+ User may also perform FFT along a subset of dimensions, e.g, 2-D C2C FFT along the
2080
+ first two dimensions, batched along the last dimension:
2081
+
2082
+ >>> axes = 0, 1
2083
+ >>> r = nvmath.fft.fft(a, axes=axes)
2084
+
2085
+ For C2C type FFT operation, the output can be directly computed inplace thus
2086
+ overwriting the input operand. This can be specified using options to the FFT:
2087
+
2088
+ >>> o = nvmath.fft.FFTOptions(inplace=True)
2089
+ >>> r = nvmath.fft.fft(a, options=o)
2090
+ >>> r is a
2091
+ True
2092
+
2093
+ See :class:`FFTOptions` for the complete list of available options.
2094
+
2095
+ The package current stream is used by default, but a stream can be explicitly
2096
+ provided to the FFT operation. This can be done if the FFT operand is computed on a
2097
+ different stream, for example:
2098
+
2099
+ >>> s = cp.cuda.Stream()
2100
+ >>> with s:
2101
+ ... a = cp.random.rand(*shape) + 1j * cp.random.rand(*shape)
2102
+ >>> r = nvmath.fft.fft(a, stream=s)
2103
+
2104
+ The operation above runs on stream `s` and is ordered with respect to the input
2105
+ computation.
2106
+
2107
+ Create a NumPy ndarray on the CPU.
2108
+
2109
+ >>> import numpy as np
2110
+ >>> b = np.random.rand(*shape) + 1j * np.random.rand(*shape)
2111
+
2112
+ Provide the NumPy ndarray to :func:`fft`, with the result also being a NumPy
2113
+ ndarray:
2114
+
2115
+ >>> r = nvmath.fft.fft(b)
2116
+
2117
+ Notes:
2118
+ - This function only takes complex operand for C2C transformation. If the user
2119
+ wishes to perform full FFT transformation on real input, please cast the input to
2120
+ the corresponding complex data type.
2121
+ - This function is a convenience wrapper around :class:`FFT` and and is specifically
2122
+ meant for *single* use. The same computation can be performed with the stateful
2123
+ API using the default `direction` argument in :meth:`FFT.execute`.
2124
+
2125
+ Further examples can be found in the `nvmath/examples/fft
2126
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/fft>`_ directory.
2127
+ """
2128
+ if check_dtype is not None:
2129
+ assert check_dtype in {"real", "complex"}, "internal error"
2130
+ operand = tensor_wrapper.wrap_operand(x)
2131
+ if ("complex" in operand.dtype) != (check_dtype == "complex"):
2132
+ raise ValueError(f"This function expects {check_dtype} operand, found {operand.dtype}")
2133
+
2134
+ with FFT(x, axes=axes, options=options, execution=execution, stream=stream) as fftobj:
2135
+ # Plan the FFT.
2136
+ fftobj.plan(stream=stream, prolog=prolog, epilog=epilog, direction=direction)
2137
+
2138
+ # Execute the FFT.
2139
+ result = fftobj.execute(direction=direction, stream=stream)
2140
+
2141
+ return result
2142
+
2143
+
2144
+ # Forward C2C FFT Function.
2145
+ fft = functools.wraps(_fft)(functools.partial(_fft, direction=FFTDirection.FORWARD, check_dtype="complex"))
2146
+ fft.__doc__ = fft.__doc__.format(**SHARED_FFT_DOCUMENTATION) # type: ignore
2147
+ fft.__name__ = "fft"
2148
+
2149
+
2150
+ # Forward R2C FFT Function
2151
+ @utils.docstring_decorator(SHARED_FFT_DOCUMENTATION, skip_missing=False)
2152
+ def rfft(
2153
+ operand,
2154
+ *,
2155
+ axes: Sequence[int] | None = None,
2156
+ options: FFTOptions | None = None,
2157
+ execution: ExecutionCPU | ExecutionCUDA | None = None,
2158
+ prolog: DeviceCallable | None = None,
2159
+ epilog: DeviceCallable | None = None,
2160
+ stream: AnyStream | None = None,
2161
+ ):
2162
+ r"""
2163
+ rfft({function_signature})
2164
+
2165
+ Perform an N-D *real-to-complex* (R2C) FFT on the provided real operand.
2166
+
2167
+ Args:
2168
+ operand: {operand}
2169
+
2170
+ axes: {axes}
2171
+
2172
+ options: {options}
2173
+
2174
+ execution: {execution}
2175
+
2176
+ prolog: {prolog}
2177
+
2178
+ epilog: {epilog}
2179
+
2180
+ stream: {stream}
2181
+
2182
+ Returns:
2183
+ A complex tensor that remains on the same device and belongs to the same package as
2184
+ the input operand. The extent of the last transformed axis in the result will be
2185
+ ``operand.shape[axes[-1]] // 2 + 1``.
2186
+
2187
+
2188
+ See Also:
2189
+ :func:`fft`, :func:`irfft`, :class:`FFT`.
2190
+ """
2191
+ wrapped_operand = tensor_wrapper.wrap_operand(operand)
2192
+ # check if input operand if real type
2193
+ if "complex" in wrapped_operand.dtype:
2194
+ raise RuntimeError(f"rfft expects a real input, but got {wrapped_operand.dtype}. Please use fft for complex input.")
2195
+
2196
+ return _fft(
2197
+ operand,
2198
+ axes=axes,
2199
+ options=options,
2200
+ execution=execution,
2201
+ prolog=prolog,
2202
+ epilog=epilog,
2203
+ stream=stream,
2204
+ check_dtype="real",
2205
+ )
2206
+
2207
+
2208
+ # Inverse C2C/R2C FFT Function.
2209
+ ifft = functools.wraps(_fft)(functools.partial(_fft, direction=FFTDirection.INVERSE, check_dtype="complex"))
2210
+ ifft.__doc__ = """
2211
+ ifft({function_signature})
2212
+
2213
+ Perform an N-D *complex-to-complex* (C2C) inverse FFT on the provided complex operand.
2214
+ The direction is implicitly inverse.
2215
+
2216
+ Args:
2217
+ operand: {operand}
2218
+
2219
+ axes: {axes}
2220
+
2221
+ options: {options}
2222
+
2223
+ execution: {execution}
2224
+
2225
+ prolog: {prolog}
2226
+
2227
+ epilog: {epilog}
2228
+
2229
+ stream: {stream}
2230
+
2231
+ Returns:
2232
+ A transformed operand that retains the same data type and shape as the input. It
2233
+ remains on the same device and uses the same package as the input operand.
2234
+
2235
+ See Also:
2236
+ :func:`fft`, :func:`irfft`, :class:`FFT`.
2237
+
2238
+ Notes:
2239
+ - This function only takes complex operand for C2C transformation. If users wishes
2240
+ to perform full FFT transformation on real input, please cast the input to the
2241
+ corresponding complex data type.
2242
+ - This function is a convenience wrapper around :class:`FFT` and and is specifically
2243
+ meant for *single* use. The same computation can be performed with the stateful
2244
+ API by passing the argument ``direction='inverse'`` when calling
2245
+ :meth:`FFT.execute`.
2246
+ """.format(**SHARED_FFT_DOCUMENTATION)
2247
+ ifft.__name__ = "ifft"
2248
+
2249
+
2250
+ # Inverse C2R FFT Function.
2251
+ @utils.docstring_decorator(SHARED_FFT_DOCUMENTATION, skip_missing=False)
2252
+ def irfft(
2253
+ x,
2254
+ *,
2255
+ axes: Sequence[int] | None = None,
2256
+ options: FFTOptions | None = None,
2257
+ execution: ExecutionCPU | ExecutionCUDA | None = None,
2258
+ prolog: DeviceCallable | None = None,
2259
+ epilog: DeviceCallable | None = None,
2260
+ stream: AnyStream | None = None,
2261
+ ):
2262
+ """
2263
+ irfft({function_signature})
2264
+
2265
+ Perform an N-D *complex-to-real* (C2R) FFT on the provided complex operand. The
2266
+ direction is implicitly inverse.
2267
+
2268
+ Args:
2269
+ operand: {operand}
2270
+
2271
+ axes: {axes}
2272
+
2273
+ options: {options}
2274
+
2275
+ execution: {execution}
2276
+
2277
+ prolog: {prolog}
2278
+
2279
+ epilog: {epilog}
2280
+
2281
+ stream: {stream}
2282
+
2283
+ Returns:
2284
+ A real tensor that remains on the same device and belongs to the same package as the
2285
+ input operand. The extent of the last transformed axis in the result will be
2286
+ ``(operand.shape[axes[-1]] - 1) * 2`` if :attr:`FFTOptions.last_axis_parity` is
2287
+ ``even``, or ``operand.shape[axes[-1]] * 2 - 1`` if
2288
+ :attr:`FFTOptions.last_axis_parity` is ``odd``.
2289
+
2290
+ See Also:
2291
+ :func:`fft`, :func:`ifft`, :class:`FFT`.
2292
+
2293
+ Examples:
2294
+
2295
+ >>> import cupy as cp
2296
+ >>> import nvmath
2297
+
2298
+ Create a 3-D symmetric complex128 ndarray on the GPU:
2299
+
2300
+ >>> shape = 512, 768, 256
2301
+ >>> a = nvmath.fft.rfft(cp.random.rand(*shape, dtype=cp.float64))
2302
+
2303
+ Perform a 3-D C2R FFT using the :func:`irfft` wrapper. The result `r` is a CuPy
2304
+ float64 ndarray:
2305
+
2306
+ >>> r = nvmath.fft.irfft(a)
2307
+ >>> r.dtype
2308
+ dtype('float64')
2309
+
2310
+ Notes:
2311
+
2312
+ - This function performs an inverse C2R N-D FFT, which is similar to `irfftn` but
2313
+ different from `irfft` in various numerical packages.
2314
+ - This function is a convenience wrapper around :class:`FFT` and and is specifically
2315
+ meant for *single* use. The same computation can be performed with the stateful
2316
+ API by setting :attr:`FFTOptions.fft_type` to ``'C2R'`` and passing the argument
2317
+ ``direction='inverse'`` when calling :meth:`FFT.execute`.
2318
+ - **The input to this function must be Hermitian-symmetric, otherwise the result is
2319
+ undefined.** While the symmetry requirement is partially captured by the different
2320
+ extents in the last transformed dimension between the input and result, there are
2321
+ additional `constraints
2322
+ <https://docs.nvidia.com/cuda/cufft/#fourier-transform-types>`_. As a specific
2323
+ example, 1-D transforms require the first element (and the last element, if the
2324
+ extent is even) of the input to be purely real-valued. In addition, if the input
2325
+ to `irfft` was generated using an R2C FFT with an odd last axis size,
2326
+ :attr:`FFTOptions.last_axis_parity` must be set to ``odd`` to recover the original
2327
+ signal.
2328
+ - For more details, please refer to `C2R example
2329
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/fft/example07_c2r.py>`_
2330
+ and `odd C2R example
2331
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/fft/example07_c2r_odd.py>`_.
2332
+ """
2333
+ options = utils.check_or_create_options(FFTOptions, options, "FFT options")
2334
+ assert options is not None
2335
+ options.fft_type = "C2R"
2336
+ return _fft(
2337
+ x,
2338
+ axes=axes,
2339
+ direction=FFTDirection.INVERSE,
2340
+ options=options,
2341
+ execution=execution,
2342
+ prolog=prolog,
2343
+ epilog=epilog,
2344
+ stream=stream,
2345
+ check_dtype="complex",
2346
+ )