nvmath-python 1.0.0__cp314-cp314t-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. nvmath/__init__.pxd +0 -0
  2. nvmath/__init__.py +45 -0
  3. nvmath/_internal/__init__.py +0 -0
  4. nvmath/_internal/attribute_ifc_factory.py +330 -0
  5. nvmath/_internal/layout.py +70 -0
  6. nvmath/_internal/templates.py +130 -0
  7. nvmath/_internal/threadsafe.py +106 -0
  8. nvmath/_internal/utils.py +43 -0
  9. nvmath/_internal/workspace.py +490 -0
  10. nvmath/_utils.py +147 -0
  11. nvmath/bindings/__init__.py +60 -0
  12. nvmath/bindings/_internal/__init__.pxd +0 -0
  13. nvmath/bindings/_internal/__init__.py +0 -0
  14. nvmath/bindings/_internal/common_types.pxd +31 -0
  15. nvmath/bindings/_internal/cublas.cp314t-win_amd64.pyd +0 -0
  16. nvmath/bindings/_internal/cublas.pxd +530 -0
  17. nvmath/bindings/_internal/cublasLt.cp314t-win_amd64.pyd +0 -0
  18. nvmath/bindings/_internal/cublasLt.pxd +59 -0
  19. nvmath/bindings/_internal/cublasMp.pxd +52 -0
  20. nvmath/bindings/_internal/cudss.cp314t-win_amd64.pyd +0 -0
  21. nvmath/bindings/_internal/cudss.pxd +54 -0
  22. nvmath/bindings/_internal/cufft.cp314t-win_amd64.pyd +0 -0
  23. nvmath/bindings/_internal/cufft.pxd +70 -0
  24. nvmath/bindings/_internal/cufftMp.pxd +77 -0
  25. nvmath/bindings/_internal/curand.cp314t-win_amd64.pyd +0 -0
  26. nvmath/bindings/_internal/curand.pxd +42 -0
  27. nvmath/bindings/_internal/cusolver.cp314t-win_amd64.pyd +0 -0
  28. nvmath/bindings/_internal/cusolver.pxd +15 -0
  29. nvmath/bindings/_internal/cusolverDn.cp314t-win_amd64.pyd +0 -0
  30. nvmath/bindings/_internal/cusolverDn.pxd +406 -0
  31. nvmath/bindings/_internal/cusolverMp.pxd +71 -0
  32. nvmath/bindings/_internal/cusolverSp.cp314t-win_amd64.pyd +0 -0
  33. nvmath/bindings/_internal/cusolverSp.pxd +75 -0
  34. nvmath/bindings/_internal/cusparse.cp314t-win_amd64.pyd +0 -0
  35. nvmath/bindings/_internal/cusparse.pxd +471 -0
  36. nvmath/bindings/_internal/cusparseLt.cp314t-win_amd64.pyd +0 -0
  37. nvmath/bindings/_internal/cusparseLt.pxd +48 -0
  38. nvmath/bindings/_internal/cutensor.cp314t-win_amd64.pyd +0 -0
  39. nvmath/bindings/_internal/cutensor.pxd +58 -0
  40. nvmath/bindings/_internal/mathdx.cp314t-win_amd64.pyd +0 -0
  41. nvmath/bindings/_internal/mathdx.pxd +116 -0
  42. nvmath/bindings/_internal/nvshmem.pxd +29 -0
  43. nvmath/bindings/_internal/utils.cp314t-win_amd64.pyd +0 -0
  44. nvmath/bindings/_internal/utils.pxd +174 -0
  45. nvmath/bindings/_internal/utils.pyi +10 -0
  46. nvmath/bindings/cublas.cp314t-win_amd64.pyd +0 -0
  47. nvmath/bindings/cublas.pxd +558 -0
  48. nvmath/bindings/cublas.pyi +812 -0
  49. nvmath/bindings/cublasLt.cp314t-win_amd64.pyd +0 -0
  50. nvmath/bindings/cublasLt.pxd +109 -0
  51. nvmath/bindings/cublasLt.pyi +1461 -0
  52. nvmath/bindings/cublasMp.pxd +85 -0
  53. nvmath/bindings/cublasMp.pyi +267 -0
  54. nvmath/bindings/cudss.cp314t-win_amd64.pyd +0 -0
  55. nvmath/bindings/cudss.pxd +98 -0
  56. nvmath/bindings/cudss.pyi +443 -0
  57. nvmath/bindings/cufft.cp314t-win_amd64.pyd +0 -0
  58. nvmath/bindings/cufft.pxd +118 -0
  59. nvmath/bindings/cufft.pyi +301 -0
  60. nvmath/bindings/cufftMp.pxd +124 -0
  61. nvmath/bindings/cufftMp.pyi +326 -0
  62. nvmath/bindings/curand.cp314t-win_amd64.pyd +0 -0
  63. nvmath/bindings/curand.pxd +71 -0
  64. nvmath/bindings/curand.pyi +189 -0
  65. nvmath/bindings/cusolver.cp314t-win_amd64.pyd +0 -0
  66. nvmath/bindings/cusolver.pxd +62 -0
  67. nvmath/bindings/cusolver.pyi +320 -0
  68. nvmath/bindings/cusolverDn.cp314t-win_amd64.pyd +0 -0
  69. nvmath/bindings/cusolverDn.pxd +430 -0
  70. nvmath/bindings/cusolverDn.pyi +422 -0
  71. nvmath/bindings/cusolverMp.pxd +98 -0
  72. nvmath/bindings/cusolverMp.pyi +114 -0
  73. nvmath/bindings/cusolverSp.cp314t-win_amd64.pyd +0 -0
  74. nvmath/bindings/cusolverSp.pxd +95 -0
  75. nvmath/bindings/cusolverSp.pyi +70 -0
  76. nvmath/bindings/cusparse.cp314t-win_amd64.pyd +0 -0
  77. nvmath/bindings/cusparse.pxd +546 -0
  78. nvmath/bindings/cusparse.pyi +1017 -0
  79. nvmath/bindings/cusparseLt.cp314t-win_amd64.pyd +0 -0
  80. nvmath/bindings/cusparseLt.pxd +99 -0
  81. nvmath/bindings/cusparseLt.pyi +252 -0
  82. nvmath/bindings/cutensor.cp314t-win_amd64.pyd +0 -0
  83. nvmath/bindings/cutensor.pxd +98 -0
  84. nvmath/bindings/cutensor.pyi +324 -0
  85. nvmath/bindings/cycublas.cp314t-win_amd64.pyd +0 -0
  86. nvmath/bindings/cycublas.pxd +664 -0
  87. nvmath/bindings/cycublasLt.cp314t-win_amd64.pyd +0 -0
  88. nvmath/bindings/cycublasLt.pxd +1045 -0
  89. nvmath/bindings/cycublasMp.pxd +171 -0
  90. nvmath/bindings/cycudss.cp314t-win_amd64.pyd +0 -0
  91. nvmath/bindings/cycudss.pxd +277 -0
  92. nvmath/bindings/cycufft.cp314t-win_amd64.pyd +0 -0
  93. nvmath/bindings/cycufft.pxd +333 -0
  94. nvmath/bindings/cycufftMp.pxd +342 -0
  95. nvmath/bindings/cycurand.cp314t-win_amd64.pyd +0 -0
  96. nvmath/bindings/cycurand.pxd +141 -0
  97. nvmath/bindings/cycusolver.cp314t-win_amd64.pyd +0 -0
  98. nvmath/bindings/cycusolver.pxd +137 -0
  99. nvmath/bindings/cycusolverDn.cp314t-win_amd64.pyd +0 -0
  100. nvmath/bindings/cycusolverDn.pxd +443 -0
  101. nvmath/bindings/cycusolverMp.pxd +107 -0
  102. nvmath/bindings/cycusolverSp.cp314t-win_amd64.pyd +0 -0
  103. nvmath/bindings/cycusolverSp.pxd +93 -0
  104. nvmath/bindings/cycusparse.cp314t-win_amd64.pyd +0 -0
  105. nvmath/bindings/cycusparse.pxd +679 -0
  106. nvmath/bindings/cycusparseLt.cp314t-win_amd64.pyd +0 -0
  107. nvmath/bindings/cycusparseLt.pxd +135 -0
  108. nvmath/bindings/cycutensor.cp314t-win_amd64.pyd +0 -0
  109. nvmath/bindings/cycutensor.pxd +189 -0
  110. nvmath/bindings/cymathdx.cp314t-win_amd64.pyd +0 -0
  111. nvmath/bindings/cymathdx.pxd +552 -0
  112. nvmath/bindings/cynvshmem.pxd +118 -0
  113. nvmath/bindings/mathdx.cp314t-win_amd64.pyd +0 -0
  114. nvmath/bindings/mathdx.pxd +182 -0
  115. nvmath/bindings/mathdx.pyi +1562 -0
  116. nvmath/bindings/nvpl/__init__.pxd +0 -0
  117. nvmath/bindings/nvpl/__init__.py +13 -0
  118. nvmath/bindings/nvpl/_internal/__init__.pxd +0 -0
  119. nvmath/bindings/nvpl/_internal/__init__.py +0 -0
  120. nvmath/bindings/nvpl/_internal/blas.cp314t-win_amd64.pyd +0 -0
  121. nvmath/bindings/nvpl/_internal/blas.pxd +237 -0
  122. nvmath/bindings/nvpl/_internal/fft.cp314t-win_amd64.pyd +0 -0
  123. nvmath/bindings/nvpl/_internal/fft.pxd +36 -0
  124. nvmath/bindings/nvpl/blas.cp314t-win_amd64.pyd +0 -0
  125. nvmath/bindings/nvpl/blas.pxd +131 -0
  126. nvmath/bindings/nvpl/blas.pyi +168 -0
  127. nvmath/bindings/nvpl/cyblas.cp314t-win_amd64.pyd +0 -0
  128. nvmath/bindings/nvpl/cyblas.pxd +280 -0
  129. nvmath/bindings/nvpl/cyfft.cp314t-win_amd64.pyd +0 -0
  130. nvmath/bindings/nvpl/cyfft.pxd +93 -0
  131. nvmath/bindings/nvpl/fft.cp314t-win_amd64.pyd +0 -0
  132. nvmath/bindings/nvpl/fft.pxd +100 -0
  133. nvmath/bindings/nvpl/fft.pyi +168 -0
  134. nvmath/bindings/nvshmem.pxd +54 -0
  135. nvmath/bindings/nvshmem.pyi +191 -0
  136. nvmath/device/__init__.py +38 -0
  137. nvmath/device/_deprecated.py +33 -0
  138. nvmath/device/common.py +315 -0
  139. nvmath/device/common_backend.py +131 -0
  140. nvmath/device/common_cuda.py +201 -0
  141. nvmath/device/common_numba.py +300 -0
  142. nvmath/device/common_numba_cuda_mlir.py +202 -0
  143. nvmath/device/common_opaque_tensor.py +201 -0
  144. nvmath/device/cublasdx.py +1606 -0
  145. nvmath/device/cublasdx_backend.py +860 -0
  146. nvmath/device/cublasdx_numba.py +1534 -0
  147. nvmath/device/cublasdx_numba_cuda_mlir.py +208 -0
  148. nvmath/device/cufftdx.py +373 -0
  149. nvmath/device/cufftdx_backend.py +220 -0
  150. nvmath/device/cufftdx_numba.py +140 -0
  151. nvmath/device/cufftdx_numba_cuda_mlir.py +79 -0
  152. nvmath/device/curand_kernel.py +9147 -0
  153. nvmath/device/cusolverdx.py +2708 -0
  154. nvmath/device/cusolverdx_backend.py +440 -0
  155. nvmath/device/cusolverdx_numba.py +567 -0
  156. nvmath/device/cusolverdx_numba_cuda_mlir.py +604 -0
  157. nvmath/device/cusolverdx_overload_backend.py +1029 -0
  158. nvmath/device/llvm_array.py +29 -0
  159. nvmath/device/random.py +441 -0
  160. nvmath/device/random_helpers.py +23 -0
  161. nvmath/device/random_states.py +187 -0
  162. nvmath/device/types.py +138 -0
  163. nvmath/device/vector_types_numba.py +259 -0
  164. nvmath/distributed/__init__.py +200 -0
  165. nvmath/distributed/_internal/__init__.py +0 -0
  166. nvmath/distributed/_internal/nccl.py +86 -0
  167. nvmath/distributed/_internal/nvshmem.py +307 -0
  168. nvmath/distributed/_internal/symmetric_memory.py +35 -0
  169. nvmath/distributed/_internal/tensor_ifc.py +70 -0
  170. nvmath/distributed/_internal/tensor_ifc_cupy.py +68 -0
  171. nvmath/distributed/_internal/tensor_ifc_host_device.py +172 -0
  172. nvmath/distributed/_internal/tensor_ifc_numpy.py +46 -0
  173. nvmath/distributed/_internal/tensor_ifc_torch.py +162 -0
  174. nvmath/distributed/_internal/tensor_wrapper.py +81 -0
  175. nvmath/distributed/_utils.py +167 -0
  176. nvmath/distributed/distribution/__init__.py +30 -0
  177. nvmath/distributed/distribution/_configuration.py +39 -0
  178. nvmath/distributed/distribution/distributions.py +1024 -0
  179. nvmath/distributed/distribution/redistribute.py +1284 -0
  180. nvmath/distributed/fft/__init__.py +7 -0
  181. nvmath/distributed/fft/_configuration.py +82 -0
  182. nvmath/distributed/fft/fft.py +2742 -0
  183. nvmath/distributed/linalg/__init__.py +22 -0
  184. nvmath/distributed/linalg/_internal/__init__.py +3 -0
  185. nvmath/distributed/linalg/_internal/epilog_protocol.py +586 -0
  186. nvmath/distributed/linalg/_internal/matmul_desc_ifc.py +28 -0
  187. nvmath/distributed/linalg/advanced/__init__.py +8 -0
  188. nvmath/distributed/linalg/advanced/_configuration.py +171 -0
  189. nvmath/distributed/linalg/advanced/matmulmod.py +3573 -0
  190. nvmath/distributed/linalg/generic/__init__.py +8 -0
  191. nvmath/distributed/linalg/generic/_caching.py +66 -0
  192. nvmath/distributed/linalg/generic/_configuration.py +61 -0
  193. nvmath/distributed/linalg/generic/_factorization.py +172 -0
  194. nvmath/distributed/linalg/generic/_initialization.py +966 -0
  195. nvmath/distributed/linalg/generic/_problem_spec.py +511 -0
  196. nvmath/distributed/linalg/generic/solvermod.py +1368 -0
  197. nvmath/distributed/process_group.py +408 -0
  198. nvmath/fft/__init__.py +7 -0
  199. nvmath/fft/_configuration.py +189 -0
  200. nvmath/fft/_exec_utils.py +82 -0
  201. nvmath/fft/_helpers.py +237 -0
  202. nvmath/fft/fft.py +3122 -0
  203. nvmath/internal/__init__.pxd +3 -0
  204. nvmath/internal/__init__.py +10 -0
  205. nvmath/internal/_bindings.cp314t-win_amd64.pyd +0 -0
  206. nvmath/internal/_bindings.pxd +18 -0
  207. nvmath/internal/_device_utils.py +45 -0
  208. nvmath/internal/_layout/__init__.pxd +3 -0
  209. nvmath/internal/_layout/__init__.py +7 -0
  210. nvmath/internal/_layout/_layout.cp314t-win_amd64.pyd +0 -0
  211. nvmath/internal/_layout/_layout.pxd +1303 -0
  212. nvmath/internal/_layout/_layout.pyi +1145 -0
  213. nvmath/internal/enum_utils.py +142 -0
  214. nvmath/internal/formatters.py +87 -0
  215. nvmath/internal/mem_limit.py +51 -0
  216. nvmath/internal/memory.cp314t-win_amd64.pyd +0 -0
  217. nvmath/internal/memory.pxd +13 -0
  218. nvmath/internal/memory.pyi +50 -0
  219. nvmath/internal/ndbuffer/__init__.pxd +3 -0
  220. nvmath/internal/ndbuffer/__init__.py +9 -0
  221. nvmath/internal/ndbuffer/_copy_kernel.cp314t-win_amd64.pyd +0 -0
  222. nvmath/internal/ndbuffer/_copy_kernel.pxd +10 -0
  223. nvmath/internal/ndbuffer/_jit.cp314t-win_amd64.pyd +0 -0
  224. nvmath/internal/ndbuffer/_jit.pxd +7 -0
  225. nvmath/internal/ndbuffer/_ndbuffer.cp314t-win_amd64.pyd +0 -0
  226. nvmath/internal/ndbuffer/_ndbuffer.pxd +40 -0
  227. nvmath/internal/ndbuffer/_ndbuffer.pyi +463 -0
  228. nvmath/internal/ndbuffer/copy_kernel/args.h +34 -0
  229. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/array_view.h +52 -0
  230. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/elementwise.h +68 -0
  231. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/grid_indexer.h +69 -0
  232. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/transposed.h +242 -0
  233. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/type_utils.h +39 -0
  234. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/utils.h +132 -0
  235. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/vec.h +159 -0
  236. nvmath/internal/ndbuffer/copy_kernel/elementwise.h +53 -0
  237. nvmath/internal/ndbuffer/copy_kernel/transposed.h +58 -0
  238. nvmath/internal/package_ifc.py +168 -0
  239. nvmath/internal/package_ifc_cuda.py +57 -0
  240. nvmath/internal/package_ifc_cupy.py +67 -0
  241. nvmath/internal/package_ifc_torch.py +69 -0
  242. nvmath/internal/package_wrapper.py +14 -0
  243. nvmath/internal/tensor_ifc.py +179 -0
  244. nvmath/internal/tensor_ifc_cupy.py +234 -0
  245. nvmath/internal/tensor_ifc_ndbuffer.py +147 -0
  246. nvmath/internal/tensor_ifc_numpy.py +184 -0
  247. nvmath/internal/tensor_ifc_torch.py +178 -0
  248. nvmath/internal/tensor_wrapper.py +160 -0
  249. nvmath/internal/typemaps.py +113 -0
  250. nvmath/internal/utils.py +805 -0
  251. nvmath/linalg/__init__.py +56 -0
  252. nvmath/linalg/_internal/__init__.py +3 -0
  253. nvmath/linalg/_internal/algo_cap_ifc.py +82 -0
  254. nvmath/linalg/_internal/algo_config_ifc.py +43 -0
  255. nvmath/linalg/_internal/batch.py +234 -0
  256. nvmath/linalg/_internal/enum_to_tuples.py +64 -0
  257. nvmath/linalg/_internal/epilog_protocol.py +766 -0
  258. nvmath/linalg/_internal/layout.py +624 -0
  259. nvmath/linalg/_internal/matmul_desc_ifc.py +28 -0
  260. nvmath/linalg/_internal/matmul_pref_ifc.py +27 -0
  261. nvmath/linalg/_internal/matrix_layout_ifc.py +26 -0
  262. nvmath/linalg/_internal/solver_utils.py +432 -0
  263. nvmath/linalg/_internal/typemaps.py +144 -0
  264. nvmath/linalg/_internal/utils.py +157 -0
  265. nvmath/linalg/advanced/__init__.py +8 -0
  266. nvmath/linalg/advanced/_algorithmmod.py +170 -0
  267. nvmath/linalg/advanced/_configuration.py +351 -0
  268. nvmath/linalg/advanced/helpers/__init__.py +5 -0
  269. nvmath/linalg/advanced/helpers/matmul.py +1316 -0
  270. nvmath/linalg/advanced/matmulmod.py +3734 -0
  271. nvmath/linalg/generic/__init__.py +53 -0
  272. nvmath/linalg/generic/_configuration/__init__.py +39 -0
  273. nvmath/linalg/generic/_configuration/layout.py +263 -0
  274. nvmath/linalg/generic/_configuration/match.py +734 -0
  275. nvmath/linalg/generic/_configuration/qualifiers.py +493 -0
  276. nvmath/linalg/generic/_configuration/solver_configuration.py +59 -0
  277. nvmath/linalg/generic/_configuration/wrap.py +217 -0
  278. nvmath/linalg/generic/_dtype.py +15 -0
  279. nvmath/linalg/generic/matmulmod.py +2094 -0
  280. nvmath/linalg/generic/solvermod.py +1301 -0
  281. nvmath/memory.py +279 -0
  282. nvmath/sparse/__init__.py +38 -0
  283. nvmath/sparse/_internal/__init__.py +21 -0
  284. nvmath/sparse/_internal/common_utils.py +147 -0
  285. nvmath/sparse/_internal/cudss_config_ifc.py +702 -0
  286. nvmath/sparse/_internal/cudss_data_ifc.py +399 -0
  287. nvmath/sparse/_internal/cudss_utils.py +506 -0
  288. nvmath/sparse/_internal/cusparse_utils.py +382 -0
  289. nvmath/sparse/_internal/sparse_bsc_ifc.py +303 -0
  290. nvmath/sparse/_internal/sparse_bsr_ifc.py +305 -0
  291. nvmath/sparse/_internal/sparse_coo_ifc.py +256 -0
  292. nvmath/sparse/_internal/sparse_csc_ifc.py +268 -0
  293. nvmath/sparse/_internal/sparse_csr_ifc.py +288 -0
  294. nvmath/sparse/_internal/sparse_dia_ifc.py +242 -0
  295. nvmath/sparse/_internal/sparse_format_helpers.py +601 -0
  296. nvmath/sparse/_internal/sparse_tensor_ifc.py +133 -0
  297. nvmath/sparse/_internal/sparse_ust_ifc.py +141 -0
  298. nvmath/sparse/_internal/utils.py +56 -0
  299. nvmath/sparse/advanced/__init__.py +7 -0
  300. nvmath/sparse/advanced/_configuration.py +227 -0
  301. nvmath/sparse/advanced/direct_solver.py +2069 -0
  302. nvmath/sparse/generic/__init__.py +7 -0
  303. nvmath/sparse/generic/_configuration.py +129 -0
  304. nvmath/sparse/generic/_helpers.py +137 -0
  305. nvmath/sparse/generic/_thunks.py +21 -0
  306. nvmath/sparse/generic/matmulmod.py +2353 -0
  307. nvmath/sparse/ust/__init__.py +7 -0
  308. nvmath/sparse/ust/_converters.py +422 -0
  309. nvmath/sparse/ust/_cpp.py +28 -0
  310. nvmath/sparse/ust/_drawer.py +565 -0
  311. nvmath/sparse/ust/_emitter.py +1033 -0
  312. nvmath/sparse/ust/_jit.py +188 -0
  313. nvmath/sparse/ust/_kernel.py +282 -0
  314. nvmath/sparse/ust/_utils.py +149 -0
  315. nvmath/sparse/ust/interfaces/__init__.py +0 -0
  316. nvmath/sparse/ust/interfaces/torch_interface.py +476 -0
  317. nvmath/sparse/ust/tensor.py +1016 -0
  318. nvmath/sparse/ust/tensor_format.py +957 -0
  319. nvmath/tensor/__init__.py +6 -0
  320. nvmath/tensor/_configuration.py +120 -0
  321. nvmath/tensor/_internal/__init__.py +3 -0
  322. nvmath/tensor/_internal/cutensor_config_ifc.py +279 -0
  323. nvmath/tensor/_internal/cutensor_utils.py +230 -0
  324. nvmath/tensor/_internal/data.py +43 -0
  325. nvmath/tensor/_internal/einsum_parser.py +444 -0
  326. nvmath/tensor/_internal/typemaps.py +96 -0
  327. nvmath/tensor/contract.py +1900 -0
  328. nvmath_python-1.0.0.dist-info/METADATA +134 -0
  329. nvmath_python-1.0.0.dist-info/RECORD +332 -0
  330. nvmath_python-1.0.0.dist-info/WHEEL +5 -0
  331. nvmath_python-1.0.0.dist-info/licenses/LICENSE +177 -0
  332. nvmath_python-1.0.0.dist-info/top_level.txt +2 -0
