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.
- nvmath/__init__.pxd +0 -0
- nvmath/__init__.py +45 -0
- nvmath/_internal/__init__.py +0 -0
- nvmath/_internal/attribute_ifc_factory.py +330 -0
- nvmath/_internal/layout.py +70 -0
- nvmath/_internal/templates.py +130 -0
- nvmath/_internal/threadsafe.py +106 -0
- nvmath/_internal/utils.py +43 -0
- nvmath/_internal/workspace.py +490 -0
- nvmath/_utils.py +147 -0
- nvmath/bindings/__init__.py +60 -0
- nvmath/bindings/_internal/__init__.pxd +0 -0
- nvmath/bindings/_internal/__init__.py +0 -0
- nvmath/bindings/_internal/common_types.pxd +31 -0
- nvmath/bindings/_internal/cublas.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cublas.pxd +530 -0
- nvmath/bindings/_internal/cublasLt.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cublasLt.pxd +59 -0
- nvmath/bindings/_internal/cublasMp.pxd +52 -0
- nvmath/bindings/_internal/cudss.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cudss.pxd +54 -0
- nvmath/bindings/_internal/cufft.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cufft.pxd +70 -0
- nvmath/bindings/_internal/cufftMp.pxd +77 -0
- nvmath/bindings/_internal/curand.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/curand.pxd +42 -0
- nvmath/bindings/_internal/cusolver.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cusolver.pxd +15 -0
- nvmath/bindings/_internal/cusolverDn.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cusolverDn.pxd +406 -0
- nvmath/bindings/_internal/cusolverMp.pxd +71 -0
- nvmath/bindings/_internal/cusolverSp.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cusolverSp.pxd +75 -0
- nvmath/bindings/_internal/cusparse.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cusparse.pxd +471 -0
- nvmath/bindings/_internal/cusparseLt.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cusparseLt.pxd +48 -0
- nvmath/bindings/_internal/cutensor.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cutensor.pxd +58 -0
- nvmath/bindings/_internal/mathdx.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/mathdx.pxd +116 -0
- nvmath/bindings/_internal/nvshmem.pxd +29 -0
- nvmath/bindings/_internal/utils.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/utils.pxd +174 -0
- nvmath/bindings/_internal/utils.pyi +10 -0
- nvmath/bindings/cublas.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cublas.pxd +558 -0
- nvmath/bindings/cublas.pyi +812 -0
- nvmath/bindings/cublasLt.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cublasLt.pxd +109 -0
- nvmath/bindings/cublasLt.pyi +1461 -0
- nvmath/bindings/cublasMp.pxd +85 -0
- nvmath/bindings/cublasMp.pyi +267 -0
- nvmath/bindings/cudss.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cudss.pxd +98 -0
- nvmath/bindings/cudss.pyi +443 -0
- nvmath/bindings/cufft.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cufft.pxd +118 -0
- nvmath/bindings/cufft.pyi +301 -0
- nvmath/bindings/cufftMp.pxd +124 -0
- nvmath/bindings/cufftMp.pyi +326 -0
- nvmath/bindings/curand.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/curand.pxd +71 -0
- nvmath/bindings/curand.pyi +189 -0
- nvmath/bindings/cusolver.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cusolver.pxd +62 -0
- nvmath/bindings/cusolver.pyi +320 -0
- nvmath/bindings/cusolverDn.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cusolverDn.pxd +430 -0
- nvmath/bindings/cusolverDn.pyi +422 -0
- nvmath/bindings/cusolverMp.pxd +98 -0
- nvmath/bindings/cusolverMp.pyi +114 -0
- nvmath/bindings/cusolverSp.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cusolverSp.pxd +95 -0
- nvmath/bindings/cusolverSp.pyi +70 -0
- nvmath/bindings/cusparse.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cusparse.pxd +546 -0
- nvmath/bindings/cusparse.pyi +1017 -0
- nvmath/bindings/cusparseLt.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cusparseLt.pxd +99 -0
- nvmath/bindings/cusparseLt.pyi +252 -0
- nvmath/bindings/cutensor.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cutensor.pxd +98 -0
- nvmath/bindings/cutensor.pyi +324 -0
- nvmath/bindings/cycublas.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycublas.pxd +664 -0
- nvmath/bindings/cycublasLt.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycublasLt.pxd +1045 -0
- nvmath/bindings/cycublasMp.pxd +171 -0
- nvmath/bindings/cycudss.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycudss.pxd +277 -0
- nvmath/bindings/cycufft.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycufft.pxd +333 -0
- nvmath/bindings/cycufftMp.pxd +342 -0
- nvmath/bindings/cycurand.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycurand.pxd +141 -0
- nvmath/bindings/cycusolver.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycusolver.pxd +137 -0
- nvmath/bindings/cycusolverDn.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycusolverDn.pxd +443 -0
- nvmath/bindings/cycusolverMp.pxd +107 -0
- nvmath/bindings/cycusolverSp.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycusolverSp.pxd +93 -0
- nvmath/bindings/cycusparse.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycusparse.pxd +679 -0
- nvmath/bindings/cycusparseLt.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycusparseLt.pxd +135 -0
- nvmath/bindings/cycutensor.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycutensor.pxd +189 -0
- nvmath/bindings/cymathdx.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cymathdx.pxd +552 -0
- nvmath/bindings/cynvshmem.pxd +118 -0
- nvmath/bindings/mathdx.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/mathdx.pxd +182 -0
- nvmath/bindings/mathdx.pyi +1562 -0
- nvmath/bindings/nvpl/__init__.pxd +0 -0
- nvmath/bindings/nvpl/__init__.py +13 -0
- nvmath/bindings/nvpl/_internal/__init__.pxd +0 -0
- nvmath/bindings/nvpl/_internal/__init__.py +0 -0
- nvmath/bindings/nvpl/_internal/blas.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/nvpl/_internal/blas.pxd +237 -0
- nvmath/bindings/nvpl/_internal/fft.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/nvpl/_internal/fft.pxd +36 -0
- nvmath/bindings/nvpl/blas.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/nvpl/blas.pxd +131 -0
- nvmath/bindings/nvpl/blas.pyi +168 -0
- nvmath/bindings/nvpl/cyblas.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/nvpl/cyblas.pxd +280 -0
- nvmath/bindings/nvpl/cyfft.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/nvpl/cyfft.pxd +93 -0
- nvmath/bindings/nvpl/fft.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/nvpl/fft.pxd +100 -0
- nvmath/bindings/nvpl/fft.pyi +168 -0
- nvmath/bindings/nvshmem.pxd +54 -0
- nvmath/bindings/nvshmem.pyi +191 -0
- nvmath/device/__init__.py +38 -0
- nvmath/device/_deprecated.py +33 -0
- nvmath/device/common.py +315 -0
- nvmath/device/common_backend.py +131 -0
- nvmath/device/common_cuda.py +201 -0
- nvmath/device/common_numba.py +300 -0
- nvmath/device/common_numba_cuda_mlir.py +202 -0
- nvmath/device/common_opaque_tensor.py +201 -0
- nvmath/device/cublasdx.py +1606 -0
- nvmath/device/cublasdx_backend.py +860 -0
- nvmath/device/cublasdx_numba.py +1534 -0
- nvmath/device/cublasdx_numba_cuda_mlir.py +208 -0
- nvmath/device/cufftdx.py +373 -0
- nvmath/device/cufftdx_backend.py +220 -0
- nvmath/device/cufftdx_numba.py +140 -0
- nvmath/device/cufftdx_numba_cuda_mlir.py +79 -0
- nvmath/device/curand_kernel.py +9147 -0
- nvmath/device/cusolverdx.py +2708 -0
- nvmath/device/cusolverdx_backend.py +440 -0
- nvmath/device/cusolverdx_numba.py +567 -0
- nvmath/device/cusolverdx_numba_cuda_mlir.py +604 -0
- nvmath/device/cusolverdx_overload_backend.py +1029 -0
- nvmath/device/llvm_array.py +29 -0
- nvmath/device/random.py +441 -0
- nvmath/device/random_helpers.py +23 -0
- nvmath/device/random_states.py +187 -0
- nvmath/device/types.py +138 -0
- nvmath/device/vector_types_numba.py +259 -0
- nvmath/distributed/__init__.py +200 -0
- nvmath/distributed/_internal/__init__.py +0 -0
- nvmath/distributed/_internal/nccl.py +86 -0
- nvmath/distributed/_internal/nvshmem.py +307 -0
- nvmath/distributed/_internal/symmetric_memory.py +35 -0
- nvmath/distributed/_internal/tensor_ifc.py +70 -0
- nvmath/distributed/_internal/tensor_ifc_cupy.py +68 -0
- nvmath/distributed/_internal/tensor_ifc_host_device.py +172 -0
- nvmath/distributed/_internal/tensor_ifc_numpy.py +46 -0
- nvmath/distributed/_internal/tensor_ifc_torch.py +162 -0
- nvmath/distributed/_internal/tensor_wrapper.py +81 -0
- nvmath/distributed/_utils.py +167 -0
- nvmath/distributed/distribution/__init__.py +30 -0
- nvmath/distributed/distribution/_configuration.py +39 -0
- nvmath/distributed/distribution/distributions.py +1024 -0
- nvmath/distributed/distribution/redistribute.py +1284 -0
- nvmath/distributed/fft/__init__.py +7 -0
- nvmath/distributed/fft/_configuration.py +82 -0
- nvmath/distributed/fft/fft.py +2742 -0
- nvmath/distributed/linalg/__init__.py +22 -0
- nvmath/distributed/linalg/_internal/__init__.py +3 -0
- nvmath/distributed/linalg/_internal/epilog_protocol.py +586 -0
- nvmath/distributed/linalg/_internal/matmul_desc_ifc.py +28 -0
- nvmath/distributed/linalg/advanced/__init__.py +8 -0
- nvmath/distributed/linalg/advanced/_configuration.py +171 -0
- nvmath/distributed/linalg/advanced/matmulmod.py +3573 -0
- nvmath/distributed/linalg/generic/__init__.py +8 -0
- nvmath/distributed/linalg/generic/_caching.py +66 -0
- nvmath/distributed/linalg/generic/_configuration.py +61 -0
- nvmath/distributed/linalg/generic/_factorization.py +172 -0
- nvmath/distributed/linalg/generic/_initialization.py +966 -0
- nvmath/distributed/linalg/generic/_problem_spec.py +511 -0
- nvmath/distributed/linalg/generic/solvermod.py +1368 -0
- nvmath/distributed/process_group.py +408 -0
- nvmath/fft/__init__.py +7 -0
- nvmath/fft/_configuration.py +189 -0
- nvmath/fft/_exec_utils.py +82 -0
- nvmath/fft/_helpers.py +237 -0
- nvmath/fft/fft.py +3122 -0
- nvmath/internal/__init__.pxd +3 -0
- nvmath/internal/__init__.py +10 -0
- nvmath/internal/_bindings.cp314t-win_amd64.pyd +0 -0
- nvmath/internal/_bindings.pxd +18 -0
- nvmath/internal/_device_utils.py +45 -0
- nvmath/internal/_layout/__init__.pxd +3 -0
- nvmath/internal/_layout/__init__.py +7 -0
- nvmath/internal/_layout/_layout.cp314t-win_amd64.pyd +0 -0
- nvmath/internal/_layout/_layout.pxd +1303 -0
- nvmath/internal/_layout/_layout.pyi +1145 -0
- nvmath/internal/enum_utils.py +142 -0
- nvmath/internal/formatters.py +87 -0
- nvmath/internal/mem_limit.py +51 -0
- nvmath/internal/memory.cp314t-win_amd64.pyd +0 -0
- nvmath/internal/memory.pxd +13 -0
- nvmath/internal/memory.pyi +50 -0
- nvmath/internal/ndbuffer/__init__.pxd +3 -0
- nvmath/internal/ndbuffer/__init__.py +9 -0
- nvmath/internal/ndbuffer/_copy_kernel.cp314t-win_amd64.pyd +0 -0
- nvmath/internal/ndbuffer/_copy_kernel.pxd +10 -0
- nvmath/internal/ndbuffer/_jit.cp314t-win_amd64.pyd +0 -0
- nvmath/internal/ndbuffer/_jit.pxd +7 -0
- nvmath/internal/ndbuffer/_ndbuffer.cp314t-win_amd64.pyd +0 -0
- nvmath/internal/ndbuffer/_ndbuffer.pxd +40 -0
- nvmath/internal/ndbuffer/_ndbuffer.pyi +463 -0
- nvmath/internal/ndbuffer/copy_kernel/args.h +34 -0
- nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/array_view.h +52 -0
- nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/elementwise.h +68 -0
- nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/grid_indexer.h +69 -0
- nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/transposed.h +242 -0
- nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/type_utils.h +39 -0
- nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/utils.h +132 -0
- nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/vec.h +159 -0
- nvmath/internal/ndbuffer/copy_kernel/elementwise.h +53 -0
- nvmath/internal/ndbuffer/copy_kernel/transposed.h +58 -0
- nvmath/internal/package_ifc.py +168 -0
- nvmath/internal/package_ifc_cuda.py +57 -0
- nvmath/internal/package_ifc_cupy.py +67 -0
- nvmath/internal/package_ifc_torch.py +69 -0
- nvmath/internal/package_wrapper.py +14 -0
- nvmath/internal/tensor_ifc.py +179 -0
- nvmath/internal/tensor_ifc_cupy.py +234 -0
- nvmath/internal/tensor_ifc_ndbuffer.py +147 -0
- nvmath/internal/tensor_ifc_numpy.py +184 -0
- nvmath/internal/tensor_ifc_torch.py +178 -0
- nvmath/internal/tensor_wrapper.py +160 -0
- nvmath/internal/typemaps.py +113 -0
- nvmath/internal/utils.py +805 -0
- nvmath/linalg/__init__.py +56 -0
- nvmath/linalg/_internal/__init__.py +3 -0
- nvmath/linalg/_internal/algo_cap_ifc.py +82 -0
- nvmath/linalg/_internal/algo_config_ifc.py +43 -0
- nvmath/linalg/_internal/batch.py +234 -0
- nvmath/linalg/_internal/enum_to_tuples.py +64 -0
- nvmath/linalg/_internal/epilog_protocol.py +766 -0
- nvmath/linalg/_internal/layout.py +624 -0
- nvmath/linalg/_internal/matmul_desc_ifc.py +28 -0
- nvmath/linalg/_internal/matmul_pref_ifc.py +27 -0
- nvmath/linalg/_internal/matrix_layout_ifc.py +26 -0
- nvmath/linalg/_internal/solver_utils.py +432 -0
- nvmath/linalg/_internal/typemaps.py +144 -0
- nvmath/linalg/_internal/utils.py +157 -0
- nvmath/linalg/advanced/__init__.py +8 -0
- nvmath/linalg/advanced/_algorithmmod.py +170 -0
- nvmath/linalg/advanced/_configuration.py +351 -0
- nvmath/linalg/advanced/helpers/__init__.py +5 -0
- nvmath/linalg/advanced/helpers/matmul.py +1316 -0
- nvmath/linalg/advanced/matmulmod.py +3734 -0
- nvmath/linalg/generic/__init__.py +53 -0
- nvmath/linalg/generic/_configuration/__init__.py +39 -0
- nvmath/linalg/generic/_configuration/layout.py +263 -0
- nvmath/linalg/generic/_configuration/match.py +734 -0
- nvmath/linalg/generic/_configuration/qualifiers.py +493 -0
- nvmath/linalg/generic/_configuration/solver_configuration.py +59 -0
- nvmath/linalg/generic/_configuration/wrap.py +217 -0
- nvmath/linalg/generic/_dtype.py +15 -0
- nvmath/linalg/generic/matmulmod.py +2094 -0
- nvmath/linalg/generic/solvermod.py +1301 -0
- nvmath/memory.py +279 -0
- nvmath/sparse/__init__.py +38 -0
- nvmath/sparse/_internal/__init__.py +21 -0
- nvmath/sparse/_internal/common_utils.py +147 -0
- nvmath/sparse/_internal/cudss_config_ifc.py +702 -0
- nvmath/sparse/_internal/cudss_data_ifc.py +399 -0
- nvmath/sparse/_internal/cudss_utils.py +506 -0
- nvmath/sparse/_internal/cusparse_utils.py +382 -0
- nvmath/sparse/_internal/sparse_bsc_ifc.py +303 -0
- nvmath/sparse/_internal/sparse_bsr_ifc.py +305 -0
- nvmath/sparse/_internal/sparse_coo_ifc.py +256 -0
- nvmath/sparse/_internal/sparse_csc_ifc.py +268 -0
- nvmath/sparse/_internal/sparse_csr_ifc.py +288 -0
- nvmath/sparse/_internal/sparse_dia_ifc.py +242 -0
- nvmath/sparse/_internal/sparse_format_helpers.py +601 -0
- nvmath/sparse/_internal/sparse_tensor_ifc.py +133 -0
- nvmath/sparse/_internal/sparse_ust_ifc.py +141 -0
- nvmath/sparse/_internal/utils.py +56 -0
- nvmath/sparse/advanced/__init__.py +7 -0
- nvmath/sparse/advanced/_configuration.py +227 -0
- nvmath/sparse/advanced/direct_solver.py +2069 -0
- nvmath/sparse/generic/__init__.py +7 -0
- nvmath/sparse/generic/_configuration.py +129 -0
- nvmath/sparse/generic/_helpers.py +137 -0
- nvmath/sparse/generic/_thunks.py +21 -0
- nvmath/sparse/generic/matmulmod.py +2353 -0
- nvmath/sparse/ust/__init__.py +7 -0
- nvmath/sparse/ust/_converters.py +422 -0
- nvmath/sparse/ust/_cpp.py +28 -0
- nvmath/sparse/ust/_drawer.py +565 -0
- nvmath/sparse/ust/_emitter.py +1033 -0
- nvmath/sparse/ust/_jit.py +188 -0
- nvmath/sparse/ust/_kernel.py +282 -0
- nvmath/sparse/ust/_utils.py +149 -0
- nvmath/sparse/ust/interfaces/__init__.py +0 -0
- nvmath/sparse/ust/interfaces/torch_interface.py +476 -0
- nvmath/sparse/ust/tensor.py +1016 -0
- nvmath/sparse/ust/tensor_format.py +957 -0
- nvmath/tensor/__init__.py +6 -0
- nvmath/tensor/_configuration.py +120 -0
- nvmath/tensor/_internal/__init__.py +3 -0
- nvmath/tensor/_internal/cutensor_config_ifc.py +279 -0
- nvmath/tensor/_internal/cutensor_utils.py +230 -0
- nvmath/tensor/_internal/data.py +43 -0
- nvmath/tensor/_internal/einsum_parser.py +444 -0
- nvmath/tensor/_internal/typemaps.py +96 -0
- nvmath/tensor/contract.py +1900 -0
- nvmath_python-1.0.0.dist-info/METADATA +134 -0
- nvmath_python-1.0.0.dist-info/RECORD +332 -0
- nvmath_python-1.0.0.dist-info/WHEEL +5 -0
- nvmath_python-1.0.0.dist-info/licenses/LICENSE +177 -0
- 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
|
+
)
|