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