nvmath/fft/fft.py ADDED
@@ -0,0 +1,3122 @@
1
+ # Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ __all__ = ["FFT", "fft", "ifft", "rfft", "irfft", "UnsupportedLayoutError", "estimate_workspace_size"]
6
+
7
+ import enum
8
+ import functools
9
+ import logging
10
+ import math
11
+ import operator
12
+ from collections.abc import Sequence
13
+ from dataclasses import astuple as data_cls_astuple
14
+ from dataclasses import dataclass, replace
15
+ from typing import Any, Literal
16
+
17
+ from nvmath.bindings import cufft # type: ignore
18
+
19
+ from ._configuration import DeviceCallable, ExecutionCPU, ExecutionCUDA, FFTDirection, FFTOptions
20
+
21
+ try:
22
+ from nvmath.bindings.nvpl import fft as fftw # type: ignore
23
+ except ImportError:
24
+ fftw = None # type: ignore
25
+ from nvmath import memory
26
+ from nvmath._internal.layout import is_contiguous_in_memory, is_contiguous_layout, is_overlapping_layout
27
+ from nvmath._internal.workspace import Workspace
28
+ from nvmath.bindings._internal import utils as _bindings_utils # type: ignore
29
+ from nvmath.fft._exec_utils import _check_init_cufft, _check_init_fftw, _get_num_threads_default
30
+ from nvmath.internal import tensor_wrapper, utils
31
+ from nvmath.internal.package_wrapper import AnyStream, StreamHolder
32
+ from nvmath.internal.typemaps import (
33
+ DATA_TYPE_TO_NAME,
34
+ FFTW_SUPPORTED_COMPLEX,
35
+ FFTW_SUPPORTED_DOUBLE,
36
+ FFTW_SUPPORTED_FLOAT,
37
+ FFTW_SUPPORTED_SINGLE,
38
+ FFTW_SUPPORTED_TYPES,
39
+ NAME_TO_DATA_TYPE,
40
+ cudaDataType,
41
+ )
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
+ optimized_result_layout: bool
83
+ ordered_axes: Sequence[int]
84
+ ordered_fft_in_shape: Sequence[int]
85
+ ordered_fft_in_embedding_shape: Sequence[int]
86
+ ordered_fft_out_shape: Sequence[int]
87
+ fft_batch_size: int
88
+ istride: int
89
+ idistance: int
90
+ ostride: int
91
+ odistance: int
92
+
93
+
94
+ class CBLoadType(enum.IntEnum):
95
+ COMPLEX64 = (0x0,)
96
+ COMPLEX128 = (0x1,)
97
+ FLOAT32 = (0x2,)
98
+ FLOAT64 = (0x3,)
99
+ UNDEFINED = 0x8
100
+
101
+
102
+ class CBStoreType(enum.IntEnum):
103
+ COMPLEX64 = (0x4,)
104
+ COMPLEX128 = (0x5,)
105
+ FLOAT32 = (0x6,)
106
+ FLOAT64 = (0x7,)
107
+ UNDEFINED = 0x8
108
+
109
+
110
+ SHARED_FFT_DOCUMENTATION = utils.COMMON_SHARED_DOC_MAP.copy()
111
+ SHARED_FFT_DOCUMENTATION.update(
112
+ {
113
+ "axes": """\
114
+ The dimensions along which the FFT is performed. ``axes[-1]`` is the 'last transformed' axis for rffts. Currently, it is
115
+ required that the axes are contiguous and include the first or the last dimension. Only up to 3D FFTs are
116
+ supported.""".replace("\n", " "),
117
+ #
118
+ "options": """\
119
+ Specify options for the FFT as a :class:`FFTOptions` object. Alternatively, a `dict` containing the parameters for the
120
+ ``FFTOptions`` constructor can also be provided. If not specified, the value will be set to the default-constructed
121
+ ``FFTOptions`` object.""".replace("\n", " "),
122
+ #
123
+ "execution": """\
124
+ Specify execution space options for the FFT as a :class:`ExecutionCUDA` or :class:`ExecutionCPU` object. Alternatively,
125
+ a string ('cuda' or 'cpu'), or a `dict` with the 'name' key set to 'cpu' or 'cuda' and optional parameters relevant to
126
+ the given execution space. If not specified, the execution space will be selected to match operand's storage (in GPU or
127
+ host memory), and the corresponding :class:`ExecutionCUDA` or :class:`ExecutionCPU` object will be
128
+ default-constructed.""".replace("\n", " "),
129
+ #
130
+ "prolog": """\
131
+ Provide device-callable function in LTO-IR format to use as load-callback as an object of type :class:`DeviceCallable`.
132
+ Alternatively, a `dict` containing the parameters for the ``DeviceCallable`` constructor can also be provided. The
133
+ default is no prolog. Currently, callbacks are supported only with CUDA execution.""".replace("\n", " "),
134
+ #
135
+ "epilog": """\
136
+ Provide device-callable function in LTO-IR format to use as store-callback as an object of type :class:`DeviceCallable`.
137
+ Alternatively, a `dict` containing the parameters for the ``DeviceCallable`` constructor can also be provided. The
138
+ default is no epilog. Currently, callbacks are supported only with CUDA execution.""".replace("\n", " "),
139
+ #
140
+ "direction": """\
141
+ Specify whether forward or inverse FFT is performed (:class:`FFTDirection` object, or as a string from ['forward',
142
+ 'inverse'], "or as an int from [-1, 1] denoting forward and inverse directions respectively).""".replace("\n", " "),
143
+ #
144
+ "fft_key": """\
145
+ A tuple as the key to represent the input FFT problem.""".replace("\n", " "),
146
+ #
147
+ "function_signature": """\
148
+ operand,
149
+ /,
150
+ *,
151
+ axes: Sequence[int] | None = None,
152
+ options: FFTOptions | dict[str, Any] | None = None,
153
+ execution: ExecutionCPU | ExecutionCUDA | str | dict[str, Any] | None = None,
154
+ prolog: DeviceCallable | None = None,
155
+ epilog: DeviceCallable | None = None,
156
+ stream: AnyStream | None = None
157
+ """.replace("\n", " "),
158
+ }
159
+ )
160
+
161
+
162
+ def complex_to_real_equivalent(name):
163
+ assert "complex" in name, f"Internal Error ({name=})"
164
+ m = name.split("complex")
165
+ assert len(m) in (1, 2)
166
+ size = int(m[-1]) // 2
167
+ if len(m) == 1:
168
+ return f"float{size}"
169
+ else:
170
+ return f"{m[0]}float{size}"
171
+
172
+
173
+ def real_to_complex_equivalent(name):
174
+ assert "float" in name, f"Internal Error ({name=})"
175
+ m = name.split("float")
176
+ assert len(m) in (1, 2)
177
+ size = int(m[-1])
178
+ if len(m) == 1:
179
+ return f"complex{size * 2}"
180
+ else:
181
+ return f"{m[0]}complex{size * 2}"
182
+
183
+
184
+ def _get_default_fft_abstract_type(dtype, fft_type):
185
+ if fft_type is not None:
186
+ return fft_type
187
+
188
+ f, c = "float", "complex"
189
+ if dtype[: len(f)] == f:
190
+ fft_type = "R2C"
191
+ elif dtype[: len(c)] == c:
192
+ fft_type = "C2C"
193
+ else:
194
+ raise ValueError(f"Unsupported dtype '{dtype}' for FFT.")
195
+ return fft_type
196
+
197
+
198
+ def _get_fft_result_and_compute_types(dtype, fft_abstract_type):
199
+ """
200
+ Return result and compute data type given the input data type and the FFT type.
201
+ """
202
+ if fft_abstract_type == "C2C":
203
+ return dtype, dtype
204
+ elif fft_abstract_type == "C2R":
205
+ return complex_to_real_equivalent(dtype), dtype
206
+ elif fft_abstract_type == "R2C":
207
+ return real_to_complex_equivalent(dtype), dtype
208
+ else:
209
+ raise ValueError(f"Unsupported FFT Type: '{fft_abstract_type}'")
210
+
211
+
212
+ def _get_fft_default_direction(fft_abstract_type):
213
+ """
214
+ Return the default FFT direction (as object of type configuration.FFTDirection) based on
215
+ the FFT type.
216
+ """
217
+ if fft_abstract_type in ["C2C", "R2C"]:
218
+ return FFTDirection.FORWARD
219
+ elif fft_abstract_type == "C2R":
220
+ return FFTDirection.INVERSE
221
+ else:
222
+ raise ValueError(f"Unsupported FFT Type: '{fft_abstract_type}'")
223
+
224
+
225
+ def _get_size(shape):
226
+ return functools.reduce(operator.mul, shape, 1)
227
+
228
+
229
+ def _get_last_axis_id_and_size(
230
+ axes: Sequence[int],
231
+ operand_shape: Sequence[int],
232
+ fft_abstract_type: Literal["C2C", "C2R", "R2C"],
233
+ last_axis_parity: Literal["even", "odd"],
234
+ ) -> tuple[int, int]:
235
+ """
236
+ Args:
237
+ axes: The user-specified or default FFT axes.
238
+
239
+ operand_shape: The input operand shape.
240
+
241
+ fft_abstract_type: The "abstract" type of the FFT ('C2C', 'C2R', 'R2C').
242
+
243
+ last_axis_parity: For 'C2R' FFTs, specify whether the last axis size is even or odd.
244
+
245
+ Returns the last axis ID and the corresponding axis size required for the result.
246
+ """
247
+ last_axis_id = axes[-1]
248
+
249
+ if fft_abstract_type == "C2C":
250
+ return last_axis_id, operand_shape[last_axis_id]
251
+
252
+ if fft_abstract_type == "C2R":
253
+ if last_axis_parity == "even":
254
+ return last_axis_id, 2 * (operand_shape[last_axis_id] - 1)
255
+ elif last_axis_parity == "odd":
256
+ return last_axis_id, 2 * operand_shape[last_axis_id] - 1
257
+ else:
258
+ raise AssertionError("Unreachable.")
259
+
260
+ if fft_abstract_type == "R2C":
261
+ return last_axis_id, operand_shape[last_axis_id] // 2 + 1
262
+
263
+
264
+ def _has_only_small_factors(extent):
265
+ # fast track for powers of 2 (and zero)
266
+ if extent & (extent - 1) == 0:
267
+ return True
268
+ # Divide the `extent` by the product of all the prime factors up to
269
+ # 127 present in the `extent` until there are none left.
270
+ # Considering all the prime factors at once is faster, even though the
271
+ # first call (and only the first call) to gcd operates on ints with precision
272
+ # exceeding 64 bits. For common 2, 3, 5, 7 factors, the higher powers are included,
273
+ # to reduce number of iterations.
274
+ # math.prod(p for p in range(2, 128) if is_prime(p)) * 2**10 * 3**7 * 5**5 * 7**4
275
+ magic_prod = 67455891904760197438286248026720562156610454525830430432000000
276
+ d = math.gcd(extent, magic_prod)
277
+ while d > 1:
278
+ if extent == d:
279
+ return True
280
+ extent //= d
281
+ d = math.gcd(extent, d)
282
+ return False
283
+
284
+
285
+ def _is_lto_supported_shape(
286
+ plan_traits: PlanTraits,
287
+ fft_abstract_type: Literal["C2C", "C2R", "R2C"],
288
+ operand_dtype: str,
289
+ prolog: DeviceCallable | None,
290
+ epilog: DeviceCallable | None,
291
+ ) -> bool:
292
+ """Check if this plan hits a known cuFFT 12.3.x (CUDA 13.3) bug.
293
+
294
+ On the cuFFT shipped with CUDA 13.3, an LTO callback applied on the real side
295
+ of a real transform (R2C prolog / C2R epilog) can compute incorrect results
296
+ for lengths with a large prime factor. Resolved in CUDA 13.4; cuFFT <= 12.2
297
+ reports these as unsupported. This is an interim guard so affected plans
298
+ raise instead of returning incorrect results.
299
+ """
300
+ cufft_version = cufft.get_version()
301
+ # Affected: cuFFT 12.3.x only (CUDA 13.3). cufft.get_version() encodes
302
+ # major*1000 + minor*100 + patch (e.g. 12300).
303
+ if not 12300 <= cufft_version < 12400:
304
+ return True
305
+
306
+ # Only callbacks on the real side: R2C load (real input) / C2R store (real
307
+ # output). Complex-side callbacks (R2C store, C2R load) are unaffected.
308
+ real_side = (prolog is not None and fft_abstract_type == "R2C") or (epilog is not None and fft_abstract_type == "C2R")
309
+ if not real_side:
310
+ return True
311
+
312
+ # We need to check the extent at the last axis (axes[-1]). Since
313
+ # we require `axes[-1]` to have smallest stride, it will land
314
+ # as last of the ``ordered_fft_in_shape`` or ``ordered_fft_out_shape``.
315
+ real_len = plan_traits.ordered_fft_in_shape[-1] if fft_abstract_type == "R2C" else plan_traits.ordered_fft_out_shape[-1]
316
+
317
+ # nvmath only allows in-place for C2C, so every R2C/C2R plan is out-of-place.
318
+ # Only even lengths are affected; odd (and strided/unaligned) operands take a
319
+ # different, correct path. This guard is conservative: it may reject some
320
+ # configurations that are fine, but never lets an incorrect plan through.
321
+ if real_len % 2:
322
+ return True
323
+
324
+ # Lengths whose prime factors are all <= 127 use a different (correct) path
325
+ # and are unaffected (matches the cuFFT docs' factorization condition).
326
+ if _has_only_small_factors(real_len):
327
+ return True
328
+
329
+ # Only sufficiently large transforms are affected; smaller ones use a
330
+ # different, correct path. These size thresholds match the affected cuFFT.
331
+ cap = 8192 if operand_dtype in ("float32", "complex64") else 4096
332
+ return real_len < cap
333
+
334
+
335
+ def check_inplace_overlapping_layout(shape: Sequence[int], strides: Sequence[int]):
336
+ if is_overlapping_layout(shape, strides):
337
+ raise ValueError(
338
+ f"In-place transform is not supported because the tensor with shape "
339
+ f"{shape} and strides {strides} overlaps in memory."
340
+ )
341
+
342
+
343
+ def check_embedding_possible(strides, presorted=False):
344
+ """
345
+ Check if the strides allow for calculating an embedding dimension.
346
+ """
347
+ if not presorted:
348
+ strides = sorted(strides)
349
+ # with a broadcasted view, stride can be 0
350
+ if any(strides[i - 1] == 0 for i in range(1, len(strides))):
351
+ return False
352
+ return all(strides[i] % strides[i - 1] == 0 for i in range(1, len(strides)))
353
+
354
+
355
+ def check_batch_tileable(sorted_batch_shape, sorted_batch_strides):
356
+ """
357
+ Check if FFT layout is tileable across the specified batch layout.
358
+ """
359
+ return is_contiguous_layout(sorted_batch_shape, sorted_batch_strides)
360
+
361
+
362
+ def check_contiguous_layout(axes, strides, shape):
363
+ if not axes:
364
+ return True
365
+ sorted_batch_strides, sorted_batch_shape = zip(*sorted((strides[a], shape[a]) for a in axes), strict=True)
366
+ return is_contiguous_layout(sorted_batch_shape, sorted_batch_strides)
367
+
368
+
369
+ def calculate_embedding_shape(shape: Sequence[int], strides: Sequence[int]):
370
+ """
371
+ Calculate the embedding shape for the given shape and strides.
372
+ """
373
+ n = len(strides)
374
+ # The shape is used to resolve cases like (1, 2, 1) : (2, 1, 1) in CuTe notation.
375
+ ordered_strides, _, order = zip(*sorted(zip(strides, shape, range(n), strict=True)), strict=True)
376
+
377
+ ordered_shape = [ordered_strides[i] // ordered_strides[i - 1] for i in range(1, len(ordered_strides))] + [shape[order[-1]]]
378
+
379
+ embedding_shape = [0] * n
380
+ for o in range(n):
381
+ embedding_shape[order[o]] = ordered_shape[o]
382
+
383
+ return embedding_shape, order
384
+
385
+
386
+ def axis_order_in_memory(shape, strides):
387
+ """
388
+ Compute the order in which the axes appear in memory.
389
+ """
390
+ # The shape is used to resolve cases like (1, 2, 1) : (2, 1, 1) in CuTe notation.
391
+ _, _, axis_order = zip(*sorted(zip(strides, shape, range(len(strides)), strict=True)), strict=True)
392
+
393
+ return axis_order
394
+
395
+
396
+ def calculate_strides(shape, axis_order):
397
+ """
398
+ Calculate the strides for the provided shape and axis order.
399
+ """
400
+ strides = [None] * len(shape)
401
+
402
+ stride = 1
403
+ for axis in axis_order:
404
+ strides[axis] = stride
405
+ stride *= shape[axis]
406
+
407
+ return strides
408
+
409
+
410
+ def unsupported_layout_exception(operand_dim, axes, message, logger):
411
+ logger.error(message)
412
+
413
+ permutation = tuple(a for a in range(operand_dim) if a not in axes) + tuple(axes)
414
+ fft_dim = len(axes)
415
+ axes = tuple(range(operand_dim - fft_dim, operand_dim))
416
+
417
+ message = (
418
+ f"To convert to a supported layout, create a transposed view using transpose{permutation} and copy the "
419
+ f"view into a new tensor, using view.copy() for instance, and use axes={axes}."
420
+ )
421
+ logger.error(message)
422
+
423
+ raise UnsupportedLayoutError(message, permutation, axes)
424
+
425
+
426
+ def get_null_logger(name):
427
+ logger = logging.getLogger(name)
428
+ logger.addHandler(logging.NullHandler())
429
+ logger.propagate = False
430
+
431
+ return logger
432
+
433
+
434
+ def get_fft_plan_traits(
435
+ operand_shape: Sequence[int],
436
+ operand_strides: Sequence[int],
437
+ operand_dtype,
438
+ axes: Sequence[int],
439
+ execution: ExecutionCUDA | ExecutionCPU,
440
+ *,
441
+ fft_abstract_type: Literal["C2C", "C2R", "R2C"] = "C2C",
442
+ last_axis_parity: Literal["even", "odd"] = "even",
443
+ result_layout: Literal["optimized", "natural"] = "optimized",
444
+ logger: logging.Logger | None = None,
445
+ ) -> PlanTraits:
446
+ """
447
+ Extract the FFT shape from the operand shape, compute the ordered axes so that the data
448
+ is C-contiguous in memory, and compute the result shape and strides.
449
+
450
+ Args:
451
+ operand_shape: The operand shape
452
+
453
+ operand_strides: The operand strides
454
+
455
+ axes: The axes over which the FFT is performed. For R2C and C2R transforms, the size
456
+ of the last axis in `axes` will change.
457
+
458
+ execution: The execution options, an instance of either ExecutionCUDA or
459
+ ExecutionCPU class.
460
+
461
+ fft_abstract_type: The "abstract" type of the FFT ('C2C', 'C2R', 'R2C').
462
+
463
+ last_axis_parity: For 'C2R' FFTs, specify whether the last axis size is even or odd.
464
+
465
+ The data needed for creating a cuFFT plan is returned in the following order:
466
+ (result_shape, result_strides), ordered_axes, ordered_fft_in_shape,
467
+ ordered_fft_out_shape, (istride, idistance), (ostride, odistance)
468
+ """
469
+ logger = logger if logger is not None else get_null_logger("get_fft_plan_traits_null")
470
+
471
+ if len(axes) > 3:
472
+ raise ValueError(
473
+ "Only up to 3D FFTs are currently supported. You can use the 'axes' option to specify up to three axes "
474
+ f"along which to perform the FFT. The current number of dimensions is {len(axes)} corresponding to the "
475
+ f"axes {axes}."
476
+ )
477
+
478
+ # Check for duplicate axis IDs.
479
+ if len(axes) != len(set(axes)):
480
+ raise ValueError(f"The specified FFT axes = {axes} contains duplicate axis IDs, which is not supported.")
481
+
482
+ operand_dim = len(operand_shape)
483
+ batch_axes = [axis for axis in range(operand_dim) if axis not in axes]
484
+
485
+ # Check if an embedding is possible for the provided operand layout.
486
+ if not check_embedding_possible(operand_strides):
487
+ message = (
488
+ f"The operand layout corresponding to shape = {operand_shape} and strides = {operand_strides} is "
489
+ "not currently supported because it does not have a suitable embedding dimension."
490
+ )
491
+ unsupported_layout_exception(operand_dim, axes, message, logger)
492
+
493
+ # Compute the embedding shape for the operand.
494
+ operand_embedding_shape, axis_order = calculate_embedding_shape(operand_shape, operand_strides)
495
+ logger.debug(f"The operand embedding shape = {operand_embedding_shape}.")
496
+
497
+ # The first or the last *ordered* axis must be present in the specified axes to be able
498
+ # to use the "advanced" layout.
499
+ first, last = axis_order[-1], axis_order[0]
500
+ if first not in axes and last not in axes:
501
+ raise ValueError(
502
+ f"The first ({first}) or the last ({last}) tensor axis in stride order {axis_order} must be present in the "
503
+ f"specified FFT axes {axes}."
504
+ )
505
+
506
+ # Compute the embedding input shape for the FFT.
507
+ fft_in_embedding_shape = [operand_embedding_shape[a] for a in axes]
508
+
509
+ # Compute the input shape for the FFT.
510
+ fft_in_shape, fft_in_strides = zip(*[(operand_shape[a], operand_strides[a]) for a in axes], strict=True)
511
+ if not is_contiguous_in_memory(fft_in_embedding_shape, fft_in_strides):
512
+ message = (
513
+ f"The FFT axes {axes} cannot be reordered so that the data is contiguous in memory for "
514
+ f"operand shape = {operand_shape} and operand strides = {operand_strides}."
515
+ )
516
+ unsupported_layout_exception(operand_dim, axes, message, logger)
517
+
518
+ # Reorder the FFT axes and input shape so that they are contiguous or separated by
519
+ # constant stride in memory.
520
+ quadruple = sorted(
521
+ zip(fft_in_strides, fft_in_shape, fft_in_embedding_shape, axes, strict=True), key=lambda v: v[:2], reverse=True
522
+ )
523
+
524
+ ordered_in_strides, ordered_fft_in_shape, ordered_fft_in_embedding_shape, ordered_axes = zip(*quadruple, strict=True)
525
+
526
+ # Check if R2C and C2R can be supported without copying.
527
+ if fft_abstract_type in ["R2C", "C2R"] and ordered_axes[-1] != axes[-1]:
528
+ message = (
529
+ f"The last FFT axis specified ({axes[-1]}) must have the smallest stride of all the FFT axes' "
530
+ f"strides {fft_in_strides} for FFT type '{fft_abstract_type}'."
531
+ )
532
+ unsupported_layout_exception(operand_dim, axes, message, logger)
533
+
534
+ # Input FFT size and batch size.
535
+ fft_in_size = _get_size(fft_in_shape)
536
+ if fft_in_size == 0:
537
+ raise ValueError("Invalid number of FFT data points (0) specified.")
538
+ fft_batch_size = _get_size(operand_shape) // fft_in_size
539
+
540
+ # Output FFT (ordered) shape and size.
541
+ last_axis_id, last_axis_size = _get_last_axis_id_and_size(axes, operand_shape, fft_abstract_type, last_axis_parity)
542
+ if last_axis_size == 0:
543
+ raise ValueError(
544
+ f"The size of the last FFT axis in the result for FFT type '{fft_abstract_type}' is 0 for operand shape = "
545
+ f"{operand_shape} and axes = {axes}. To fix this, provide 'last_axis_parity' = 'odd' to the FFT options."
546
+ )
547
+ ordered_fft_out_shape = list(ordered_fft_in_shape)
548
+ index = ordered_axes.index(last_axis_id)
549
+ ordered_fft_out_shape[index] = last_axis_size
550
+ fft_out_size = _get_size(ordered_fft_out_shape)
551
+
552
+ # Check that batch dimensions are tileable, as required by the "advanced" layout.
553
+ sorted_batch_shape: Sequence[int] = []
554
+ sorted_batch_strides: Sequence[int] = []
555
+ if batch_axes:
556
+ sorted_batch_strides, sorted_batch_shape = zip(
557
+ *sorted((operand_strides[a], operand_shape[a]) for a in batch_axes), strict=True
558
+ )
559
+ if not check_embedding_possible(sorted_batch_strides, presorted=True):
560
+ raise ValueError(
561
+ f"The operand layout corresponding to shape = {operand_shape} and strides = {operand_strides} "
562
+ f"together with the specified axes = {axes} is currently not supported because it is not tileable."
563
+ )
564
+ logger.debug(f"The sorted batch shape is {sorted_batch_shape}.")
565
+ logger.debug(f"The sorted batch strides are {sorted_batch_strides}.")
566
+ if not check_batch_tileable(sorted_batch_shape, sorted_batch_strides):
567
+ message = (
568
+ f"The operand layout corresponding to shape = {operand_shape} and strides = {operand_strides} "
569
+ f"together with the specified axes = {axes} is currently not supported because it is not tileable."
570
+ )
571
+ unsupported_layout_exception(operand_dim, axes, message, logger)
572
+ logger.debug(
573
+ f"The operand layout corresponding to shape = {operand_shape} and strides = {operand_strides} together with "
574
+ f"the specified axes = {axes} IS tileable."
575
+ )
576
+
577
+ # The result tensor has updated shape for R2C and C2R transforms.
578
+ result_shape = list(operand_shape)
579
+ result_shape[last_axis_id] = last_axis_size
580
+
581
+ # The result tensor layout is either natural or chosen for optimal cuFFT performance,
582
+ # based on the operand layout and user-provided option.
583
+
584
+ # We can keep the input's layout (i.e. operand's extents order of increasing strides)
585
+ # without performance hit, if the samples do not interleave.
586
+ # Otherwise, we try to keep it only when explicitly asked (result_layout=natural)
587
+ is_sample_interleaved = bool(sorted_batch_strides and sorted_batch_strides[0] <= ordered_in_strides[0])
588
+ logger.debug(f"Are the samples interleaved? {is_sample_interleaved}.")
589
+
590
+ use_optimized_result_layout = is_sample_interleaved and result_layout != "natural"
591
+ if not use_optimized_result_layout: # Natural (== operand) layout.
592
+ axis_order = axis_order_in_memory(operand_shape, operand_strides)
593
+ result_strides = calculate_strides(result_shape, axis_order)
594
+ # If the resulting output operand is not tilable, keeping the original layout is not
595
+ # possible. If `not is_sample_interleaved` the batch must be tilable, because the
596
+ # min batch stride is bigger than max fft stride
597
+ if is_sample_interleaved:
598
+ if not check_contiguous_layout(batch_axes, result_strides, result_shape):
599
+ message = (
600
+ f"The operand layout corresponding to shape = {operand_shape} and strides = {operand_strides} "
601
+ f"together with the specified axes = {axes} is currently not supported with "
602
+ "result_layout='natural', because the output batch would not be tileable."
603
+ )
604
+ unsupported_layout_exception(operand_dim, axes, message, logger)
605
+ if not check_contiguous_layout(axes, result_strides, result_shape):
606
+ message = (
607
+ f"The operand layout corresponding to shape = {operand_shape} and strides = {operand_strides} "
608
+ f"together with the specified axes = {axes} is currently not supported with "
609
+ "result_layout='natural', because the output sample would be non-contiguous."
610
+ )
611
+ unsupported_layout_exception(operand_dim, axes, message, logger)
612
+ else: # Optimized layout.
613
+ axis_order = tuple(
614
+ list(reversed(ordered_axes)) + sorted((a for a in batch_axes), key=lambda v: (operand_strides[v], operand_shape[v]))
615
+ )
616
+ result_strides = calculate_strides(result_shape, axis_order)
617
+ logger.debug(f"The result layout is '{result_layout}' with the result_strides {result_strides}.")
618
+
619
+ # Compute the operand linear stride and distance needed for the cuFFT plan.
620
+ last_ordered_in_stride = ordered_in_strides[-1]
621
+ min_in_stride = min(operand_strides)
622
+
623
+ if last_ordered_in_stride == min_in_stride:
624
+ istride, idistance = (
625
+ min_in_stride,
626
+ _get_size(ordered_fft_in_embedding_shape) if not sorted_batch_strides else sorted_batch_strides[0],
627
+ )
628
+ else:
629
+ istride, idistance = (
630
+ last_ordered_in_stride,
631
+ min_in_stride if not sorted_batch_strides else sorted_batch_strides[0],
632
+ )
633
+
634
+ # Compute the result linear stride and distance needed for the cuFFT plan.
635
+ ostride = result_strides[ordered_axes[-1]] # minimal output fft stride
636
+ odistance = fft_out_size if not batch_axes else min(result_strides[axis] for axis in batch_axes)
637
+
638
+ if execution.name == "cpu":
639
+ if fft_out_size == 1:
640
+ istride = ostride = 1
641
+ else:
642
+ assert execution.name == "cuda"
643
+ if operand_dtype in ("float16", "complex32"):
644
+ if fft_abstract_type == "R2C" and istride != 1:
645
+ raise ValueError(
646
+ f"The {fft_abstract_type} FFT of half-precision tensor ({operand_dtype}) "
647
+ f"is currently not supported for strided inputs "
648
+ f"(got input stride {istride})."
649
+ )
650
+ if fft_abstract_type == "C2R" and ostride != 1:
651
+ raise ValueError(
652
+ f"The {fft_abstract_type} FFT of half-precision tensor ({operand_dtype}) "
653
+ f"is currently not supported for strided outputs "
654
+ f"(got output stride {ostride})."
655
+ )
656
+ if fft_out_size == 1:
657
+ if cufft.get_version() < 10702: # 10702 is shipped with CTK 11.7
658
+ raise ValueError(
659
+ f"The FFT of sample size 1 and half-precision type ({operand_dtype}) "
660
+ f"of size 1 is not supported by the installed cuFFT version. "
661
+ )
662
+ # There is a bug that leads to invalid memory access (CTK 12.1) for
663
+ # one-element, strided C2C complex32 tensors (either in the input or output)
664
+ # or results in CUFFT_INVALID_SIZE (CTK 12.3). This workaround relies on the
665
+ # fact that the [i|o]stride effectively does not matter in a one-element
666
+ # sample.
667
+ elif fft_abstract_type == "C2C":
668
+ istride = ostride = 1
669
+
670
+ # There's a bug in cuFFT in CTKs prior to 11.4U2
671
+ if len(axes) == 3 and fft_batch_size > 1 and cufft.get_version() < 10502:
672
+ raise ValueError(
673
+ "The 3D batched FFT is not supported by the installed cuFFT version. "
674
+ "Please update your CUDA Toolkit (to 11.4.2 or newer)"
675
+ )
676
+
677
+ plan_traits = PlanTraits(
678
+ result_shape=tuple(result_shape),
679
+ result_strides=tuple(result_strides),
680
+ optimized_result_layout=use_optimized_result_layout,
681
+ ordered_axes=tuple(ordered_axes),
682
+ ordered_fft_in_shape=tuple(ordered_fft_in_shape),
683
+ ordered_fft_in_embedding_shape=tuple(ordered_fft_in_embedding_shape),
684
+ ordered_fft_out_shape=tuple(ordered_fft_out_shape),
685
+ fft_batch_size=fft_batch_size,
686
+ istride=istride,
687
+ idistance=idistance,
688
+ ostride=ostride,
689
+ odistance=odistance,
690
+ )
691
+ return plan_traits
692
+
693
+
694
+ def _allocate_operand_or_identity(
695
+ user_operand: utils.TensorHolder,
696
+ stream_holder,
697
+ execution_space,
698
+ memory_space,
699
+ device_id: int | Literal["cpu"],
700
+ fft_abstract_type,
701
+ logger,
702
+ ):
703
+ """
704
+ Allocate the internal operand for the given execution/memory space, or
705
+ return the user operand as-is (identity) when
706
+ no internal buffer/mirror is needed.
707
+
708
+ Used during construction and after release_operand() when the internal
709
+ buffer needs to be created from scratch.
710
+ """
711
+ if execution_space == memory_space:
712
+ if fft_abstract_type != "C2R":
713
+ return user_operand, None
714
+ else:
715
+ # For C2R, we need to take a copy to avoid input being overwritten
716
+ logger.info("For C2R FFT with input operand on GPU, the input is copied to avoid being overwritten by cuFFT.")
717
+ internal_operand = utils.create_empty_tensor(
718
+ user_operand.__class__,
719
+ user_operand.shape,
720
+ user_operand.dtype,
721
+ device_id,
722
+ stream_holder,
723
+ verify_strides=True,
724
+ strides=user_operand.strides,
725
+ )
726
+ internal_operand.copy_(user_operand, stream_holder=stream_holder)
727
+ # We don't need to keep the operand backup, because C2R precludes `inplace=True`
728
+ return internal_operand, None
729
+ else:
730
+ # Copy the `operand` to memory that matches the exec space
731
+ # and keep the original `operand` to handle `options.inplace=True`
732
+ if execution_space == "cuda":
733
+ assert isinstance(device_id, int)
734
+ to_device: int | Literal["cpu"] = device_id
735
+ else:
736
+ assert execution_space == "cpu"
737
+ to_device = "cpu"
738
+ exec_space_copy = user_operand.to(to_device, stream_holder)
739
+ return exec_space_copy, user_operand
740
+
741
+
742
+ def _copy_operand_or_identity(
743
+ internal_operand: utils.TensorHolder,
744
+ new_operand: utils.TensorHolder,
745
+ stream_holder,
746
+ execution_space,
747
+ memory_space,
748
+ fft_abstract_type,
749
+ logger,
750
+ ):
751
+ """
752
+ Copy new operand data into an existing internal buffer, or return the
753
+ new operand as-is (identity) when no internal buffer is needed.
754
+
755
+ Used by reset_operand() when the operand has NOT been released, so the
756
+ internal buffer is still valid and can be reused via in-place copy.
757
+ """
758
+ if execution_space == memory_space:
759
+ if fft_abstract_type != "C2R":
760
+ return new_operand, None
761
+ else:
762
+ logger.info("For C2R FFT with input operand on GPU, the input is copied to avoid being overwritten by cuFFT.")
763
+ internal_operand.copy_(new_operand, stream_holder=stream_holder)
764
+ return internal_operand, None
765
+ else:
766
+ internal_operand.copy_(new_operand, stream_holder=stream_holder)
767
+ return internal_operand, new_operand
768
+
769
+
770
+ def create_xt_plan_args(*, plan_traits=None, fft_abstract_type=None, operand_data_type=None, inplace=None):
771
+ """
772
+ Create the arguments to xt_make_plan_many() except for the handle. This is also used for
773
+ computing the FFT key.
774
+ """
775
+ assert plan_traits is not None, "Internal error."
776
+ assert fft_abstract_type is not None, "Internal error."
777
+ assert operand_data_type is not None, "Internal error."
778
+ assert inplace is not None, "Internal error."
779
+
780
+ result_data_type, compute_data_type = _get_fft_result_and_compute_types(operand_data_type, fft_abstract_type)
781
+
782
+ # The input shape to the plan should be the logical FFT shape.
783
+ ordered_plan_shape = plan_traits.ordered_fft_out_shape if fft_abstract_type == "C2R" else plan_traits.ordered_fft_in_shape
784
+
785
+ # Handle in-place transforms.
786
+ if inplace:
787
+ ordered_fft_out_shape, ostride, odistance = (
788
+ plan_traits.ordered_fft_in_embedding_shape,
789
+ plan_traits.istride,
790
+ plan_traits.idistance,
791
+ )
792
+ else:
793
+ ordered_fft_out_shape, ostride, odistance = (
794
+ plan_traits.ordered_fft_out_shape,
795
+ plan_traits.ostride,
796
+ plan_traits.odistance,
797
+ )
798
+
799
+ return (
800
+ len(ordered_plan_shape),
801
+ ordered_plan_shape,
802
+ plan_traits.ordered_fft_in_embedding_shape,
803
+ plan_traits.istride,
804
+ plan_traits.idistance,
805
+ NAME_TO_DATA_TYPE[operand_data_type],
806
+ ordered_fft_out_shape,
807
+ ostride,
808
+ odistance,
809
+ NAME_TO_DATA_TYPE[result_data_type],
810
+ plan_traits.fft_batch_size,
811
+ NAME_TO_DATA_TYPE[compute_data_type],
812
+ )
813
+
814
+
815
+ def fftw_plan_args(xt_plan_args, operand_ptr, result_ptr, fft_abstract_type, direction):
816
+ """
817
+ Create the arguments for fftw API based on the args created by create_xt_plan_args and
818
+ pointers to the input and the output tensors. Note, that while the pointers to the data
819
+ are required in planning, different pointers may be passed to the same plan in
820
+ subsequent execute call (assuming dtype, memory layout, alignment, and inplace
821
+ properties do not change).
822
+ """
823
+ (
824
+ rank,
825
+ n,
826
+ inembed,
827
+ istride,
828
+ idist,
829
+ input_t,
830
+ onembed,
831
+ ostride,
832
+ odist,
833
+ output_t,
834
+ batch,
835
+ execution_t,
836
+ ) = xt_plan_args
837
+
838
+ if input_t in FFTW_SUPPORTED_SINGLE:
839
+ assert output_t in FFTW_SUPPORTED_SINGLE, "Expected single precision output for single precision input"
840
+ precision = fftw.Precision.FLOAT
841
+ elif input_t in FFTW_SUPPORTED_DOUBLE:
842
+ assert output_t in FFTW_SUPPORTED_DOUBLE, "Expected double precision output for double precision input"
843
+ precision = fftw.Precision.DOUBLE
844
+ else:
845
+ supported_types_str = ", ".join(DATA_TYPE_TO_NAME[dtype] for dtype in FFTW_SUPPORTED_TYPES)
846
+ raise ValueError(
847
+ f"Currently, the CPU FFT supports following input types: {supported_types_str}. "
848
+ f"Got input of type {DATA_TYPE_TO_NAME[input_t]}."
849
+ )
850
+ kind = getattr(fftw.Kind, fft_abstract_type)
851
+ if kind == fftw.Kind.C2C:
852
+ supported_in_t, supported_out_t = FFTW_SUPPORTED_COMPLEX, FFTW_SUPPORTED_COMPLEX
853
+ elif kind == fftw.Kind.C2R:
854
+ supported_in_t, supported_out_t = FFTW_SUPPORTED_COMPLEX, FFTW_SUPPORTED_FLOAT
855
+ else:
856
+ assert kind == fftw.Kind.R2C
857
+ supported_in_t, supported_out_t = FFTW_SUPPORTED_FLOAT, FFTW_SUPPORTED_COMPLEX
858
+ if input_t not in supported_in_t:
859
+ supported_types_str = ", ".join(DATA_TYPE_TO_NAME[dtype] for dtype in supported_in_t)
860
+ raise ValueError(
861
+ f"Got unsupported input data type {DATA_TYPE_TO_NAME[input_t]} "
862
+ f"for the {fft_abstract_type} transform. "
863
+ f"Supported types are {supported_types_str}."
864
+ )
865
+ assert output_t in supported_out_t, "Mismatched data type and FFT transform type"
866
+ if direction is None:
867
+ sign = fftw.Sign.UNSPECIFIED
868
+ else:
869
+ sign = fftw.Sign(direction)
870
+ return (
871
+ precision,
872
+ kind,
873
+ sign,
874
+ rank,
875
+ n,
876
+ batch,
877
+ operand_ptr,
878
+ inembed,
879
+ istride,
880
+ idist,
881
+ result_ptr,
882
+ onembed,
883
+ ostride,
884
+ odist,
885
+ fftw.PlannerFlags.ESTIMATE,
886
+ )
887
+
888
+
889
+ def setup_options_and_execution(
890
+ memory_space: Literal["cpu", "cuda"] | None, options, execution
891
+ ) -> tuple[FFTOptions, ExecutionCUDA | ExecutionCPU]:
892
+ # The operand's memory space is used as the default execution space when 'execution'
893
+ # is not specified. It may be None only when 'execution' is provided explicitly.
894
+ execution = utils.check_or_create_one_of_options(
895
+ (ExecutionCUDA, ExecutionCPU),
896
+ execution,
897
+ "'execution' options",
898
+ default_name=memory_space,
899
+ )
900
+ if execution.name == "cpu":
901
+ _check_init_fftw()
902
+ if execution.num_threads is None:
903
+ execution = replace(execution, num_threads=_get_num_threads_default())
904
+ if not isinstance(execution.num_threads, int) or execution.num_threads <= 0:
905
+ raise ValueError("The 'num_threads' must be a positive integer")
906
+ else:
907
+ assert execution.name == "cuda"
908
+ _check_init_cufft()
909
+ options = utils.check_or_create_options(FFTOptions, options, "FFT options")
910
+ return options, execution
911
+
912
+
913
+ def _compute_fft_plan_args_and_traits(
914
+ shape: Sequence[int],
915
+ strides: Sequence[int],
916
+ dtype: str,
917
+ *,
918
+ axes: Sequence[int] | None = None,
919
+ options: FFTOptions,
920
+ execution: ExecutionCPU | ExecutionCUDA,
921
+ inplace: bool | None = None,
922
+ memory_space: Literal["cpu", "cuda"] | None = None,
923
+ ) -> tuple[tuple, PlanTraits]:
924
+ """
925
+ (private method) Compute plan_args and plan_traits from operand metadata.
926
+
927
+ Args:
928
+ shape: The operand shape (in number of elements per dimension).
929
+ strides: The operand strides (in number of elements per dimension).
930
+ dtype: The operand data type name (e.g. ``"complex64"``).
931
+ axes: The axes along which to perform the FFT.
932
+ options: Normalized FFTOptions object.
933
+ execution: ExecutionCPU or ExecutionCUDA object.
934
+ inplace: Whether the operation is in-place. If None, computed from
935
+ options.inplace with cross-device override (which requires
936
+ ``memory_space``).
937
+ memory_space: Where the operand resides ("cpu" or "cuda"). Only consulted
938
+ when ``inplace`` is None, to apply the cross-device override.
939
+
940
+ Returns:
941
+ tuple: (plan_args, plan_traits) containing the plan arguments and traits.
942
+ """
943
+ fft_abstract_type = _get_default_fft_abstract_type(dtype, options.fft_type)
944
+ if axes is None:
945
+ axes = range(len(shape))
946
+ else:
947
+ operand_dim = len(shape)
948
+ # Mirror FFT.__init__: enforce bounds and support negative indices.
949
+ if any(axis >= operand_dim or axis < -operand_dim for axis in axes):
950
+ raise ValueError(f"The specified FFT axes {tuple(axes)} are out of bounds for a {operand_dim}-D tensor.")
951
+ axes = tuple(axis % operand_dim for axis in axes)
952
+
953
+ # Determine plan traits.
954
+ plan_traits = get_fft_plan_traits(
955
+ shape,
956
+ strides,
957
+ dtype,
958
+ axes,
959
+ execution,
960
+ fft_abstract_type=fft_abstract_type,
961
+ last_axis_parity=options.last_axis_parity,
962
+ result_layout=options.result_layout,
963
+ logger=None,
964
+ )
965
+
966
+ # If inplace is not explicitly provided, use options.inplace.
967
+ # For cross-device, inplace is always True (the operand needs to be copied once anyway).
968
+ if inplace is None:
969
+ assert memory_space is not None, "Internal error: memory_space is required when inplace is None."
970
+ execution_space = execution.name
971
+ assert execution.name in ("cpu", "cuda")
972
+ inplace = memory_space != execution_space or options.inplace
973
+
974
+ if inplace:
975
+ check_inplace_overlapping_layout(shape, strides)
976
+
977
+ # Get the arguments to xt_make_plan_many.
978
+ plan_args = create_xt_plan_args(
979
+ plan_traits=plan_traits,
980
+ fft_abstract_type=fft_abstract_type,
981
+ operand_data_type=dtype,
982
+ inplace=inplace,
983
+ )
984
+
985
+ return plan_args, plan_traits
986
+
987
+
988
+ @utils.docstring_decorator(SHARED_FFT_DOCUMENTATION, skip_missing=True)
989
+ def create_fft_key(
990
+ plan_args: tuple,
991
+ execution: ExecutionCPU | ExecutionCUDA,
992
+ *,
993
+ prolog: DeviceCallable | None = None,
994
+ epilog: DeviceCallable | None = None,
995
+ ) -> tuple[tuple, tuple | None, tuple]:
996
+ """
997
+ Create an FFT key from plan_args and execution options.
998
+
999
+ This key is not designed to be serialized and used on a different machine. It is meant
1000
+ for runtime use only.
1001
+
1002
+ It is the user's responsibility to augment this key with the stream in case they use
1003
+ stream-ordered memory pools.
1004
+
1005
+ Args:
1006
+ plan_args: The plan arguments from create_xt_plan_args.
1007
+
1008
+ execution: {execution}
1009
+
1010
+ prolog: {prolog}
1011
+
1012
+ .. experimental:: parameter
1013
+
1014
+ epilog: {epilog}
1015
+
1016
+ .. experimental:: parameter
1017
+
1018
+
1019
+ Returns:
1020
+ tuple: (plan_args, callable_data, execution_tuple) representing the FFT key.
1021
+ """
1022
+ # Prolog and epilog, if used.
1023
+ if prolog is not None or epilog is not None:
1024
+ prolog = utils.check_or_create_options(DeviceCallable, prolog, "prolog", keep_none=True)
1025
+ epilog = utils.check_or_create_options(DeviceCallable, epilog, "epilog", keep_none=True)
1026
+
1027
+ def get_data(device_callable):
1028
+ return None if device_callable is None else (device_callable.ltoir, device_callable.data)
1029
+
1030
+ callable_data = get_data(prolog), get_data(epilog)
1031
+ else:
1032
+ callable_data = None
1033
+
1034
+ # The key is based on plan arguments, callback data (a callable object of type
1035
+ # DeviceCallback or None) and the execution options (in "normalized" form of
1036
+ # ("cpu"/"cuda", *execution_options)). `name` is a ClassVar on the templates, so
1037
+ # `astuple()` excludes it; prepend it explicitly to preserve the contract.
1038
+ return plan_args, callable_data, (execution.name, *data_cls_astuple(execution)) # type: ignore[arg-type]
1039
+
1040
+
1041
+ _CUDA_TYPES_TO_CUFFT_TYPE = {
1042
+ (cudaDataType.CUDA_C_32F, cudaDataType.CUDA_C_32F): cufft.Type.C2C,
1043
+ (cudaDataType.CUDA_R_32F, cudaDataType.CUDA_C_32F): cufft.Type.R2C,
1044
+ (cudaDataType.CUDA_C_32F, cudaDataType.CUDA_R_32F): cufft.Type.C2R,
1045
+ (cudaDataType.CUDA_C_64F, cudaDataType.CUDA_C_64F): cufft.Type.Z2Z,
1046
+ (cudaDataType.CUDA_R_64F, cudaDataType.CUDA_C_64F): cufft.Type.D2Z,
1047
+ (cudaDataType.CUDA_C_64F, cudaDataType.CUDA_R_64F): cufft.Type.Z2D,
1048
+ }
1049
+
1050
+
1051
+ def estimate_workspace_size(
1052
+ key: tuple[tuple, tuple | None, tuple],
1053
+ *,
1054
+ technique: str = "default",
1055
+ handle: int | None = None,
1056
+ ) -> int:
1057
+ """
1058
+ Estimate the workspace size in bytes for the given FFT key.
1059
+
1060
+ Args:
1061
+ key: The FFT key returned by :meth:`FFT.create_key` or
1062
+ :meth:`FFT.create_key_from_metadata`.
1063
+
1064
+ technique: The estimation technique to use.
1065
+
1066
+ - ``"default"``: Uses ``cufftEstimateMany``, which returns
1067
+ the workspace size for default plan settings without
1068
+ requiring a handle. Does not support half or bfloat16
1069
+ precision keys.
1070
+ - ``"refined"``: Uses ``cufftXtGetSizeMany``, which requires
1071
+ a ``handle`` and returns the workspace size accounting for
1072
+ any settings in the handle (e.g., plan properties).
1073
+ With a freshly-created handle, this returns the
1074
+ same value as ``"default"``.
1075
+
1076
+ handle: A cuFFT handle as returned by
1077
+ ``nvmath.bindings.cufft.create()``. Required when
1078
+ ``technique="refined"``; ignored otherwise. The caller is
1079
+ responsible to ensure the handle was created on the same
1080
+ CUDA device that the key targets.
1081
+
1082
+ Returns:
1083
+ int: The estimated workspace size in bytes.
1084
+
1085
+ Semantics:
1086
+ - This function does not create a full plan. The callback data
1087
+ (prolog/epilog) in the key is ignored, as the underlying
1088
+ cuFFT size estimation APIs do not account for callbacks.
1089
+
1090
+ - The returned value is 0 in two cases: (1) the key specifies
1091
+ CPU execution, since workspace management is not applicable to
1092
+ the CPU backend, or (2) the key specifies CUDA execution but
1093
+ no temporary workspace is needed for the given FFT configuration.
1094
+
1095
+ .. tip::
1096
+ If an allocated operand is not available but the problem's metadata is known,
1097
+ the key can be built from it using :meth:`FFT.create_key_from_metadata`
1098
+ (see its documentation for the accepted parameters).
1099
+
1100
+ .. seealso::
1101
+ :meth:`FFT.create_key`, :meth:`FFT.create_key_from_metadata`
1102
+ """
1103
+ if technique not in ("default", "refined"):
1104
+ raise ValueError(f"technique must be 'default' or 'refined', got {technique!r}.")
1105
+
1106
+ if not isinstance(key, tuple) or len(key) != 3:
1107
+ raise ValueError(f"The key must be a 3-tuple as returned by FFT.create_key(). Got {key}.")
1108
+
1109
+ plan_args, _, execution_tuple = key
1110
+ if not isinstance(plan_args, tuple) or len(plan_args) != 12:
1111
+ raise ValueError("The first element of the key (plan_args) must be a 12-element tuple as returned by FFT.create_key().")
1112
+ if not isinstance(execution_tuple, tuple) or len(execution_tuple) < 1:
1113
+ raise ValueError("The third element of the key (execution) must be a non-empty tuple as returned by FFT.create_key().")
1114
+
1115
+ assert execution_tuple[0] in ("cpu", "cuda"), (
1116
+ f"Internal error: expected execution_tuple[0] to be 'cuda' or 'cpu', got {execution_tuple[0]!r}."
1117
+ )
1118
+ if execution_tuple[0] == "cpu":
1119
+ return 0
1120
+
1121
+ if technique == "refined":
1122
+ if handle is None:
1123
+ raise ValueError("A cuFFT handle is required when technique='refined'.")
1124
+ return cufft.xt_get_size_many(handle, *plan_args)
1125
+
1126
+ # technique == "default"
1127
+ inputtype = plan_args[5]
1128
+ outputtype = plan_args[9]
1129
+ cufft_type = _CUDA_TYPES_TO_CUFFT_TYPE.get((inputtype, outputtype))
1130
+ if cufft_type is None:
1131
+ raise ValueError(
1132
+ f"technique='default' (cufftEstimateMany) does not support "
1133
+ f"the data types in this key (inputtype={inputtype}, "
1134
+ f"outputtype={outputtype}). Use technique='refined' instead."
1135
+ )
1136
+ return cufft.estimate_many(
1137
+ plan_args[0], # rank
1138
+ plan_args[1], # n
1139
+ plan_args[2], # inembed
1140
+ plan_args[3], # istride
1141
+ plan_args[4], # idist
1142
+ plan_args[6], # onembed
1143
+ plan_args[7], # ostride
1144
+ plan_args[8], # odist
1145
+ cufft_type,
1146
+ plan_args[10], # batch
1147
+ )
1148
+
1149
+
1150
+ def set_prolog_and_epilog(handle, prolog, epilog, operand_dtype, result_dtype, logger):
1151
+ def set_callback(cbkind, cbobj, dtype):
1152
+ if cbobj is None:
1153
+ return
1154
+
1155
+ assert cbkind in ["prolog", "epilog"], "Internal error."
1156
+ CBType = CBLoadType if cbkind == "prolog" else CBStoreType
1157
+
1158
+ try:
1159
+ cufft.xt_set_jit_callback(handle, 0, cbobj.ltoir, cbobj.size, CBType[dtype.upper()], [cbobj.data])
1160
+ except _bindings_utils.FunctionNotFoundError as e:
1161
+ version = cufft.get_version()
1162
+ raise RuntimeError(
1163
+ f"The currently running cuFFT version {version} does not support LTO callbacks. \n"
1164
+ f"cuFFT LTO callbacks are supported starting with cuFFT 11.3, "
1165
+ f"shipped with CUDA Toolkit 12.6U2 (11.3.0) or newer. \n"
1166
+ ) from e
1167
+
1168
+ logger.info(f"The specified LTO-IR {cbkind} has been set.")
1169
+ if isinstance(cbobj.ltoir, int):
1170
+ logger.debug(f"The {cbkind} LTO-IR pointer is {cbobj.ltoir}.")
1171
+ logger.debug(f"The {cbkind} LTO-IR size is {cbobj.size}, and data is {cbobj.data}.")
1172
+
1173
+ if prolog is not None:
1174
+ set_callback("prolog", prolog, operand_dtype)
1175
+ if epilog is not None:
1176
+ set_callback("epilog", epilog, result_dtype)
1177
+
1178
+
1179
+ class InvalidFFTState(Exception):
1180
+ pass
1181
+
1182
+
1183
+ @utils.docstring_decorator(SHARED_FFT_DOCUMENTATION, skip_missing=False)
1184
+ class FFT:
1185
+ """
1186
+ Create a stateful object that encapsulates the specified FFT computations and required
1187
+ resources. This object ensures the validity of resources during use and releases them
1188
+ when they are no longer needed to prevent misuse.
1189
+
1190
+ This object encompasses all functionalities of function-form APIs :func:`fft`,
1191
+ :func:`ifft`, :func:`rfft`, and :func:`irfft`, which are convenience wrappers around it.
1192
+ The stateful object also allows for the amortization of preparatory costs when the same
1193
+ FFT operation is to be performed on multiple operands with the same problem
1194
+ specification (see :meth:`reset_operand`, :meth:`reset_operand_unchecked`,
1195
+ and :meth:`create_key` for more details).
1196
+
1197
+ Using the stateful object typically involves the following steps:
1198
+
1199
+ 1. **Problem Specification**: Initialize the object with a defined operation and
1200
+ options.
1201
+ 2. **Preparation**: Use :meth:`plan` to determine the best algorithmic implementation
1202
+ for this specific FFT operation.
1203
+ 3. **Execution**: Perform the FFT computation with :meth:`execute`, which can be either
1204
+ forward or inverse FFT transformation.
1205
+ 4. **Resource Management**: Ensure all resources are released either by explicitly
1206
+ calling :meth:`free` or by managing the stateful object within a context manager.
1207
+
1208
+ Detailed information on each step described above can be obtained by passing in a
1209
+ :class:`logging.Logger` object to :class:`FFTOptions` or by setting the appropriate
1210
+ options in the root logger object, which is used by default:
1211
+
1212
+ >>> import logging
1213
+ >>> logging.basicConfig(
1214
+ ... level=logging.INFO,
1215
+ ... format="%(asctime)s %(levelname)-8s %(message)s",
1216
+ ... datefmt="%m-%d %H:%M:%S",
1217
+ ... )
1218
+
1219
+ .. versionchanged:: 0.9.0
1220
+ The `operand` parameter is now positional-only.
1221
+
1222
+ Args:
1223
+ operand: {operand}
1224
+
1225
+ axes: {axes}
1226
+
1227
+ options: {options}
1228
+
1229
+ execution: {execution}
1230
+
1231
+ stream: {stream}
1232
+
1233
+ .. seealso::
1234
+ :meth:`plan`, :meth:`reset_operand`, :meth:`reset_operand_unchecked`,
1235
+ :meth:`release_operand`, :meth:`execute`, :meth:`create_key`
1236
+
1237
+ Examples:
1238
+
1239
+ >>> import cupy as cp
1240
+ >>> import nvmath
1241
+
1242
+ Create a 3-D complex128 ndarray on the GPU:
1243
+
1244
+ >>> shape = 128, 128, 128
1245
+ >>> a = cp.random.rand(*shape) + 1j * cp.random.rand(*shape)
1246
+
1247
+ We will define a 2-D C2C FFT operation along the first two dimensions, batched along
1248
+ the last dimension:
1249
+
1250
+ >>> axes = 0, 1
1251
+
1252
+ Create an FFT object encapsulating the problem specification above:
1253
+
1254
+ >>> f = nvmath.fft.FFT(a, axes=axes)
1255
+
1256
+ Options can be provided above to control the behavior of the operation using the
1257
+ `options` argument (see :class:`FFTOptions`). Similarly, the execution space (CUDA
1258
+ or CPU) and execution options can be passed using the `execution` argument (see
1259
+ :class:`ExecutionCUDA`, :class:`ExecutionCPU`).
1260
+
1261
+ Next, plan the FFT. Load and/or store callback functions can be provided to
1262
+ :meth:`plan` using the `prolog` and `epilog` option:
1263
+
1264
+ >>> f.plan()
1265
+
1266
+ Now execute the FFT, and obtain the result `r1` as a CuPy ndarray. The transform
1267
+ will be performed on GPU, because ``execution`` was not explicitly specified and
1268
+ ``a`` resides in GPU memory.
1269
+
1270
+ >>> r1 = f.execute()
1271
+
1272
+ Finally, free the FFT object's resources. To avoid this explicit call, it's
1273
+ recommended to use the FFT object as a context manager as shown below, if possible.
1274
+
1275
+ >>> f.free()
1276
+
1277
+ Note that all :class:`FFT` methods execute on the current stream by default.
1278
+ Alternatively, the `stream` argument can be used to run a method on a specified
1279
+ stream.
1280
+
1281
+ Let's now look at the same problem with NumPy ndarrays on the CPU.
1282
+
1283
+ Create a 3-D complex128 NumPy ndarray on the CPU:
1284
+
1285
+ >>> import numpy as np
1286
+ >>> shape = 128, 128, 128
1287
+ >>> a = np.random.rand(*shape) + 1j * np.random.rand(*shape)
1288
+
1289
+ Create an FFT object encapsulating the problem specification described earlier and
1290
+ use it as a context manager.
1291
+
1292
+ >>> with nvmath.fft.FFT(a, axes=axes) as f:
1293
+ ... f.plan()
1294
+ ...
1295
+ ... # Execute the FFT to get the first result.
1296
+ ... r1 = f.execute()
1297
+
1298
+ All the resources used by the object are released at the end of the block.
1299
+
1300
+ The operation was performed on the CPU because ``a`` resides in host memory. With
1301
+ ``execution`` specified to 'cuda', the NumPy array would be temporarily copied to
1302
+ device memory and transformed on the GPU:
1303
+
1304
+ >>> with nvmath.fft.FFT(a, axes=axes, execution="cuda") as f:
1305
+ ... f.plan()
1306
+ ...
1307
+ ... # Execute the FFT to get the first result.
1308
+ ... r1 = f.execute()
1309
+
1310
+ Further examples can be found in the `nvmath/examples/fft
1311
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/fft>`_ directory.
1312
+
1313
+ Notes:
1314
+
1315
+ - The input must be Hermitian-symmetric when :attr:`FFTOptions.fft_type` is
1316
+ ``'C2R'``, otherwise the result is undefined. As a specific example, if the input
1317
+ for a C2R FFT was generated using an R2C FFT with an odd last axis size, then
1318
+ :attr:`FFTOptions.last_axis_parity` must be set to `odd` to recover the original
1319
+ signal.
1320
+ """
1321
+
1322
+ def __init__(
1323
+ self,
1324
+ operand,
1325
+ /,
1326
+ *,
1327
+ axes: Sequence[int] | None = None,
1328
+ options: FFTOptions | dict[str, Any] | None = None,
1329
+ execution: ExecutionCPU | ExecutionCUDA | str | dict[str, Any] | None = None,
1330
+ stream: AnyStream | None = None,
1331
+ ):
1332
+ self.operand = operand = tensor_wrapper.wrap_operand(operand)
1333
+ options, execution = setup_options_and_execution(operand.device, options, execution)
1334
+ self.options = options
1335
+ self.execution_options = execution
1336
+
1337
+ self.operand_dim = len(operand.shape)
1338
+
1339
+ if not axes and self.operand_dim > 3:
1340
+ raise ValueError(
1341
+ f"The tensor is {self.operand_dim}-D and FFTs in number of dimensions > 3 is not supported. The FFT "
1342
+ "axes need to be specified using the 'axes' option."
1343
+ )
1344
+
1345
+ if self.operand_dim == 0:
1346
+ raise ValueError(f"The tensor is {self.operand_dim}-D (i.e. a scalar). FFT does not support scalars.")
1347
+
1348
+ self.operand_data_type = operand.dtype
1349
+ self.fft_abstract_type = _get_default_fft_abstract_type(self.operand_data_type, options.fft_type)
1350
+
1351
+ self.result_data_type, self.compute_data_type = _get_fft_result_and_compute_types(operand.dtype, self.fft_abstract_type)
1352
+
1353
+ self.logger = options.logger if options.logger is not None else logging.getLogger()
1354
+ self.logger.info(f"The FFT type is {self.fft_abstract_type}.")
1355
+ self.logger.info(
1356
+ f"The input data type is {self.operand_data_type}, and the result data type is {self.result_data_type}."
1357
+ )
1358
+
1359
+ if axes is None:
1360
+ axes = range(self.operand_dim)
1361
+
1362
+ if any(axis >= self.operand_dim or axis < -self.operand_dim for axis in axes):
1363
+ raise ValueError(f"The specified FFT axes {axes} are out of bounds for a {self.operand_dim}-D tensor.")
1364
+
1365
+ # Handle negative axis indices.
1366
+ self.axes = tuple(axis % self.operand_dim for axis in axes)
1367
+ self.logger.info(f"The specified FFT axes are {self.axes}.")
1368
+
1369
+ self.package = utils.infer_object_package(operand.tensor)
1370
+
1371
+ # NumPy and CuPy don't support complex32 yet.
1372
+ if self.package in ["numpy", "cupy"] and self.result_data_type == "complex32":
1373
+ raise TypeError(
1374
+ f"The result data type {self.result_data_type} is not supported by the operand package '{self.package}'."
1375
+ )
1376
+
1377
+ # Infer operand package, execution space, and memory space.
1378
+ if execution.name == "cuda":
1379
+ if operand.device == "cuda": # exec space matches the mem space
1380
+ self.memory_space = "cuda"
1381
+ self.device_id = operand.device_id
1382
+ else: # we need to move inputs cpu -> gpu and outputs gpu -> cpu
1383
+ self.memory_space = "cpu"
1384
+ self.device_id = execution.device_id
1385
+ else:
1386
+ assert execution.name == "cpu"
1387
+ self.device_id = "cpu"
1388
+ if operand.device_id == "cpu": # exec space matches the mem space
1389
+ self.memory_space = "cpu"
1390
+ else: # we need to move inputs gpu -> cpu and outputs cpu -> gpu
1391
+ self.memory_space = "cuda"
1392
+ self.execution_space = execution.name
1393
+ self.operand_device_id = operand.device_id
1394
+ self.internal_op_package = self._internal_operand_package(self.package)
1395
+ exec_stream_holder, operand_stream_holder = self._get_or_create_stream_maybe(stream)
1396
+
1397
+ self.logger.info(
1398
+ f"The input tensor's memory space is {self.memory_space}, and the execution space "
1399
+ f"is {self.execution_space}, with device {self.device_id}."
1400
+ )
1401
+
1402
+ self.logger.info(
1403
+ f"The specified stream for the FFT ctor is "
1404
+ f"{(exec_stream_holder or operand_stream_holder) and getattr(exec_stream_holder or operand_stream_holder, 'obj', None)}." # noqa: E501
1405
+ )
1406
+
1407
+ # In-place FFT option, available only for C2C transforms.
1408
+ self.inplace = self.options.inplace
1409
+ if self.inplace and self.fft_abstract_type != "C2C":
1410
+ raise ValueError(
1411
+ f"The in-place option (FFTOptions.inplace=True) is only supported for complex-to-complex FFT. "
1412
+ f"The FFT type is '{self.fft_abstract_type}'."
1413
+ )
1414
+
1415
+ # Key and plan_args computed from the user's original operand
1416
+ # before any copy across memory spaces occurs.
1417
+ #
1418
+ # When a copy occurs (cross-device or C2R), the copied operand may have different
1419
+ # strides than the original. We always compute the key from the user's original
1420
+ # operand to match what create_key() does. This ensures:
1421
+ # 1. get_key() returns the same value as create_key() for the same operand
1422
+ # 2. reset_operand() validates against the user-facing key, not the internal copy
1423
+ # We only need one key because reset_operand() validation
1424
+ # ensures the new operand produces the same key.
1425
+ self.key_from_user_operand: tuple[tuple, tuple | None, tuple] | None = None
1426
+ self.plan_args_from_user_operand: tuple | None = None
1427
+
1428
+ # Compute from original operand whenever a copy will occur:
1429
+ # - Cross-device: .to() may change strides
1430
+ # - C2R: copy preserves strides, but we compute for consistency
1431
+ if self.memory_space != self.execution_space or self.fft_abstract_type == "C2R":
1432
+ try:
1433
+ # Pass inplace explicitly to match what create_key() does.
1434
+ self.plan_args_from_user_operand, _ = _compute_fft_plan_args_and_traits(
1435
+ operand.shape,
1436
+ operand.strides,
1437
+ operand.dtype,
1438
+ axes=self.axes,
1439
+ options=self.options,
1440
+ execution=self.execution_options,
1441
+ inplace=self.options.inplace,
1442
+ memory_space=operand.device,
1443
+ )
1444
+ self.key_from_user_operand = create_fft_key(self.plan_args_from_user_operand, self.execution_options)
1445
+ except UnsupportedLayoutError:
1446
+ # If the layout is unsupported, we won't be able to compute a key.
1447
+ # This is fine - validation will fall back to other checks.
1448
+ self.key_from_user_operand = None
1449
+ self.plan_args_from_user_operand = None
1450
+
1451
+ # Copy the operand to execution_space's device if needed.
1452
+ self.operand, self.operand_backup = _allocate_operand_or_identity(
1453
+ operand,
1454
+ operand_stream_holder,
1455
+ self.execution_space,
1456
+ self.memory_space,
1457
+ self.device_id,
1458
+ self.fft_abstract_type,
1459
+ self.logger,
1460
+ )
1461
+
1462
+ # Track whether the user has called release_operand(). This flag is
1463
+ # checked in _check_valid_operand to prevent execution after the user
1464
+ # has released their operand. It is cleared by reset_operand() and
1465
+ # reset_operand_unchecked().
1466
+ self._operand_released = False
1467
+
1468
+ # For C2R transforms, cuFFT and FFTW overwrite the input buffer
1469
+ # during execute(). This flag marks the operand as stale after each
1470
+ # execute() call so that users must call reset_operand() or
1471
+ # reset_operand_unchecked() before executing again.
1472
+ self._c2r_operand_stale = False
1473
+
1474
+ # For C2R with same-space CUDA, we allocated an auxiliary buffer above
1475
+ # on operand_stream_holder. Track its allocation stream so we can ensure
1476
+ # proper ordering, for example when the buffer is freed in free().
1477
+ self.c2r_buffer_stream = None
1478
+ if self.execution_space == "cuda" and self.execution_space == self.memory_space and self.fft_abstract_type == "C2R":
1479
+ assert operand_stream_holder is not None
1480
+ self.c2r_buffer_stream = operand_stream_holder.obj
1481
+
1482
+ operand = self.operand
1483
+ # Class invariant: self.internal_operand_layout always reflects the layout of
1484
+ # self.operand. For same-space this is the user's operand; for
1485
+ # cross-space it is the execution-space copy.
1486
+ self.internal_operand_layout = TensorLayout(shape=operand.shape, strides=operand.strides)
1487
+
1488
+ self._preallocated_result: utils.TensorHolder | None = None
1489
+
1490
+ if self.options.inplace: # Don't use self.inplace here, because we always set it to True for CPU tensors.
1491
+ self.logger.info("The FFT will be performed in-place, with the result overwriting the input.")
1492
+ else:
1493
+ self.logger.info("The FFT will be performed out-of-place.")
1494
+
1495
+ # Check if FFT is supported and calculate plan traits.
1496
+ self.plan_traits = get_fft_plan_traits(
1497
+ operand.shape,
1498
+ operand.strides,
1499
+ operand.dtype,
1500
+ self.axes,
1501
+ self.execution_options,
1502
+ fft_abstract_type=self.fft_abstract_type,
1503
+ last_axis_parity=self.options.last_axis_parity,
1504
+ result_layout=self.options.result_layout,
1505
+ logger=self.logger,
1506
+ )
1507
+
1508
+ # Derive plan_args_from_user_operand from plan_traits if not already set.
1509
+ if self.plan_args_from_user_operand is None:
1510
+ self.plan_args_from_user_operand = create_xt_plan_args(
1511
+ plan_traits=self.plan_traits,
1512
+ fft_abstract_type=self.fft_abstract_type,
1513
+ operand_data_type=self.operand_data_type,
1514
+ inplace=self.options.inplace,
1515
+ )
1516
+
1517
+ # Ensure key_from_user_operand is set (for same-device case
1518
+ # and cross-device fallback).
1519
+ if self.key_from_user_operand is None:
1520
+ self.key_from_user_operand = create_fft_key(self.plan_args_from_user_operand, self.execution_options)
1521
+
1522
+ self.logger.info(
1523
+ f"The operand data type = {self.operand_data_type}, shape = {self.internal_operand_layout.shape}, and "
1524
+ f"strides = {self.internal_operand_layout.strides}."
1525
+ )
1526
+ result_data_type, result_shape, result_strides = (
1527
+ (self.operand_data_type, self.internal_operand_layout.shape, self.internal_operand_layout.strides)
1528
+ if self.inplace
1529
+ else (self.result_data_type, self.plan_traits.result_shape, self.plan_traits.result_strides)
1530
+ )
1531
+ self.logger.info(f"The result data type = {result_data_type}, shape = {result_shape}, and strides = {result_strides}.")
1532
+ self.logger.info(f"The FFT batch size is {self.plan_traits.fft_batch_size}.")
1533
+
1534
+ ordered_fft_out_shape, ostride, odistance = (
1535
+ (self.plan_traits.ordered_fft_in_shape, self.plan_traits.istride, self.plan_traits.idistance)
1536
+ if self.inplace
1537
+ else (self.plan_traits.ordered_fft_out_shape, self.plan_traits.ostride, self.plan_traits.odistance)
1538
+ )
1539
+ self.logger.debug(
1540
+ f"The plan ordered axes = {self.plan_traits.ordered_axes}, ordered input shape = "
1541
+ f"{self.plan_traits.ordered_fft_in_shape}, ordered input embedding shape = "
1542
+ f"{self.plan_traits.ordered_fft_in_embedding_shape}, ordered output shape = {ordered_fft_out_shape}."
1543
+ )
1544
+ self.logger.debug(f"The plan input stride is {self.plan_traits.istride} with distance {self.plan_traits.idistance}.")
1545
+ self.logger.debug(f"The plan output stride is {ostride} with distance {odistance}.")
1546
+
1547
+ # The result's package and device.
1548
+ self.result_class = operand.__class__
1549
+
1550
+ # Set blocking or non-blocking behavior.
1551
+ self.blocking = self.options.blocking is True or self.memory_space == "cpu" or self.execution_space == "cpu"
1552
+ if self.blocking:
1553
+ self.call_prologue = "This call is blocking and will return only after the operation is complete."
1554
+ else:
1555
+ self.call_prologue = (
1556
+ "This call is non-blocking and will return immediately after the operation is launched on the device."
1557
+ )
1558
+
1559
+ # Set memory allocator.
1560
+ if self.execution_space == "cpu":
1561
+ self.allocator = None # currently, the nvpl/fftw does not support custom workspace allocation
1562
+ else:
1563
+ self.allocator = (
1564
+ options.allocator
1565
+ if options.allocator is not None
1566
+ else memory._MEMORY_MANAGER[self.internal_op_package](self.device_id, self.logger)
1567
+ )
1568
+
1569
+ if self.execution_space == "cpu":
1570
+ # the handle is created alongside planning
1571
+ self.handle = None
1572
+ else:
1573
+ # Create handle.
1574
+ with utils.device_ctx(self.device_id):
1575
+ self.handle = cufft.create()
1576
+
1577
+ # Set stream for the FFT.
1578
+ cufft.set_stream(self.handle, exec_stream_holder.ptr) # type: ignore[union-attr]
1579
+
1580
+ # Plan attributes.
1581
+ cufft.set_auto_allocation(self.handle, 0)
1582
+
1583
+ self.fft_planned = False
1584
+
1585
+ # Workspace lifecycle is managed by `Workspace` (allocate-on-demand,
1586
+ # reuse, mid-call release, exception cleanup, stream-ordered free).
1587
+ # The on_allocated callback re-registers the buffer with the cuFFT
1588
+ # handle via cufft.set_work_area on every fresh allocation (not on
1589
+ # reuse or the 0-byte fast path). On the CPU/nvpl/fftw path there
1590
+ # is no workspace concept, so self.workspace is left as None.
1591
+ if self.execution_space == "cuda":
1592
+ self.workspace: Workspace | None = Workspace(
1593
+ self.allocator,
1594
+ self.logger,
1595
+ label="fft workspace",
1596
+ on_allocated=lambda raw_ptr: cufft.set_work_area(self.handle, raw_ptr),
1597
+ device_id=self.device_id,
1598
+ )
1599
+ else:
1600
+ self.workspace = None
1601
+
1602
+ # Last event recorded by cuda_call_ctx; consumed on workspace release
1603
+ # so the free is ordered after the compute that touched the buffer.
1604
+ self.last_compute_event = None
1605
+
1606
+ self.valid_state = True
1607
+ self.logger.info("The FFT operation has been created.")
1608
+
1609
+ def _check_planned(self, *args, **kwargs):
1610
+ """ """
1611
+ what = kwargs["what"]
1612
+ if not self.fft_planned:
1613
+ raise RuntimeError(f"{what} cannot be performed before plan() has been called.")
1614
+
1615
+ @utils.precondition(_check_planned, "Returning the key")
1616
+ def get_key(self, *, prolog: DeviceCallable | None = None, epilog: DeviceCallable | None = None):
1617
+ """
1618
+ Get the key based on the current state of this FFT object,
1619
+ supplemented with the callbacks. The key is computed from the object's
1620
+ current plan, execution options, and operand properties.
1621
+
1622
+ Args:
1623
+ prolog: {prolog}
1624
+
1625
+ .. experimental:: parameter
1626
+
1627
+ epilog: {epilog}
1628
+
1629
+ .. experimental:: parameter
1630
+
1631
+ Returns:
1632
+ {fft_key}
1633
+
1634
+ .. seealso::
1635
+ :meth:`create_key`, :func:`estimate_workspace_size`
1636
+ """
1637
+ # plan_args_from_user_operand is guaranteed to be set after __init__
1638
+ return create_fft_key(
1639
+ self.plan_args_from_user_operand, # type: ignore[arg-type]
1640
+ self.execution_options,
1641
+ prolog=prolog,
1642
+ epilog=epilog,
1643
+ )
1644
+
1645
+ @staticmethod
1646
+ def create_key(
1647
+ operand,
1648
+ *,
1649
+ axes: Sequence[int] | None = None,
1650
+ options: FFTOptions | dict[str, Any] | None = None,
1651
+ execution: ExecutionCPU | ExecutionCUDA | str | dict[str, Any] | None = None,
1652
+ prolog: DeviceCallable | None = None,
1653
+ epilog: DeviceCallable | None = None,
1654
+ ):
1655
+ """
1656
+ Create a key as a compact representation of the FFT problem specification based on
1657
+ the given operand, axes and the FFT options. Note that different combinations of
1658
+ operand layout, axes and options can potentially correspond to the same underlying
1659
+ problem specification (key). Users may reuse the FFT objects when different input
1660
+ problems map to an identical key.
1661
+
1662
+ Args:
1663
+ operand: {operand}
1664
+
1665
+ axes: {axes}
1666
+
1667
+ options: {options}
1668
+
1669
+ execution: {execution}
1670
+
1671
+ prolog: {prolog}
1672
+
1673
+ .. experimental:: parameter
1674
+
1675
+ epilog: {epilog}
1676
+
1677
+ .. experimental:: parameter
1678
+
1679
+ Returns:
1680
+ {fft_key}
1681
+
1682
+ Notes:
1683
+ - Users may take advantage of this method to create cached version of
1684
+ :func:`fft` based on the stateful object APIs (see `caching.py
1685
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/fft/caching.py>`_
1686
+ for an example implementation).
1687
+ - This key is meant for runtime use only and not designed to be serialized or
1688
+ used on a different machine.
1689
+ - It is the user's responsibility to augment this key with the stream in case
1690
+ they use stream-ordered memory pools.
1691
+ - The operand's data is never accessed, only the metadata. When an operand is
1692
+ not at hand, an equivalent key can be built from that metadata alone
1693
+ with :meth:`create_key_from_metadata`.
1694
+
1695
+ .. seealso::
1696
+ :meth:`create_key_from_metadata`, :meth:`get_key`,
1697
+ :func:`estimate_workspace_size`
1698
+ """
1699
+ operand = tensor_wrapper.wrap_operand(operand)
1700
+ if len(operand.shape) == 0:
1701
+ # Match FFT.__init__: fail fast before any backend/plan computation.
1702
+ raise ValueError("The FFT of a scalar is a no-op.")
1703
+ options, execution = setup_options_and_execution(operand.device, options, execution)
1704
+ plan_args, _ = _compute_fft_plan_args_and_traits(
1705
+ operand.shape,
1706
+ operand.strides,
1707
+ operand.dtype,
1708
+ axes=axes,
1709
+ options=options,
1710
+ execution=execution,
1711
+ inplace=options.inplace,
1712
+ )
1713
+ return create_fft_key(plan_args, execution, prolog=prolog, epilog=epilog)
1714
+
1715
+ @staticmethod
1716
+ def create_key_from_metadata(
1717
+ shape: Sequence[int],
1718
+ dtype: str,
1719
+ memory_space: Literal["cpu", "cuda"],
1720
+ *,
1721
+ strides: Sequence[int] | None = None,
1722
+ axes: Sequence[int] | None = None,
1723
+ options: FFTOptions | None = None,
1724
+ execution: ExecutionCPU | ExecutionCUDA | None = None,
1725
+ prolog: DeviceCallable | None = None,
1726
+ epilog: DeviceCallable | None = None,
1727
+ ) -> tuple[tuple, tuple | None, tuple]:
1728
+ """
1729
+ .. experimental:: method
1730
+
1731
+ Create an FFT key from metadata.
1732
+
1733
+ This is the metadata-based counterpart to :meth:`create_key`. Rather than a live
1734
+ operand, it describes the problem through the operand's layout (``shape``,
1735
+ ``dtype``, ``memory_space``, and optionally ``strides``). This is useful when
1736
+ an operand is yet to be allocated but its layout is already known, for example
1737
+ to estimate the workspace size (see :func:`estimate_workspace_size`) ahead of
1738
+ allocation.
1739
+
1740
+ Args:
1741
+ shape: Sequence of ints (extent of each dimension in number of elements).
1742
+
1743
+ dtype: The data-type name (e.g. ``"complex64"``, ``"float32"``).
1744
+
1745
+ memory_space: "cpu" or "cuda"; it is used as the default execution space
1746
+ when ``execution`` is not given.
1747
+
1748
+ strides: Sequence of ints (in number of elements per dimension).
1749
+ If omitted, a C-contiguous (row-major) layout is assumed.
1750
+
1751
+ axes: {axes}
1752
+
1753
+ options: {options}
1754
+
1755
+ execution: Specify execution space options for the FFT as a
1756
+ :class:`ExecutionCUDA` or :class:`ExecutionCPU` object. Alternatively, a
1757
+ string ('cuda' or 'cpu'), or a `dict` with the 'name' key set to 'cpu' or
1758
+ 'cuda' and optional parameters relevant to the given execution space. If
1759
+ not specified, the execution space defaults to ``memory_space``.
1760
+
1761
+ prolog: {prolog}
1762
+
1763
+ epilog: {epilog}
1764
+
1765
+ Returns:
1766
+ {fft_key}
1767
+
1768
+ Examples:
1769
+ Build a key from metadata (a contiguous, GPU-resident operand) and use it to
1770
+ estimate the cuFFT workspace size, without allocating an operand. Omitting
1771
+ ``strides`` assumes a C-contiguous (row-major) layout:
1772
+
1773
+ >>> import nvmath
1774
+ >>> key = nvmath.fft.FFT.create_key_from_metadata(
1775
+ ... (512, 512),
1776
+ ... "complex64",
1777
+ ... "cuda",
1778
+ ... )
1779
+ >>> nbytes = nvmath.fft.estimate_workspace_size(key)
1780
+
1781
+ The key matches the one built from an equivalent live operand:
1782
+
1783
+ >>> import cupy as cp
1784
+ >>> a = cp.empty((512, 512), dtype=cp.complex64)
1785
+ >>> key == nvmath.fft.FFT.create_key(a, execution="cuda")
1786
+ True
1787
+
1788
+ .. seealso::
1789
+ :meth:`create_key`, :meth:`get_key`, :func:`estimate_workspace_size`
1790
+ """
1791
+ if memory_space not in ("cpu", "cuda"):
1792
+ raise ValueError(f"The 'memory_space' must be 'cpu' or 'cuda', got {memory_space!r}.")
1793
+
1794
+ if len(shape) == 0:
1795
+ # Equivalent of FFT.__init__'s scalar guard: FFT does not support 0-D operands.
1796
+ raise ValueError(
1797
+ f"The provided 'shape' {tuple(shape)} identifies a scalar (0-D operand), which FFT does not support."
1798
+ )
1799
+
1800
+ if not isinstance(dtype, str) or dtype not in NAME_TO_DATA_TYPE:
1801
+ raise ValueError(
1802
+ f"Unsupported or invalid 'dtype' {dtype!r}. It must be one of the supported "
1803
+ f"data-type names: {sorted(NAME_TO_DATA_TYPE)}."
1804
+ )
1805
+
1806
+ if strides is None:
1807
+ # Default to a row-major layout.
1808
+ strides = tuple(calculate_strides(shape, reversed(range(len(shape)))))
1809
+ elif len(strides) != len(shape):
1810
+ raise ValueError(f"The length of 'strides' ({len(strides)}) must match the length of 'shape' ({len(shape)}).")
1811
+
1812
+ options, execution = setup_options_and_execution(memory_space, options, execution)
1813
+ plan_args, _ = _compute_fft_plan_args_and_traits(
1814
+ shape, strides, dtype, axes=axes, options=options, execution=execution, inplace=options.inplace
1815
+ )
1816
+ return create_fft_key(plan_args, execution, prolog=prolog, epilog=epilog)
1817
+
1818
+ def __enter__(self):
1819
+ return self
1820
+
1821
+ def __exit__(self, exc_type, exc_value, traceback):
1822
+ self.free()
1823
+
1824
+ def _check_valid_fft(self, *args, **kwargs):
1825
+ """
1826
+ Check if FFT object is alive and well.
1827
+ """
1828
+ if not self.valid_state:
1829
+ raise InvalidFFTState("The FFT object cannot be used after resources are free'd")
1830
+
1831
+ def _free_plan_resources(self, exception: Exception | None = None) -> bool:
1832
+ """
1833
+ Free resources allocated in planning.
1834
+ """
1835
+
1836
+ if self.workspace is not None:
1837
+ self.workspace.set_size(0)
1838
+ self.fft_planned = False
1839
+ return True
1840
+
1841
+ def _internal_operand_package(self, package_name):
1842
+ if self.execution_space == "cuda":
1843
+ return package_name if package_name != "numpy" else "cuda"
1844
+ else:
1845
+ return package_name if package_name != "cupy" else "cupy_host"
1846
+
1847
+ def _get_or_create_stream_maybe(self, stream: AnyStream) -> tuple[StreamHolder | None, StreamHolder | None]:
1848
+ if self.execution_space == "cuda":
1849
+ assert isinstance(self.device_id, int), self.device_id
1850
+ stream_holder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
1851
+ return stream_holder, stream_holder
1852
+ elif self.memory_space == "cuda":
1853
+ assert isinstance(self.operand_device_id, int), self.operand_device_id
1854
+ operand_device_steam = utils.get_or_create_stream(self.operand_device_id, stream, self.package)
1855
+ return None, operand_device_steam
1856
+ else:
1857
+ return None, None
1858
+
1859
+ def _allocate_result_operand(self, exec_stream_holder: StreamHolder | None, log_debug):
1860
+ if log_debug:
1861
+ self.logger.debug("Beginning output (empty) tensor creation...")
1862
+ self.logger.debug(
1863
+ f"The output tensor shape = {self.plan_traits.result_shape} with strides = "
1864
+ f"{self.plan_traits.result_strides} and data type '{self.result_data_type}'."
1865
+ )
1866
+ result = utils.create_empty_tensor(
1867
+ self.result_class,
1868
+ self.plan_traits.result_shape,
1869
+ self.result_data_type,
1870
+ self.device_id,
1871
+ exec_stream_holder,
1872
+ verify_strides=False, # the strides are computed so that they are contiguous
1873
+ strides=self.plan_traits.result_strides,
1874
+ )
1875
+ if log_debug:
1876
+ self.logger.debug("The output (empty) tensor has been created.")
1877
+ return result
1878
+
1879
+ def _get_validate_direction(self, direction):
1880
+ if isinstance(direction, str) and (d := direction.upper()) in ["FORWARD", "INVERSE"]:
1881
+ direction = FFTDirection[d]
1882
+ else:
1883
+ direction = FFTDirection(direction)
1884
+
1885
+ if self.fft_abstract_type == "C2R":
1886
+ if direction != FFTDirection.INVERSE:
1887
+ raise ValueError(
1888
+ f"The specified direction {direction.name} is not compatible with the FFT type '{self.fft_abstract_type}'."
1889
+ )
1890
+ elif self.fft_abstract_type == "R2C": # noqa: SIM102
1891
+ if direction != FFTDirection.FORWARD:
1892
+ raise ValueError(
1893
+ f"The specified direction {direction.name} is not compatible with the FFT type '{self.fft_abstract_type}'."
1894
+ )
1895
+ return direction
1896
+
1897
+ @utils.precondition(_check_valid_fft)
1898
+ @utils.atomic(_free_plan_resources, method=True)
1899
+ def plan(
1900
+ self,
1901
+ *,
1902
+ prolog: DeviceCallable | None = None,
1903
+ epilog: DeviceCallable | None = None,
1904
+ stream: AnyStream | None = None,
1905
+ direction: FFTDirection | None = None,
1906
+ ):
1907
+ """Plan the FFT.
1908
+
1909
+ Args:
1910
+ prolog: {prolog}
1911
+
1912
+ .. experimental:: parameter
1913
+
1914
+ epilog: {epilog}
1915
+
1916
+ .. experimental:: parameter
1917
+
1918
+ stream: {stream}
1919
+
1920
+ direction: If specified, the same direction must be passed to subsequent
1921
+ :meth:`execute` calls. It may be used as a hint to optimize C2C planning for
1922
+ CPU FFT calls.
1923
+ """
1924
+ log_info = self.logger.isEnabledFor(logging.INFO)
1925
+ log_debug = self.logger.isEnabledFor(logging.DEBUG)
1926
+
1927
+ if self.fft_planned:
1928
+ self.logger.debug("The FFT has already been planned, and redoing the plan is not supported.")
1929
+ return
1930
+
1931
+ if self.execution_space == "cpu":
1932
+ stream_holder = None
1933
+ if prolog is not None or epilog is not None:
1934
+ raise ValueError("The 'prolog' and 'epilog' are not supported with CPU 'execution'.")
1935
+ else:
1936
+ assert isinstance(self.device_id, int)
1937
+ stream_holder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
1938
+
1939
+ # Set stream for the FFT.
1940
+ cufft.set_stream(self.handle, stream_holder.ptr)
1941
+
1942
+ # Set LTO-IR callbacks, if present.
1943
+ prolog = utils.check_or_create_options(DeviceCallable, prolog, "prolog", keep_none=True)
1944
+ epilog = utils.check_or_create_options(DeviceCallable, epilog, "epilog", keep_none=True)
1945
+ if not _is_lto_supported_shape(
1946
+ self.plan_traits,
1947
+ self.fft_abstract_type,
1948
+ self.operand_data_type,
1949
+ prolog,
1950
+ epilog,
1951
+ ):
1952
+ cb_kind = "prologs" if self.fft_abstract_type == "R2C" else "epilogs"
1953
+ raise ValueError(
1954
+ f"CUFFT_NOT_SUPPORTED\n"
1955
+ f"cuFFT 12.3.x (CUDA 13.3) does not support LTO {cb_kind} for "
1956
+ f"{self.fft_abstract_type} FFT. "
1957
+ f"As a workaround, you may: "
1958
+ f"1. upgrade to CUDA 13.4 or later, "
1959
+ f"2. apply the callback only on the complex side (epilog for R2C, prolog for C2R), "
1960
+ f"3. adjust the real operand size to comprise prime factors not larger than 127"
1961
+ )
1962
+ set_prolog_and_epilog(self.handle, prolog, epilog, self.operand_data_type, self.result_data_type, self.logger)
1963
+
1964
+ # Get all the arguments to xt_make_plan_many except for the first (the handle).
1965
+ if self.inplace:
1966
+ check_inplace_overlapping_layout(self.operand.shape, self.operand.strides)
1967
+ if self.operand_backup is not None:
1968
+ check_inplace_overlapping_layout(self.operand_backup.shape, self.operand_backup.strides)
1969
+
1970
+ plan_args = create_xt_plan_args(
1971
+ plan_traits=self.plan_traits,
1972
+ fft_abstract_type=self.fft_abstract_type,
1973
+ operand_data_type=self.operand_data_type,
1974
+ inplace=self.inplace,
1975
+ )
1976
+
1977
+ if log_debug:
1978
+ self.logger.debug(f"The FFT key (sans callback) is {self.key_from_user_operand}.")
1979
+
1980
+ self.logger.debug(
1981
+ f"The operand CUDA type is {NAME_TO_DATA_TYPE[self.operand_data_type].name}, and the result CUDA type is "
1982
+ f"{NAME_TO_DATA_TYPE[self.result_data_type].name}."
1983
+ )
1984
+ self.logger.debug(f"The CUDA type used for compute is {NAME_TO_DATA_TYPE[self.compute_data_type].name}.")
1985
+ if log_info:
1986
+ self.logger.info("Starting FFT planning...")
1987
+
1988
+ if self.execution_space == "cpu":
1989
+ if direction is not None:
1990
+ direction = self._get_validate_direction(direction)
1991
+ if self.inplace:
1992
+ result_ptr = self.operand.data_ptr
1993
+ else:
1994
+ # FFTW3 API requires passing pointers to the input and output during
1995
+ # planning. Passing different pointers to (properly strided and aligned)
1996
+ # data in subsequent execute calls is supported, but it is not clear what
1997
+ # planning is allowed to do with the provided pointers. For one, planning
1998
+ # can compare the two pointers for equality to decide if it is inplace or
1999
+ # out-of-place operation. To avoid subtle issues, just preallocate the
2000
+ # result tensor earlier.
2001
+ self._preallocated_result = self._allocate_result_operand(None, True)
2002
+ result_ptr = self._preallocated_result.data_ptr # type: ignore[attr-defined, union-attr]
2003
+ precision, *plan_args = fftw_plan_args(
2004
+ plan_args,
2005
+ self.operand.data_ptr,
2006
+ result_ptr,
2007
+ fft_abstract_type=self.fft_abstract_type,
2008
+ direction=direction,
2009
+ )
2010
+ with utils.host_call_ctx(timing=log_info) as elapsed:
2011
+ fftw.plan_with_nthreads(precision, self.execution_options.num_threads) # type: ignore[union-attr]
2012
+ try:
2013
+ assert self.handle is None
2014
+ self.handle = fftw.plan_many(precision, *plan_args)
2015
+ except OverflowError as e:
2016
+ raise ValueError(
2017
+ "Currently, the CPU FFT only supports the problem sizes that "
2018
+ "can be expressed as 32-bit signed integer. "
2019
+ "Tensors with shape extents or stride larger than "
2020
+ "`2147483647` are not currently supported."
2021
+ ) from e
2022
+ else:
2023
+ # FIXME: Move creation of stream_holder into this block, so assertion is not
2024
+ # needed
2025
+ assert isinstance(self.device_id, int), self.device_id
2026
+ assert stream_holder is not None
2027
+ with utils.cuda_call_ctx(stream_holder, blocking=True, timing=log_info) as (
2028
+ self.last_compute_event,
2029
+ elapsed,
2030
+ ):
2031
+ assert self.workspace is not None # cuda execution space => workspace was created
2032
+ self.workspace.set_size(int(cufft.xt_make_plan_many(self.handle, *plan_args)))
2033
+
2034
+ self.fft_planned = True
2035
+
2036
+ if log_info and elapsed.data is not None:
2037
+ self.logger.info(f"The FFT planning phase took {elapsed.data:.3f} ms to complete.")
2038
+
2039
+ def _maybe_wait_c2r_aux_buffer_on_last_compute(self):
2040
+ """
2041
+ (private) Make the C2R same-space auxiliary buffer's alloc stream
2042
+ wait on ``self.last_compute_event``, so any in-flight compute using
2043
+ the buffer is ordered before its (potentially stream-ordered-async)
2044
+ free. No-op in every other configuration, or when there is no
2045
+ outstanding compute event.
2046
+ """
2047
+ if (
2048
+ self.execution_space == self.memory_space
2049
+ and self.fft_abstract_type == "C2R"
2050
+ and self.c2r_buffer_stream is not None
2051
+ and self.last_compute_event is not None
2052
+ ):
2053
+ self.c2r_buffer_stream.wait(self.last_compute_event)
2054
+
2055
+ def _reset_operand_validate(self, operand):
2056
+ """(private method) Validate operand compatibility.
2057
+ This method is used by :meth:`reset_operand` to validate that the new
2058
+ user-provided operand satisfies all the requirements listed in
2059
+ the :meth:`reset_operand` docstring.
2060
+
2061
+ Args:
2062
+ operand: The new operand tensor to validate.
2063
+
2064
+ Raises:
2065
+ TypeError: If the library package doesn't match.
2066
+ ValueError: If the data type or device doesn't match, or if the
2067
+ operand's traits are incompatible with the original operand.
2068
+ """
2069
+
2070
+ # Validate package match
2071
+ package = utils.infer_object_package(operand.tensor)
2072
+ if self.package != package:
2073
+ message = f"Library package mismatch: '{self.package}' => '{package}'"
2074
+ raise TypeError(message)
2075
+
2076
+ # Validate data type match
2077
+ utils.check_attribute_match(self.operand_data_type, operand.dtype, "data type")
2078
+
2079
+ # Validate device match
2080
+ # In principle, we could support memory_space change, but it would require
2081
+ # updating self.memory_space and dependent properties like self.blocking,
2082
+ # which could be error-prone and would prevent inplace optimizations.
2083
+ operand_device_id = operand.device_id
2084
+ if operand_device_id != self.operand_device_id:
2085
+
2086
+ def device_str(device_id: int | Literal["cpu"]) -> str:
2087
+ return f"cuda:{device_id}" if isinstance(device_id, int) else f"{device_id}"
2088
+
2089
+ raise ValueError(
2090
+ f"The new operand must be on the same device as the original one. "
2091
+ f"The new operand's device is {device_str(operand_device_id)}, "
2092
+ f"the original device is {device_str(self.operand_device_id)}"
2093
+ )
2094
+
2095
+ # Validate FFT plan compatibility using key_from_user_operand,
2096
+ # matches create_key() behavior.
2097
+ # key_from_user_operand is always set after __init__.
2098
+ assert self.key_from_user_operand is not None, "Internal error: key_from_user_operand not set."
2099
+
2100
+ try:
2101
+ # Pass inplace explicitly to match what create_key() does.
2102
+ new_plan_args, _ = _compute_fft_plan_args_and_traits(
2103
+ operand.shape,
2104
+ operand.strides,
2105
+ operand.dtype,
2106
+ axes=self.axes,
2107
+ options=self.options,
2108
+ execution=self.execution_options,
2109
+ inplace=self.options.inplace,
2110
+ memory_space=operand.device,
2111
+ )
2112
+ new_key = create_fft_key(new_plan_args, self.execution_options)
2113
+ except UnsupportedLayoutError:
2114
+ new_key = None
2115
+
2116
+ if self.key_from_user_operand != new_key:
2117
+ self.logger.debug(f"The FFT key corresponding to the original operand is: {self.key_from_user_operand}.")
2118
+ if new_key is None:
2119
+ self.logger.debug(
2120
+ "The FFT key for the new operand cannot be computed since the layout "
2121
+ f"(shape = {operand.shape}, strides = {operand.strides}) and axes = {self.axes} combination "
2122
+ "is unsupported."
2123
+ )
2124
+ else:
2125
+ self.logger.debug(f"The FFT key corresponding to the new operand is: {new_key}.")
2126
+ raise ValueError(
2127
+ "The new operand's traits (data type, shape, or strides) are incompatible with that of the original operand."
2128
+ )
2129
+
2130
+ # Validation passed. No updates needed because:
2131
+ # - key_from_user_operand == new_key
2132
+ # - plan_args_from_user_operand produces same key, get_key() returns correct value
2133
+ # - plan_traits stays unchanged (copy produces the same layout)
2134
+ self.logger.debug("Validation passed; no updates needed since keys match.")
2135
+
2136
+ def _update_plan_traits_for_new_operand(self, new_operand_shape, new_operand_strides):
2137
+ """
2138
+ (private) Recompute plan_traits.result_shape/strides after
2139
+ a reset whose operand has different properties but a matching key.
2140
+
2141
+ Args:
2142
+ new_operand_shape: Shape of the internal buffer. Batch
2143
+ dimensions may differ from the original (e.g.
2144
+ (2,3,64) -> (3,2,64) with axes=(2,)).
2145
+ new_operand_strides: Strides of the internal buffer. Used to
2146
+ recompute the result axis order.
2147
+
2148
+ The FFT axis sizes in result_shape may differ from the operand
2149
+ (R2C/C2R), so we preserve them from the old result_shape and only
2150
+ replace the batch dimensions from the new operand.
2151
+
2152
+ The axis order used to compute result_strides depends on the
2153
+ layout strategy chosen at plan time (stored in
2154
+ plan_traits.optimized_result_layout):
2155
+ - Natural: axis_order_in_memory (mirrors the operand layout).
2156
+ - Optimized: reversed FFT axes + batch axes sorted by stride.
2157
+ """
2158
+ new_shape = list(new_operand_shape)
2159
+ old_result_shape = self.plan_traits.result_shape
2160
+ # Preserve FFT axis sizes (they differ from operand for R2C/C2R).
2161
+ for ax in self.plan_traits.ordered_axes:
2162
+ new_shape[ax] = old_result_shape[ax]
2163
+ new_result_shape = tuple(new_shape)
2164
+
2165
+ if self.plan_traits.optimized_result_layout:
2166
+ fft_axes_set = set(self.plan_traits.ordered_axes)
2167
+ batch_axes = [a for a in range(len(new_operand_shape)) if a not in fft_axes_set]
2168
+ axis_order = tuple(
2169
+ list(reversed(self.plan_traits.ordered_axes))
2170
+ + sorted(batch_axes, key=lambda v: (new_operand_strides[v], new_operand_shape[v]))
2171
+ )
2172
+ else:
2173
+ axis_order = axis_order_in_memory(new_operand_shape, new_operand_strides)
2174
+
2175
+ self.plan_traits.result_shape = new_result_shape
2176
+ self.plan_traits.result_strides = calculate_strides(new_result_shape, axis_order)
2177
+
2178
+ def _update_layout_and_plan_traits_if_needed(self):
2179
+ """
2180
+ (private) Update internal_operand_layout from the current internal operand,
2181
+ and recompute plan_traits if shape or strides changed.
2182
+
2183
+ The comparison is against self.operand (the internal buffer), not the
2184
+ user's original operand. For cross-space scenarios, the internal
2185
+ buffer is always a contiguous copy, so stride-only user changes are
2186
+ invisible here — which is correct because cuFFT sees contiguous data.
2187
+
2188
+ When does _update_plan_traits_for_new_operand get called?
2189
+ The columns below refer to the *internal buffer* (self.operand),
2190
+ not the user's original operand.
2191
+
2192
+ internal internal
2193
+ shape strides plan_traits
2194
+ Scenario (user's operand) changed changed updated
2195
+ -------------------------------- -------- -------- -----------
2196
+ Same operand (no change) No No No
2197
+ Different shape, same-space Yes Yes Yes
2198
+ Different shape, cross-space Yes Yes Yes
2199
+ Different strides only, same-space No Yes Yes
2200
+ Different strides only, cross-space No No No
2201
+ (cross-space copy is always contiguous, so internal strides
2202
+ are unchanged regardless of the user's strides)
2203
+ """
2204
+ new_layout = TensorLayout(shape=self.operand.shape, strides=self.operand.strides)
2205
+ if tuple(self.internal_operand_layout.shape) != tuple(new_layout.shape) or tuple(
2206
+ self.internal_operand_layout.strides
2207
+ ) != tuple(new_layout.strides):
2208
+ self._update_plan_traits_for_new_operand(new_layout.shape, new_layout.strides)
2209
+
2210
+ # Always update: the new operand may have a different layout but a
2211
+ # compatible key. Class invariant: self.internal_operand_layout
2212
+ # always reflects the layout of the currently stored operand.
2213
+ self.internal_operand_layout = new_layout
2214
+
2215
+ def _reset_operand_set_stream_and_update_operand(self, operand, stream: AnyStream | None):
2216
+ """(private method) Set the stream, copy the operand, and update layout information.
2217
+ This method is used by ``reset_operand()`` after validation has passed.
2218
+ It performs the following operations:
2219
+
2220
+ 1. Gets or creates the execution and operand streams
2221
+ 2. Sets the stream for the cuFFT handle (for CUDA execution)
2222
+ 3. Copies the operand tensor if necessary (reallocates when shape changed)
2223
+ 4. Updates internal_operand_layout and plan_traits if needed
2224
+
2225
+ Args:
2226
+ operand: The validated operand tensor to use.
2227
+ stream: Optional stream to use for execution.
2228
+ If None, a stream is created or retrieved.
2229
+ """
2230
+
2231
+ exec_stream_holder, operand_stream_holder = self._get_or_create_stream_maybe(stream)
2232
+ self.logger.info(
2233
+ "The specified stream for reset_operand() is "
2234
+ f"{(exec_stream_holder or operand_stream_holder) and (exec_stream_holder or operand_stream_holder).obj}." # type: ignore[union-attr]
2235
+ )
2236
+
2237
+ # Allocate a new internal buffer when the previous one was released
2238
+ # or when the new operand has a different shape (e.g., same FFT key but
2239
+ # rearranged batch dimensions). copy_() rejects incompatible shapes
2240
+ # due to broadcasting, and the underlying tensor type varies across
2241
+ # backends, so there is no uniform reshape; we reallocate instead.
2242
+ needs_reallocation = self._operand_released or self.operand.shape != operand.shape
2243
+ if needs_reallocation:
2244
+ # When the shape changes, the old self.operand buffer is implicitly
2245
+ # released here (replaced by the new allocation). For the C2R
2246
+ # same-space case this is an internal aux buffer the user cannot
2247
+ # stream-order against; in the other cases the old buffer is
2248
+ # either the user's tensor (same-space non-C2R, user orders) or a
2249
+ # cross-space mirror (cross-space operations are blocking).
2250
+ # When the operand was already released, release_operand() already
2251
+ # performed this ordering, so we skip it here.
2252
+ if not self._operand_released:
2253
+ self._maybe_wait_c2r_aux_buffer_on_last_compute()
2254
+ self.operand, self.operand_backup = _allocate_operand_or_identity(
2255
+ operand,
2256
+ operand_stream_holder,
2257
+ self.execution_space,
2258
+ self.memory_space,
2259
+ self.device_id,
2260
+ self.fft_abstract_type,
2261
+ self.logger,
2262
+ )
2263
+ else:
2264
+ self.operand, self.operand_backup = _copy_operand_or_identity(
2265
+ self.operand,
2266
+ operand,
2267
+ operand_stream_holder,
2268
+ self.execution_space,
2269
+ self.memory_space,
2270
+ self.fft_abstract_type,
2271
+ self.logger,
2272
+ )
2273
+
2274
+ # No-op for non-C2R; for C2R same-space, the operand copy above
2275
+ # provides fresh data so the stale flag can be cleared.
2276
+ self._c2r_operand_stale = False
2277
+
2278
+ if (
2279
+ needs_reallocation
2280
+ and self.execution_space == "cuda"
2281
+ and self.execution_space == self.memory_space
2282
+ and self.fft_abstract_type == "C2R"
2283
+ ):
2284
+ assert operand_stream_holder is not None
2285
+ self.c2r_buffer_stream = operand_stream_holder.obj
2286
+
2287
+ self._update_layout_and_plan_traits_if_needed()
2288
+
2289
+ # Log final result layout
2290
+ self.logger.info(
2291
+ f"The reset operand shape = {self.internal_operand_layout.shape}, "
2292
+ f"and strides = {self.internal_operand_layout.strides}."
2293
+ )
2294
+ result_shape, result_strides = (
2295
+ (self.internal_operand_layout.shape, self.internal_operand_layout.strides)
2296
+ if self.inplace
2297
+ else (self.plan_traits.result_shape, self.plan_traits.result_strides)
2298
+ )
2299
+ self.logger.info(f"The result shape = {result_shape}, and strides = {result_strides}.")
2300
+ self.logger.info("The operand has been reset to the specified operand.")
2301
+
2302
+ @utils.precondition(_check_valid_fft)
2303
+ def reset_operand(self, operand, *, stream: AnyStream | None = None):
2304
+ """
2305
+ Reset the operand held by this :class:`FFT` instance to a new compatible
2306
+ operand for subsequent execution.
2307
+
2308
+ Args:
2309
+ operand: A tensor (ndarray-like object) compatible with the previous one.
2310
+ The new operand is considered compatible if all the
2311
+ following properties match with the previous one:
2312
+
2313
+ - The problem specification key for the new operand. Generally the keys will
2314
+ match if the operand shares the same layout (shape, strides and data
2315
+ type). The keys may still match for certain operands with different
2316
+ layout, see :meth:`create_key` for details.
2317
+ - The package that the new operand belongs to.
2318
+ - The memory space of the new operand (CPU or GPU).
2319
+ - The device that new operand belongs to if it is on GPU.
2320
+
2321
+ stream: {stream}
2322
+
2323
+ Semantics:
2324
+ - If execution space == memory space and the FFT is not a C2R transform:
2325
+ operand reference update with no data copying.
2326
+
2327
+ - If execution space == memory space, the FFT is a C2R transform:
2328
+ one data copy to an auxiliary tensor, required to prevent cuFFT from
2329
+ overwriting the user's input.
2330
+
2331
+ - If execution space != memory space:
2332
+ data must be copied between different memory spaces.
2333
+
2334
+ Examples:
2335
+
2336
+ >>> import cupy as cp
2337
+ >>> import nvmath
2338
+
2339
+ Create a 3-D complex128 ndarray on the GPU:
2340
+
2341
+ >>> shape = 128, 128, 128
2342
+ >>> a = cp.random.rand(*shape) + 1j * cp.random.rand(*shape)
2343
+
2344
+ Create an FFT object as a context manager
2345
+
2346
+ >>> axes = 0, 1
2347
+ >>> with nvmath.fft.FFT(a, axes=axes) as f:
2348
+ ... # Plan the FFT
2349
+ ... f.plan()
2350
+ ...
2351
+ ... # Execute the FFT to get the first result.
2352
+ ... r1 = f.execute()
2353
+ ...
2354
+ ... # Reset the operand to a new CuPy ndarray.
2355
+ ... b = cp.random.rand(*shape) + 1j * cp.random.rand(*shape)
2356
+ ... f.reset_operand(b)
2357
+ ...
2358
+ ... # Execute to get the new result corresponding to the updated operand.
2359
+ ... r2 = f.execute()
2360
+
2361
+ With :meth:`reset_operand`, minimal overhead is achieved as problem
2362
+ specification and planning are only performed once.
2363
+ However it still performs validation to ensure that the operand is compatible
2364
+ with the original, and, if enabled, logging. See :meth:`reset_operand_unchecked`
2365
+ for an alternative when the caller has already validated the operand or chooses
2366
+ to skip validation and logging.
2367
+
2368
+ For the particular example above, explicitly calling :meth:`reset_operand` is
2369
+ equivalent to updating the operand in-place, i.e, replacing
2370
+ ``f.reset_operand(b)`` with ``a[:]=b``. Note that updating the operand in-place
2371
+ should be adopted with caution as it can only yield the expected result and
2372
+ incur no additional copies under the additional constraints below:
2373
+
2374
+ - The operation is not a complex-to-real (C2R) FFT.
2375
+ - The operand's memory matches the FFT execution space. More precisely, the
2376
+ operand memory space should be accessible from the execution space (CPU or
2377
+ CUDA).
2378
+
2379
+ For more details, please refer to `inplace update example
2380
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/fft/example05_stateful_inplace.py>`_.
2381
+
2382
+ .. seealso::
2383
+ :meth:`reset_operand_unchecked`, :meth:`release_operand`
2384
+ """
2385
+
2386
+ self.logger.info("Resetting operand...")
2387
+
2388
+ if operand is None:
2389
+ raise ValueError("reset_operand() requires a valid operand.")
2390
+
2391
+ operand = tensor_wrapper.wrap_operand(operand)
2392
+ self._reset_operand_validate(operand)
2393
+ self._reset_operand_set_stream_and_update_operand(operand, stream)
2394
+ self._operand_released = False
2395
+
2396
+ def reset_operand_unchecked(self, operand, *, stream: AnyStream | None = None):
2397
+ """
2398
+ .. experimental:: method
2399
+
2400
+ This method is a performance-optimized alternative to :meth:`reset_operand` that
2401
+ eliminates validation and logging overhead, making it ideal for
2402
+ performance-critical loops where operand compatibility is guaranteed by the caller.
2403
+
2404
+ Args:
2405
+ operand: A tensor (ndarray-like object) that is **guaranteed** by the user
2406
+ to be compatible with the original operand used during planning.
2407
+ See the ``operand`` parameter in :meth:`reset_operand` for the definition
2408
+ of compatibility.
2409
+
2410
+ stream: {stream}
2411
+
2412
+ Returns:
2413
+ None
2414
+
2415
+ Semantics:
2416
+ The semantics are the same as in :meth:`reset_operand`,
2417
+ except that this method does not perform any validation (e.g. package
2418
+ match, data type match, key match, etc.) or logging.
2419
+
2420
+ When to Use:
2421
+ - Performance-critical loops with repeated FFT executions on different operands
2422
+
2423
+ - After verifying correctness with :meth:`reset_operand` during development
2424
+
2425
+ - When operand compatibility is guaranteed by construction or invariant
2426
+
2427
+ Examples:
2428
+
2429
+ **Example 1: Optimizing a processing loop**
2430
+
2431
+ .. code-block:: python
2432
+
2433
+ import cupy as cp
2434
+ import nvmath
2435
+
2436
+ shape = (1024, 1024)
2437
+ operand = cp.random.rand(*shape, dtype=cp.complex64)
2438
+
2439
+ fft = nvmath.fft.FFT(operand, execution="cuda")
2440
+ with fft:
2441
+ fft.plan()
2442
+ for i in range(10000):
2443
+ # Process and create new operand with the same shape, dtype,
2444
+ # and device as the original operand
2445
+ new_operand = process_data(...)
2446
+ fft.reset_operand_unchecked(new_operand)
2447
+ result = fft.execute()
2448
+ # block until the result is ready
2449
+ ...
2450
+
2451
+ **Example 2: Streaming data processing**
2452
+
2453
+ Processing a stream of incoming data operands with identical layout:
2454
+
2455
+ .. code-block:: python
2456
+
2457
+ import cupy as cp
2458
+ import nvmath
2459
+
2460
+ # Create a stateful FFT object and prepare it once.
2461
+ shape = (512, 512)
2462
+ initial_operand = cp.empty(shape, dtype=cp.complex64)
2463
+
2464
+ fft = nvmath.fft.FFT(initial_operand, execution="cuda")
2465
+ with fft:
2466
+ fft.plan()
2467
+
2468
+ # Process stream of incoming operands
2469
+ for operand in incoming_data_stream():
2470
+ # The user guarantees that the operand is compatible
2471
+ # with the original (same shape, dtype, device, ...).
2472
+ fft.reset_operand_unchecked(operand)
2473
+ result = fft.execute()
2474
+ # block until the result is ready
2475
+ process_spectrum(result)
2476
+ ...
2477
+
2478
+ .. seealso::
2479
+ :meth:`reset_operand`: Safe, validated method for changing operands.
2480
+ :meth:`release_operand`: Release the internal references to the operand.
2481
+ :meth:`create_key`: For understanding FFT key compatibility.
2482
+ :func:`estimate_workspace_size`: For estimating workspace size.
2483
+ """
2484
+ # Case 1: Same execution and memory space, non-C2R transform
2485
+ # The user guarantees that the operand is in the same memory space as the original
2486
+ # operand so we can directly update the operand reference without copying data.
2487
+ # The TensorHolder wrapper is always alive (release_operand only clears
2488
+ # .tensor), so we can unconditionally swap the inner tensor.
2489
+ if self.execution_space == self.memory_space and self.fft_abstract_type != "C2R":
2490
+ self.operand.tensor = operand
2491
+ self.operand_backup = None
2492
+ self._operand_released = False
2493
+ self._update_layout_and_plan_traits_if_needed()
2494
+ return
2495
+
2496
+ # Cases 2 and 3 require data copying, so we need the stream
2497
+ _, operand_stream_holder = self._get_or_create_stream_maybe(stream)
2498
+ # and also require the wrapped operand
2499
+ operand_wrapped = tensor_wrapper.wrap_operand(operand)
2500
+
2501
+ # Case 2: C2R transform with same execution and memory space
2502
+ # Data must be copied to prevent cuFFT from overwriting the user's input buffer.
2503
+ # This is a corner case that stems from cuFFT behavior.
2504
+ if self.execution_space == self.memory_space:
2505
+ needs_reallocation = self._operand_released or self.operand.shape != operand_wrapped.shape
2506
+ if needs_reallocation:
2507
+ self.operand = utils.create_empty_tensor(
2508
+ operand_wrapped.__class__,
2509
+ operand_wrapped.shape,
2510
+ operand_wrapped.dtype,
2511
+ self.device_id,
2512
+ operand_stream_holder,
2513
+ verify_strides=True,
2514
+ strides=operand_wrapped.strides,
2515
+ )
2516
+ if self.execution_space == "cuda":
2517
+ self.c2r_buffer_stream = operand_stream_holder.obj # type: ignore[union-attr]
2518
+
2519
+ self.operand.copy_(operand_wrapped, stream_holder=operand_stream_holder)
2520
+ self.operand_backup = None
2521
+ self._operand_released = False
2522
+ self._c2r_operand_stale = False
2523
+ self._update_layout_and_plan_traits_if_needed()
2524
+ return
2525
+
2526
+ # Case 3: Cross-space scenario (execution_space != memory_space)
2527
+ # Example: CPU operand with CUDA execution, or CUDA operand with CPU execution.
2528
+ # Data must be copied between memory spaces.
2529
+ needs_reallocation = self._operand_released or self.operand.shape != operand_wrapped.shape
2530
+ if needs_reallocation:
2531
+ if self.execution_space == "cuda":
2532
+ to_device = self.device_id
2533
+ else:
2534
+ to_device = "cpu"
2535
+ self.operand = operand_wrapped.to(to_device, operand_stream_holder)
2536
+ else:
2537
+ self.operand.copy_(operand_wrapped, stream_holder=operand_stream_holder)
2538
+ self.operand_backup.tensor = operand_wrapped.tensor
2539
+ self._operand_released = False
2540
+ self._c2r_operand_stale = False
2541
+ self._update_layout_and_plan_traits_if_needed()
2542
+
2543
+ @utils.precondition(_check_valid_fft)
2544
+ def release_operand(self):
2545
+ """
2546
+ {release_operand}
2547
+ """
2548
+ if self._operand_released:
2549
+ self.logger.info("Operand has already been released; nothing to do.")
2550
+ return
2551
+
2552
+ # We release the references to the user-provided
2553
+ # operand and/or GPU mirrors of the user-provided operand
2554
+ # and/or the internal auxiliary buffer for C2R transforms
2555
+ # since it can be non-negligible memory.
2556
+ # Note that we do not release the whole wrapper objects,
2557
+ # but only their internal tensor references, which allow
2558
+ # us to reuse them without re-wrapping saving some overhead.
2559
+
2560
+ # Case 1 (same-space, non-C2R):
2561
+ # self.operand is the user's tensor,
2562
+ # self.operand_backup is None.
2563
+ # Case 2 (same-space, C2R):
2564
+ # self.operand references the internal auxiliary buffer
2565
+ # The user's tensor is not referenced. We must ensure the compute
2566
+ # that used this buffer has completed before releasing it.
2567
+ # Case 3 (cross-space):
2568
+ # self.operand is an internal mirror, self.operand_backup
2569
+ # is the user's tensor. Release both.
2570
+
2571
+ self._maybe_wait_c2r_aux_buffer_on_last_compute()
2572
+ self.c2r_buffer_stream = None
2573
+ self.operand.tensor = None
2574
+ if self.operand_backup is not None:
2575
+ self.operand_backup.tensor = None
2576
+
2577
+ self._operand_released = True
2578
+ self.logger.info("User-provided operand has been released.")
2579
+
2580
+ def get_input_layout(self):
2581
+ """
2582
+ Returns a pair of tuples: shape and strides of the FFT input.
2583
+
2584
+ .. note::
2585
+ In some cases, the FFT operation requires taking a copy of the input tensor
2586
+ (e.g. C2R cuFFT, or provided tensor resides on CPU but FFT is executed on GPU).
2587
+ The copied tensor strides may differ from the input tensor passed by the user,
2588
+ if the original tensor's strides do not conform to dense C-like layout.
2589
+ """
2590
+ return self.internal_operand_layout.shape, self.internal_operand_layout.strides
2591
+
2592
+ def get_output_layout(self):
2593
+ """
2594
+ Returns a pair of tuples: shape and strides of the FFT output.
2595
+ """
2596
+ return (
2597
+ (self.internal_operand_layout.shape, self.internal_operand_layout.strides)
2598
+ if self.inplace
2599
+ else (self.plan_traits.result_shape, self.plan_traits.result_strides)
2600
+ )
2601
+
2602
+ def _check_valid_operand(self, *args, **kwargs):
2603
+ """ """
2604
+ what = kwargs["what"]
2605
+ if self._operand_released:
2606
+ raise RuntimeError(
2607
+ f"{what} cannot be performed after the operand has been released. Use reset_operand() to provide a new "
2608
+ f"operand before performing the {what.lower()}."
2609
+ )
2610
+
2611
+ @utils.precondition(_check_valid_fft)
2612
+ @utils.precondition(_check_planned, "Execution")
2613
+ @utils.precondition(_check_valid_operand, "Execution")
2614
+ def execute(self, direction: FFTDirection | None = None, stream: AnyStream | None = None, release_workspace: bool = False):
2615
+ """
2616
+ Execute the FFT operation.
2617
+
2618
+ Args:
2619
+ direction: {direction}
2620
+
2621
+ stream: {stream}
2622
+
2623
+ release_workspace: {release_workspace}
2624
+
2625
+ Returns:
2626
+ The transformed operand, which remains on the same device and utilizes the same
2627
+ package as the input operand. The data type and shape of the transformed operand
2628
+ depend on the type of input operand:
2629
+
2630
+ - For C2C FFT, the data type and shape remain identical to the input.
2631
+ - For R2C and C2R FFT, both data type and shape differ from the input.
2632
+ """
2633
+
2634
+ if self.fft_abstract_type == "C2R" and self._c2r_operand_stale:
2635
+ raise RuntimeError(
2636
+ "For C2R FFTs, execute() cannot be called multiple times without "
2637
+ "resetting the operand in between. Please call reset_operand() or "
2638
+ "reset_operand_unchecked() before executing again."
2639
+ )
2640
+
2641
+ log_info = self.logger.isEnabledFor(logging.INFO)
2642
+ log_debug = self.logger.isEnabledFor(logging.DEBUG)
2643
+
2644
+ if direction is None:
2645
+ direction = _get_fft_default_direction(self.fft_abstract_type)
2646
+ else:
2647
+ direction = self._get_validate_direction(direction)
2648
+
2649
+ exec_stream_holder, operand_stream_holder = self._get_or_create_stream_maybe(stream)
2650
+
2651
+ if self.execution_space == "cpu":
2652
+ # Allocate output operand if needed.
2653
+ if self.inplace:
2654
+ result_ptr = self.operand.data_ptr
2655
+ else:
2656
+ if self._preallocated_result is not None:
2657
+ self.result = self._preallocated_result
2658
+ self._preallocated_result = None
2659
+ else:
2660
+ self.result = self._allocate_result_operand(exec_stream_holder, log_debug)
2661
+ result_ptr = self.result.data_ptr
2662
+
2663
+ if log_info:
2664
+ self.logger.info(
2665
+ f"Starting FFT {self.fft_abstract_type} calculation in the {direction.name} direction..." # type: ignore[union-attr]
2666
+ )
2667
+ self.logger.info(f"{self.call_prologue}")
2668
+
2669
+ with utils.host_call_ctx(timing=log_info) as elapsed:
2670
+ fftw.execute(self.handle, self.operand.data_ptr, result_ptr, direction)
2671
+ else:
2672
+ assert isinstance(self.device_id, int), self.device_id
2673
+ assert exec_stream_holder is not None
2674
+ assert self.workspace is not None # cuda execution space => workspace was created
2675
+
2676
+ # Set stream for the FFT.
2677
+ cufft.set_stream(self.handle, exec_stream_holder.ptr)
2678
+
2679
+ # The workspace buffer is plumbed into cuFFT via cufft.set_work_area,
2680
+ # which is invoked by the Workspace.on_allocated callback wired in
2681
+ # __init__ — so we don't need ws.raw_ptr / ws.size at the xt_exec
2682
+ # call site, only the with-block lifetime guarantees.
2683
+ with self.workspace.allocate_perhaps(
2684
+ exec_stream_holder,
2685
+ get_last_event=lambda: self.last_compute_event,
2686
+ ):
2687
+ # Allocate output operand if needed.
2688
+ if self.inplace:
2689
+ result_ptr = self.operand.data_ptr
2690
+ else:
2691
+ self.result = self._allocate_result_operand(exec_stream_holder, log_debug)
2692
+ result_ptr = self.result.data_ptr
2693
+
2694
+ if log_info:
2695
+ self.logger.info(
2696
+ f"Starting FFT {self.fft_abstract_type} calculation in the {direction.name} direction..." # type: ignore[union-attr]
2697
+ )
2698
+ self.logger.info(f"{self.call_prologue}")
2699
+
2700
+ with utils.cuda_call_ctx(exec_stream_holder, self.blocking, timing=log_info) as (
2701
+ self.last_compute_event,
2702
+ elapsed,
2703
+ ):
2704
+ if log_debug:
2705
+ self.logger.debug("The cuFFT execution function is 'xt_exec'.")
2706
+ cufft.xt_exec(self.handle, self.operand.data_ptr, result_ptr, direction)
2707
+
2708
+ if release_workspace:
2709
+ self.workspace.release(self.last_compute_event)
2710
+
2711
+ if self.fft_abstract_type == "C2R":
2712
+ self._c2r_operand_stale = True
2713
+
2714
+ if log_info and elapsed.data is not None:
2715
+ self.logger.info(f"The FFT calculation took {elapsed.data:.3f} ms to complete.")
2716
+
2717
+ # Return the result.
2718
+ result = self.operand if self.inplace else self.result
2719
+ if self.memory_space == self.execution_space:
2720
+ out = result.tensor
2721
+ else:
2722
+ if self.options.inplace: # Don't use self.inplace here, because we always set it to True for CPU tensors.
2723
+ self.operand_backup.copy_(result, stream_holder=operand_stream_holder)
2724
+ out = self.operand_backup.tensor
2725
+ else:
2726
+ target_dev = "cpu" if self.memory_space == "cpu" else self.operand_device_id
2727
+ out = result.to(target_dev, stream_holder=operand_stream_holder).tensor # type: ignore[arg-type]
2728
+
2729
+ # Release internal reference to the result to permit recycling of memory.
2730
+ self.result = None # type: ignore
2731
+
2732
+ return out
2733
+
2734
+ def free(self):
2735
+ """Free FFT resources.
2736
+
2737
+ It is recommended that the :class:`FFT` object be used within a context, but if it
2738
+ is not possible then this method must be called explicitly to ensure that the FFT
2739
+ resources (especially internal library objects) are properly cleaned up.
2740
+ """
2741
+
2742
+ if not self.valid_state:
2743
+ return
2744
+
2745
+ try:
2746
+ # Ensure ordering with respect to the last computation
2747
+ # to avoid race conditions when releasing internal resources.
2748
+ self._maybe_wait_c2r_aux_buffer_on_last_compute()
2749
+ if self.workspace is not None:
2750
+ self.workspace.release(self.last_compute_event)
2751
+ self.last_compute_event = None
2752
+
2753
+ if self.handle is not None:
2754
+ if self.execution_space == "cuda":
2755
+ cufft.destroy(self.handle)
2756
+ else:
2757
+ fftw.destroy(self.handle)
2758
+ self.handle = None
2759
+
2760
+ # Set all attributes to None except for logger and valid_state
2761
+ _keep = {"logger", "valid_state"}
2762
+ for attr in list(vars(self)):
2763
+ if attr not in _keep:
2764
+ setattr(self, attr, None)
2765
+
2766
+ except Exception as e:
2767
+ self.logger.critical("Internal error: only part of the FFT object's resources have been released.")
2768
+ self.logger.critical(str(e))
2769
+ raise e
2770
+ finally:
2771
+ self.valid_state = False
2772
+
2773
+ self.logger.info("The FFT object's resources have been released.")
2774
+
2775
+
2776
+ def _fft(
2777
+ operand,
2778
+ /,
2779
+ *,
2780
+ axes: Sequence[int] | None = None,
2781
+ direction: FFTDirection | None = None,
2782
+ options: FFTOptions | dict[str, Any] | None = None,
2783
+ execution: ExecutionCPU | ExecutionCUDA | str | dict[str, Any] | None = None,
2784
+ prolog: DeviceCallable | None = None,
2785
+ epilog: DeviceCallable | None = None,
2786
+ stream: AnyStream | None = None,
2787
+ check_dtype: str | None = None,
2788
+ ):
2789
+ r"""
2790
+ fft({function_signature})
2791
+
2792
+ Perform an N-D *complex-to-complex* (C2C) FFT on the provided complex operand.
2793
+
2794
+ Args:
2795
+ operand: {operand}
2796
+
2797
+ axes: {axes}
2798
+
2799
+ options: {options}
2800
+
2801
+ execution: {execution}
2802
+
2803
+ prolog: {prolog}
2804
+
2805
+ .. experimental:: parameter
2806
+
2807
+ epilog: {epilog}
2808
+
2809
+ .. experimental:: parameter
2810
+
2811
+ stream: {stream}
2812
+
2813
+ Returns:
2814
+ A transformed operand that retains the same data type and shape as the input. It
2815
+ remains on the same device and uses the same package as the input operand.
2816
+
2817
+ .. seealso::
2818
+ :func:`ifft`, :func:`irfft`, :func:`rfft`, :class:`FFT`
2819
+
2820
+ Examples:
2821
+
2822
+ >>> import cupy as cp
2823
+ >>> import nvmath
2824
+
2825
+ Create a 3-D complex128 ndarray on the GPU:
2826
+
2827
+ >>> shape = 256, 256, 256
2828
+ >>> a = cp.random.rand(*shape, dtype=cp.float64) + 1j * cp.random.rand(
2829
+ ... *shape, dtype=cp.float64
2830
+ ... )
2831
+
2832
+ Perform a 3-D C2C FFT using :func:`fft`. The result `r` is also a CuPy complex128
2833
+ ndarray:
2834
+
2835
+ >>> r = nvmath.fft.fft(a)
2836
+
2837
+ User may also perform FFT along a subset of dimensions, e.g, 2-D C2C FFT along the
2838
+ first two dimensions, batched along the last dimension:
2839
+
2840
+ >>> axes = 0, 1
2841
+ >>> r = nvmath.fft.fft(a, axes=axes)
2842
+
2843
+ For C2C type FFT operation, the output can be directly computed inplace thus
2844
+ overwriting the input operand. This can be specified using options to the FFT:
2845
+
2846
+ >>> o = nvmath.fft.FFTOptions(inplace=True)
2847
+ >>> r = nvmath.fft.fft(a, options=o)
2848
+ >>> r is a
2849
+ True
2850
+
2851
+ See :class:`FFTOptions` for the complete list of available options.
2852
+
2853
+ The package current stream is used by default, but a stream can be explicitly
2854
+ provided to the FFT operation. This can be done if the FFT operand is computed on a
2855
+ different stream, for example:
2856
+
2857
+ >>> s = cp.cuda.Stream()
2858
+ >>> with s:
2859
+ ... a = cp.random.rand(*shape) + 1j * cp.random.rand(*shape)
2860
+ >>> r = nvmath.fft.fft(a, stream=s)
2861
+
2862
+ The operation above runs on stream `s` and is ordered with respect to the input
2863
+ computation.
2864
+
2865
+ Create a NumPy ndarray on the CPU.
2866
+
2867
+ >>> import numpy as np
2868
+ >>> b = np.random.rand(*shape) + 1j * np.random.rand(*shape)
2869
+
2870
+ Provide the NumPy ndarray to :func:`fft`, with the result also being a NumPy
2871
+ ndarray:
2872
+
2873
+ >>> r = nvmath.fft.fft(b)
2874
+
2875
+ Notes:
2876
+ - This function only takes complex operand for C2C transformation. If the user
2877
+ wishes to perform full FFT transformation on real input, please cast the input to
2878
+ the corresponding complex data type.
2879
+ - This function is a convenience wrapper around :class:`FFT` and is specifically
2880
+ meant for *single* use. The same computation can be performed with the stateful
2881
+ API using the default `direction` argument in :meth:`FFT.execute`.
2882
+
2883
+ Further examples can be found in the `nvmath/examples/fft
2884
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/fft>`_ directory.
2885
+ """
2886
+ if check_dtype is not None:
2887
+ assert check_dtype in {"real", "complex"}, "internal error"
2888
+ wrapped = tensor_wrapper.wrap_operand(operand)
2889
+ if ("complex" in wrapped.dtype) != (check_dtype == "complex"):
2890
+ raise ValueError(f"This function expects {check_dtype} operand, found {wrapped.dtype}")
2891
+
2892
+ with FFT(operand, axes=axes, options=options, execution=execution, stream=stream) as fftobj:
2893
+ # Plan the FFT.
2894
+ fftobj.plan(stream=stream, prolog=prolog, epilog=epilog, direction=direction)
2895
+
2896
+ # Execute the FFT.
2897
+ result = fftobj.execute(direction=direction, stream=stream)
2898
+
2899
+ return result
2900
+
2901
+
2902
+ # Forward C2C FFT Function.
2903
+ fft = functools.wraps(_fft)(functools.partial(_fft, direction=FFTDirection.FORWARD, check_dtype="complex"))
2904
+ if fft.__doc__ is not None:
2905
+ fft.__doc__ = fft.__doc__.format(**SHARED_FFT_DOCUMENTATION) # type: ignore
2906
+ fft.__name__ = "fft"
2907
+
2908
+
2909
+ # Forward R2C FFT Function
2910
+ @utils.docstring_decorator(SHARED_FFT_DOCUMENTATION, skip_missing=False)
2911
+ def rfft(
2912
+ operand,
2913
+ /,
2914
+ *,
2915
+ axes: Sequence[int] | None = None,
2916
+ options: FFTOptions | dict[str, Any] | None = None,
2917
+ execution: ExecutionCPU | ExecutionCUDA | str | dict[str, Any] | None = None,
2918
+ prolog: DeviceCallable | None = None,
2919
+ epilog: DeviceCallable | None = None,
2920
+ stream: AnyStream | None = None,
2921
+ ):
2922
+ r"""
2923
+ rfft({function_signature})
2924
+
2925
+ Perform an N-D *real-to-complex* (R2C) FFT on the provided real operand.
2926
+
2927
+ .. versionchanged:: 0.9.0
2928
+ The ``operand`` parameter is now positional-only.
2929
+
2930
+ Args:
2931
+ operand: {operand}
2932
+
2933
+ axes: {axes}
2934
+
2935
+ options: {options}
2936
+
2937
+ execution: {execution}
2938
+
2939
+ prolog: {prolog}
2940
+
2941
+ .. experimental:: parameter
2942
+
2943
+ epilog: {epilog}
2944
+
2945
+ .. experimental:: parameter
2946
+
2947
+ stream: {stream}
2948
+
2949
+ Returns:
2950
+ A complex tensor that remains on the same device and belongs to the same package as
2951
+ the input operand. The extent of the last transformed axis in the result will be
2952
+ ``operand.shape[axes[-1]] // 2 + 1``.
2953
+
2954
+
2955
+ .. seealso::
2956
+ :func:`fft`, :func:`irfft`, :class:`FFT`.
2957
+ """
2958
+ wrapped_operand = tensor_wrapper.wrap_operand(operand)
2959
+ # check if input operand if real type
2960
+ if "complex" in wrapped_operand.dtype:
2961
+ raise RuntimeError(f"rfft expects a real input, but got {wrapped_operand.dtype}. Please use fft for complex input.")
2962
+
2963
+ return _fft(
2964
+ operand,
2965
+ axes=axes,
2966
+ options=options,
2967
+ execution=execution,
2968
+ prolog=prolog,
2969
+ epilog=epilog,
2970
+ stream=stream,
2971
+ check_dtype="real",
2972
+ )
2973
+
2974
+
2975
+ # Inverse C2C/R2C FFT Function.
2976
+ ifft = functools.wraps(_fft)(functools.partial(_fft, direction=FFTDirection.INVERSE, check_dtype="complex"))
2977
+ ifft.__doc__ = """
2978
+ ifft({function_signature})
2979
+
2980
+ Perform an N-D *complex-to-complex* (C2C) inverse FFT on the provided complex operand.
2981
+ The direction is implicitly inverse.
2982
+
2983
+ Args:
2984
+ operand: {operand}
2985
+
2986
+ axes: {axes}
2987
+
2988
+ options: {options}
2989
+
2990
+ execution: {execution}
2991
+
2992
+ prolog: {prolog}
2993
+
2994
+ .. experimental:: parameter
2995
+
2996
+ epilog: {epilog}
2997
+
2998
+ .. experimental:: parameter
2999
+
3000
+ stream: {stream}
3001
+
3002
+ Returns:
3003
+ A transformed operand that retains the same data type and shape as the input. It
3004
+ remains on the same device and uses the same package as the input operand.
3005
+
3006
+ .. seealso::
3007
+ :func:`fft`, :func:`irfft`, :class:`FFT`.
3008
+
3009
+ Notes:
3010
+ - This function only takes complex operand for C2C transformation. If the user wishes
3011
+ to perform full FFT transformation on real input, please cast the input to the
3012
+ corresponding complex data type.
3013
+ - This function is a convenience wrapper around :class:`FFT` and is specifically
3014
+ meant for *single* use. The same computation can be performed with the stateful
3015
+ API by passing the argument ``direction='inverse'`` when calling
3016
+ :meth:`FFT.execute`.
3017
+ """.format(**SHARED_FFT_DOCUMENTATION)
3018
+ ifft.__name__ = "ifft"
3019
+
3020
+
3021
+ # Inverse C2R FFT Function.
3022
+ @utils.docstring_decorator(SHARED_FFT_DOCUMENTATION, skip_missing=False)
3023
+ def irfft(
3024
+ operand,
3025
+ /,
3026
+ *,
3027
+ axes: Sequence[int] | None = None,
3028
+ options: FFTOptions | dict[str, Any] | None = None,
3029
+ execution: ExecutionCPU | ExecutionCUDA | str | dict[str, Any] | None = None,
3030
+ prolog: DeviceCallable | None = None,
3031
+ epilog: DeviceCallable | None = None,
3032
+ stream: AnyStream | None = None,
3033
+ ):
3034
+ """
3035
+ irfft({function_signature})
3036
+
3037
+ Perform an N-D *complex-to-real* (C2R) FFT on the provided complex operand. The
3038
+ direction is implicitly inverse.
3039
+
3040
+ Args:
3041
+ operand: {operand}
3042
+
3043
+ axes: {axes}
3044
+
3045
+ options: {options}
3046
+
3047
+ execution: {execution}
3048
+
3049
+ prolog: {prolog}
3050
+
3051
+ .. experimental:: parameter
3052
+
3053
+ epilog: {epilog}
3054
+
3055
+ .. experimental:: parameter
3056
+
3057
+ stream: {stream}
3058
+
3059
+ Returns:
3060
+ A real tensor that remains on the same device and belongs to the same package as the
3061
+ input operand. The extent of the last transformed axis in the result will be
3062
+ ``(operand.shape[axes[-1]] - 1) * 2`` if :attr:`FFTOptions.last_axis_parity` is
3063
+ ``even``, or ``operand.shape[axes[-1]] * 2 - 1`` if
3064
+ :attr:`FFTOptions.last_axis_parity` is ``odd``.
3065
+
3066
+ .. seealso::
3067
+ :func:`fft`, :func:`ifft`, :class:`FFT`.
3068
+
3069
+ Examples:
3070
+
3071
+ >>> import cupy as cp
3072
+ >>> import nvmath
3073
+
3074
+ Create a 3-D symmetric complex128 ndarray on the GPU:
3075
+
3076
+ >>> shape = 512, 768, 256
3077
+ >>> a = nvmath.fft.rfft(cp.random.rand(*shape, dtype=cp.float64))
3078
+
3079
+ Perform a 3-D C2R FFT using the :func:`irfft` wrapper. The result `r` is a CuPy
3080
+ float64 ndarray:
3081
+
3082
+ >>> r = nvmath.fft.irfft(a)
3083
+ >>> r.dtype
3084
+ dtype('float64')
3085
+
3086
+ Notes:
3087
+
3088
+ - This function performs an inverse C2R N-D FFT, which is similar to `irfftn` but
3089
+ different from `irfft` in various numerical packages.
3090
+ - This function is a convenience wrapper around :class:`FFT` and is specifically
3091
+ meant for *single* use. The same computation can be performed with the stateful
3092
+ API by setting :attr:`FFTOptions.fft_type` to ``'C2R'`` and passing the argument
3093
+ ``direction='inverse'`` when calling :meth:`FFT.execute`.
3094
+ - **The input to this function must be Hermitian-symmetric, otherwise the result is
3095
+ undefined.** While the symmetry requirement is partially captured by the different
3096
+ extents in the last transformed dimension between the input and result, there are
3097
+ additional `constraints
3098
+ <https://docs.nvidia.com/cuda/cufft/#fourier-transform-types>`_. As a specific
3099
+ example, 1-D transforms require the first element (and the last element, if the
3100
+ extent is even) of the input to be purely real-valued. In addition, if the input
3101
+ to `irfft` was generated using an R2C FFT with an odd last axis size,
3102
+ :attr:`FFTOptions.last_axis_parity` must be set to ``odd`` to recover the original
3103
+ signal.
3104
+ - For more details, please refer to `C2R example
3105
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/fft/example07_c2r.py>`_
3106
+ and `odd C2R example
3107
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/fft/example07_c2r_odd.py>`_.
3108
+ """
3109
+ options = utils.check_or_create_options(FFTOptions, options, "FFT options")
3110
+ assert options is not None
3111
+ options.fft_type = "C2R"
3112
+ return _fft(
3113
+ operand,
3114
+ axes=axes,
3115
+ direction=FFTDirection.INVERSE,
3116
+ options=options,
3117
+ execution=execution,
3118
+ prolog=prolog,
3119
+ epilog=epilog,
3120
+ stream=stream,
3121
+ check_dtype="complex",
3122
+ )