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
|
@@ -0,0 +1,2742 @@
|
|
|
1
|
+
# Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
__all__ = ["allocate_operand", "FFT", "fft", "ifft", "rfft", "irfft"]
|
|
6
|
+
|
|
7
|
+
import functools
|
|
8
|
+
import logging
|
|
9
|
+
import math
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from types import ModuleType
|
|
13
|
+
from typing import Any, Literal, cast
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
import nvmath.distributed
|
|
18
|
+
from nvmath import memory
|
|
19
|
+
from nvmath._internal.layout import is_overlapping_layout
|
|
20
|
+
from nvmath.bindings import cufftMp as cufft # type: ignore
|
|
21
|
+
from nvmath.bindings import nvshmem # type: ignore
|
|
22
|
+
from nvmath.distributed._internal import tensor_wrapper
|
|
23
|
+
from nvmath.distributed._internal.nvshmem import NvshmemMemoryManager
|
|
24
|
+
from nvmath.distributed._internal.tensor_ifc import DistributedTensor
|
|
25
|
+
from nvmath.distributed._internal.tensor_ifc_numpy import CudaDistributedTensor
|
|
26
|
+
from nvmath.distributed.distribution import Box, Distribution, Slab
|
|
27
|
+
from nvmath.internal import formatters, ndbuffer, utils
|
|
28
|
+
from nvmath.internal.package_wrapper import AnyStream, StreamHolder
|
|
29
|
+
from nvmath.internal.typemaps import NAME_TO_DATA_TYPE, NAME_TO_ITEM_SIZE
|
|
30
|
+
|
|
31
|
+
from ._configuration import FFTDirection, FFTOptions
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class TensorLayout:
|
|
36
|
+
"""An internal data class for capturing the tensor layout."""
|
|
37
|
+
|
|
38
|
+
shape: Sequence[int]
|
|
39
|
+
strides: Sequence[int]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class _ProblemSpec:
|
|
44
|
+
"""This is used in a custom reduction to check that the FFT problem specification
|
|
45
|
+
is consistent across processes, and to infer global information (e.g shape)."""
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class Options:
|
|
49
|
+
"""
|
|
50
|
+
This is used for _ProblemSpec instead of FFTOptions
|
|
51
|
+
because it's going to be serialized as part of the custom reduction of the
|
|
52
|
+
_ProblemSpec, and we want to control which fields are included (for example
|
|
53
|
+
we don't need the logger).
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, options: FFTOptions):
|
|
57
|
+
self.fft_type = options.fft_type
|
|
58
|
+
self.redistribute = options.redistribute
|
|
59
|
+
self.blocking = options.blocking
|
|
60
|
+
|
|
61
|
+
fft_type: Literal["C2C", "C2R", "R2C"] | None
|
|
62
|
+
redistribute: bool
|
|
63
|
+
blocking: Literal[True, "auto"]
|
|
64
|
+
|
|
65
|
+
shape: list[int] # operand shape
|
|
66
|
+
is_C: bool # Is C memory layout
|
|
67
|
+
operand_dtype: str # str because TensorHolder.dtype returns str
|
|
68
|
+
package: Literal["numpy", "cupy", "torch"] # operand package
|
|
69
|
+
memory_space: Literal["cuda", "cpu"] # operand memory space
|
|
70
|
+
distribution: Slab | Sequence[Box] # distribution of FFT input/output operands
|
|
71
|
+
rank: int
|
|
72
|
+
options: Options # FFT options
|
|
73
|
+
|
|
74
|
+
# Global number of elements in the operand (calculated as part of the reduction).
|
|
75
|
+
# NOTE: Only computed and used with box distribution.
|
|
76
|
+
global_size: int = 0
|
|
77
|
+
# Max number of elements of the input operand across processes.
|
|
78
|
+
# NOTE: Only computed and used with box distribution.
|
|
79
|
+
input_max_elements: int = 0
|
|
80
|
+
# Max number of elements of the output operand across processes.
|
|
81
|
+
# NOTE: Only computed and used with box distribution.
|
|
82
|
+
output_max_elements: int = 0
|
|
83
|
+
# is_leaf=True means that this is the _ProblemSpec of a process before reducing
|
|
84
|
+
# with that of another process.
|
|
85
|
+
is_leaf: bool = True
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
SHARED_FFT_DOCUMENTATION = utils.COMMON_SHARED_DOC_MAP.copy()
|
|
89
|
+
SHARED_FFT_DOCUMENTATION.update(
|
|
90
|
+
{
|
|
91
|
+
"operand": SHARED_FFT_DOCUMENTATION["operand"],
|
|
92
|
+
#
|
|
93
|
+
"operand_admonitions": """
|
|
94
|
+
.. important::
|
|
95
|
+
GPU operands must be on the symmetric heap (for example, allocated with
|
|
96
|
+
``nvmath.distributed.allocate_symmetric_memory()``).
|
|
97
|
+
""",
|
|
98
|
+
#
|
|
99
|
+
"options": """\
|
|
100
|
+
Specify options for the FFT as a :class:`FFTOptions` object. Alternatively, a `dict` containing the parameters for the
|
|
101
|
+
``FFTOptions`` constructor can also be provided. If not specified, the value will be set to the default-constructed
|
|
102
|
+
``FFTOptions`` object.""".replace("\n", " "),
|
|
103
|
+
#
|
|
104
|
+
"distribution": """\
|
|
105
|
+
Specifies the distribution of input and output operands across processes, which can be: (i) according to
|
|
106
|
+
a Slab distribution (see :class:`nvmath.distributed.distribution.Slab`), or (ii) a custom box distribution
|
|
107
|
+
(see :class:`nvmath.distributed.distribution.Box`). With Slab distribution, this indicates the distribution
|
|
108
|
+
of the input operand (the output operand will use the complementary Slab distribution).
|
|
109
|
+
With box distribution, this indicates the input and output boxes.""".replace("\n", " "),
|
|
110
|
+
#
|
|
111
|
+
"direction": """\
|
|
112
|
+
Specify whether forward or inverse FFT is performed (:class:`FFTDirection` object, or as a string from ['forward',
|
|
113
|
+
'inverse'], "or as an int from [-1, 1] denoting forward and inverse directions respectively).""".replace("\n", " "),
|
|
114
|
+
#
|
|
115
|
+
"sync_symmetric_memory": """\
|
|
116
|
+
Indicates whether to issue a symmetric memory synchronization operation on the execute stream
|
|
117
|
+
before the FFT. Note that before the FFT starts executing, it is required that the input operand
|
|
118
|
+
be ready on all processes. A symmetric memory synchronization ensures completion and visibility
|
|
119
|
+
by all processes of previously issued local stores to symmetric memory. Advanced users who choose
|
|
120
|
+
to manage the synchronization on their own using the appropriate NVSHMEM API, or who know that
|
|
121
|
+
GPUs are already synchronized on the source operand, can set this to False.""".replace("\n", " "),
|
|
122
|
+
#
|
|
123
|
+
"function_signature": """\
|
|
124
|
+
operand,
|
|
125
|
+
/,
|
|
126
|
+
*,
|
|
127
|
+
distribution: Distribution | Sequence[Box],
|
|
128
|
+
sync_symmetric_memory: bool = True,
|
|
129
|
+
options: FFTOptions | dict[str, Any] | None = None,
|
|
130
|
+
stream: AnyStream | None = None
|
|
131
|
+
""".replace("\n", " "),
|
|
132
|
+
#
|
|
133
|
+
"reset_operand_unchecked": utils._reset_operand_unchecked_docstring(
|
|
134
|
+
False, version_added="0.9.0", validation_examples="package match, data type match, distribution validation"
|
|
135
|
+
),
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _calculate_slab_shape_strides(global_extents, partition_dim, rank, nranks, global_extents_padded=None):
|
|
141
|
+
"""Calculate the local slab shape and strides for the given rank, given the global shape
|
|
142
|
+
and partition dimension. If `global_extents_padded` is provided, calculate the strides
|
|
143
|
+
based on this shape.
|
|
144
|
+
"""
|
|
145
|
+
n = nranks
|
|
146
|
+
S = global_extents[partition_dim]
|
|
147
|
+
partition_dim_local_size = (S // n + 1) if rank < S % n else S // n
|
|
148
|
+
slab_shape = list(global_extents)
|
|
149
|
+
slab_shape[partition_dim] = partition_dim_local_size
|
|
150
|
+
if global_extents_padded is not None:
|
|
151
|
+
_, strides = _calculate_slab_shape_strides(global_extents_padded, partition_dim, rank, nranks)
|
|
152
|
+
else:
|
|
153
|
+
strides = calculate_strides(slab_shape, reversed(range(len(global_extents))))
|
|
154
|
+
return tuple(slab_shape), strides
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _calculate_local_box(global_shape, partition_dim, rank, nranks):
|
|
158
|
+
"""Given a global shape of data that is partitioned across ranks along the
|
|
159
|
+
`partition_dim` dimension according to cuFFTMp's slab distribution, return
|
|
160
|
+
the local box of this rank (as lower and upper coordinates in the global shape).
|
|
161
|
+
"""
|
|
162
|
+
lower = [0] * len(global_shape)
|
|
163
|
+
for i in range(rank):
|
|
164
|
+
shape, _ = _calculate_slab_shape_strides(global_shape, partition_dim, i, nranks)
|
|
165
|
+
lower[partition_dim] += shape[partition_dim]
|
|
166
|
+
shape, _ = _calculate_slab_shape_strides(global_shape, partition_dim, rank, nranks)
|
|
167
|
+
upper = list(shape)
|
|
168
|
+
upper[partition_dim] += lower[partition_dim]
|
|
169
|
+
return lower, upper
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _get_fft_concrete_type(dtype, fft_abstract_type):
|
|
173
|
+
FFTType = cufft.Type
|
|
174
|
+
if fft_abstract_type == "C2C":
|
|
175
|
+
if dtype == "complex64":
|
|
176
|
+
return FFTType["C2C"]
|
|
177
|
+
elif dtype == "complex128":
|
|
178
|
+
return FFTType["Z2Z"]
|
|
179
|
+
else:
|
|
180
|
+
raise ValueError(f"Incompatible dtype '{dtype}' for complex-to-complex transform.")
|
|
181
|
+
elif fft_abstract_type == "R2C":
|
|
182
|
+
if dtype == "float32":
|
|
183
|
+
return FFTType["R2C"]
|
|
184
|
+
elif dtype == "float64":
|
|
185
|
+
return FFTType["D2Z"]
|
|
186
|
+
else:
|
|
187
|
+
raise ValueError(f"Incompatible dtype '{dtype}' for real-to-complex transform.")
|
|
188
|
+
elif fft_abstract_type == "C2R":
|
|
189
|
+
if dtype == "complex64":
|
|
190
|
+
return FFTType["C2R"]
|
|
191
|
+
elif dtype == "complex128":
|
|
192
|
+
return FFTType["Z2D"]
|
|
193
|
+
else:
|
|
194
|
+
raise ValueError(f"Incompatible dtype '{dtype}' for complex-to-real transform.")
|
|
195
|
+
else:
|
|
196
|
+
raise ValueError(f"Unsupported FFT Type: '{fft_abstract_type}'")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _get_validate_direction(direction, fft_abstract_type):
|
|
200
|
+
if isinstance(direction, str) and (d := direction.upper()) in ["FORWARD", "INVERSE"]:
|
|
201
|
+
direction = FFTDirection[d]
|
|
202
|
+
else:
|
|
203
|
+
direction = FFTDirection(direction)
|
|
204
|
+
|
|
205
|
+
if fft_abstract_type == "C2R":
|
|
206
|
+
if direction != FFTDirection.INVERSE:
|
|
207
|
+
raise ValueError(
|
|
208
|
+
f"The specified direction {direction.name} is not compatible with the FFT type '{fft_abstract_type}'."
|
|
209
|
+
)
|
|
210
|
+
elif fft_abstract_type == "R2C": # noqa: SIM102
|
|
211
|
+
if direction != FFTDirection.FORWARD:
|
|
212
|
+
raise ValueError(
|
|
213
|
+
f"The specified direction {direction.name} is not compatible with the FFT type '{fft_abstract_type}'."
|
|
214
|
+
)
|
|
215
|
+
return direction
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def complex_to_real_equivalent(name):
|
|
219
|
+
assert "complex" in name, f"Internal Error ({name=})"
|
|
220
|
+
m = name.split("complex")
|
|
221
|
+
assert len(m) in (1, 2)
|
|
222
|
+
size = int(m[-1]) // 2
|
|
223
|
+
if len(m) == 1:
|
|
224
|
+
return f"float{size}"
|
|
225
|
+
else:
|
|
226
|
+
return f"{m[0]}float{size}"
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def real_to_complex_equivalent(name):
|
|
230
|
+
assert "float" in name, f"Internal Error ({name=})"
|
|
231
|
+
m = name.split("float")
|
|
232
|
+
assert len(m) in (1, 2)
|
|
233
|
+
size = int(m[-1])
|
|
234
|
+
if len(m) == 1:
|
|
235
|
+
return f"complex{size * 2}"
|
|
236
|
+
else:
|
|
237
|
+
return f"{m[0]}complex{size * 2}"
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _get_default_fft_abstract_type(dtype, fft_type) -> Literal["R2C", "C2R", "C2C"]:
|
|
241
|
+
if fft_type is not None:
|
|
242
|
+
return fft_type
|
|
243
|
+
|
|
244
|
+
f, c = "float", "complex"
|
|
245
|
+
if dtype[: len(f)] == f:
|
|
246
|
+
fft_type = "R2C"
|
|
247
|
+
elif dtype[: len(c)] == c:
|
|
248
|
+
fft_type = "C2C"
|
|
249
|
+
else:
|
|
250
|
+
raise ValueError(f"Unsupported dtype '{dtype}' for FFT.")
|
|
251
|
+
return fft_type
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _get_fft_result_and_compute_types(dtype, fft_abstract_type):
|
|
255
|
+
"""
|
|
256
|
+
Return result and compute data type given the input data type and the FFT type.
|
|
257
|
+
"""
|
|
258
|
+
if fft_abstract_type == "C2C":
|
|
259
|
+
return dtype, dtype
|
|
260
|
+
elif fft_abstract_type == "C2R":
|
|
261
|
+
return complex_to_real_equivalent(dtype), dtype
|
|
262
|
+
elif fft_abstract_type == "R2C":
|
|
263
|
+
return real_to_complex_equivalent(dtype), dtype
|
|
264
|
+
else:
|
|
265
|
+
raise ValueError(f"Unsupported FFT Type: '{fft_abstract_type}'")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _get_fft_default_direction(fft_abstract_type) -> FFTDirection:
|
|
269
|
+
"""
|
|
270
|
+
Return the default FFT direction (as object of type configuration.FFTDirection) based on
|
|
271
|
+
the FFT type.
|
|
272
|
+
"""
|
|
273
|
+
if fft_abstract_type in ["C2C", "R2C"]:
|
|
274
|
+
return FFTDirection.FORWARD
|
|
275
|
+
elif fft_abstract_type == "C2R":
|
|
276
|
+
return FFTDirection.INVERSE
|
|
277
|
+
else:
|
|
278
|
+
raise ValueError(f"Unsupported FFT Type: '{fft_abstract_type}'")
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def check_inplace_overlapping_layout(operand: utils.TensorHolder):
|
|
282
|
+
if is_overlapping_layout(operand.shape, operand.strides):
|
|
283
|
+
raise ValueError(
|
|
284
|
+
f"In-place transform is not supported because the tensor with shape "
|
|
285
|
+
f"{operand.shape} and strides {operand.strides} overlaps in memory."
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def calculate_strides(shape, axis_order):
|
|
290
|
+
"""
|
|
291
|
+
Calculate the strides for the provided shape and axis order.
|
|
292
|
+
"""
|
|
293
|
+
strides = [None] * len(shape)
|
|
294
|
+
|
|
295
|
+
stride = 1
|
|
296
|
+
for axis in axis_order:
|
|
297
|
+
strides[axis] = stride
|
|
298
|
+
stride *= shape[axis]
|
|
299
|
+
|
|
300
|
+
return tuple(strides)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _allocate_with_padded_buffer(
|
|
304
|
+
shape: Sequence[int],
|
|
305
|
+
capacity: int,
|
|
306
|
+
input_dtype,
|
|
307
|
+
memory_space: Literal["cpu", "cuda"],
|
|
308
|
+
package: ModuleType,
|
|
309
|
+
):
|
|
310
|
+
"""Allocate distributed tensor with memory for `capacity` elements of `input_dtype`
|
|
311
|
+
dtype on each rank, and return a view of shape `shape` of the first prod(shape)
|
|
312
|
+
elements in the 1D array.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
shape: Shape of the view returned. Note that this view will have a base tensor with
|
|
316
|
+
possibly larger capacity than required for this shape. The shape can vary across
|
|
317
|
+
ranks.
|
|
318
|
+
|
|
319
|
+
capacity: capacity of the allocated buffer in number of elements of the specified
|
|
320
|
+
dtype. **NOTE: the capacity must be the same on every rank and this is not
|
|
321
|
+
checked. Non-uniform capacity across ranks can lead to undefined behavior**.
|
|
322
|
+
|
|
323
|
+
input_dtype: dtype of the tensor elements.
|
|
324
|
+
"""
|
|
325
|
+
size = math.prod(shape)
|
|
326
|
+
assert size <= capacity, f"Internal error: requested shape {shape} exceeds specified capacity {capacity}"
|
|
327
|
+
if memory_space == "cuda":
|
|
328
|
+
if package is ndbuffer:
|
|
329
|
+
# Mimics allocate_symmetric_memory, just with NDBuffer which is not
|
|
330
|
+
# included there, as allocate_symmetric_memory is user-facing
|
|
331
|
+
# and NDBuffer is internal.
|
|
332
|
+
ctx = nvmath.distributed.get_context()
|
|
333
|
+
assert ctx is not None
|
|
334
|
+
a = CudaDistributedTensor.empty(
|
|
335
|
+
capacity,
|
|
336
|
+
dtype=input_dtype,
|
|
337
|
+
device_id=ctx.device_id,
|
|
338
|
+
symmetric_memory="nvshmem",
|
|
339
|
+
make_symmetric=False,
|
|
340
|
+
skip_symmetric_check=True,
|
|
341
|
+
).tensor
|
|
342
|
+
else:
|
|
343
|
+
a = nvmath.distributed.allocate_symmetric_memory((capacity,), package, dtype=input_dtype, skip_symmetric_check=True)
|
|
344
|
+
else:
|
|
345
|
+
a = package.empty((capacity,), dtype=input_dtype)
|
|
346
|
+
return tensor_wrapper.wrap_operand(a[:size]).reshape(shape, copy=False)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _calculate_capacity(
|
|
350
|
+
problem_spec: _ProblemSpec,
|
|
351
|
+
global_shape: Sequence[int],
|
|
352
|
+
fft_type: Literal["C2C", "C2R", "R2C"],
|
|
353
|
+
nranks: int,
|
|
354
|
+
) -> int:
|
|
355
|
+
"""Calculate the max number of elements that the input buffer on every rank must be able
|
|
356
|
+
to hold in order to perform the specified distributed FFT. Since the memory allocation
|
|
357
|
+
is on the symmetric heap, we need to use the same (max) capacity on every rank. Also
|
|
358
|
+
recall that the transform is inplace, so the buffer must be able to hold both the input
|
|
359
|
+
and output given the FFT type and input/output operand distribution."""
|
|
360
|
+
|
|
361
|
+
distribution = problem_spec.distribution
|
|
362
|
+
if fft_type == "C2C":
|
|
363
|
+
if isinstance(distribution, Slab):
|
|
364
|
+
# capacity is max of X-slab and Y-slab size on rank 0.
|
|
365
|
+
s1, _ = _calculate_slab_shape_strides(global_shape, 0, 0, nranks) # X-slab
|
|
366
|
+
s2, _ = _calculate_slab_shape_strides(global_shape, 1, 0, nranks) # Y-slab
|
|
367
|
+
return max(math.prod(s1), math.prod(s2))
|
|
368
|
+
else:
|
|
369
|
+
# capacity is the max number of elements across ranks for both input and output.
|
|
370
|
+
return max(problem_spec.input_max_elements, problem_spec.output_max_elements)
|
|
371
|
+
elif fft_type == "R2C":
|
|
372
|
+
if isinstance(distribution, Slab):
|
|
373
|
+
# capacity is max of X-slab and Y-slab size on rank 0 for complex shape.
|
|
374
|
+
global_output_shape = list(global_shape)
|
|
375
|
+
global_output_shape[-1] = global_output_shape[-1] // 2 + 1 # this is the complex shape
|
|
376
|
+
|
|
377
|
+
s1, _ = _calculate_slab_shape_strides(global_output_shape, 0, 0, nranks) # X-slab
|
|
378
|
+
s2, _ = _calculate_slab_shape_strides(global_output_shape, 1, 0, nranks) # Y-slab
|
|
379
|
+
|
|
380
|
+
# Capacity is returned in terms of input (real) elements.
|
|
381
|
+
return max(math.prod(s1) * 2, math.prod(s2) * 2)
|
|
382
|
+
else:
|
|
383
|
+
# Capacity is returned in terms of input (real) elements.
|
|
384
|
+
return max(problem_spec.input_max_elements, 2 * problem_spec.output_max_elements)
|
|
385
|
+
elif fft_type == "C2R":
|
|
386
|
+
if isinstance(distribution, Slab):
|
|
387
|
+
# capacity is max of X-slab and Y-slab size on rank 0.
|
|
388
|
+
s1, _ = _calculate_slab_shape_strides(global_shape, 0, 0, nranks) # X-slab
|
|
389
|
+
s2, _ = _calculate_slab_shape_strides(global_shape, 1, 0, nranks) # Y-slab
|
|
390
|
+
return max(math.prod(s1), math.prod(s2))
|
|
391
|
+
else:
|
|
392
|
+
# Capacity is returned in terms of input (complex) elements.
|
|
393
|
+
return max(
|
|
394
|
+
problem_spec.input_max_elements, problem_spec.output_max_elements // 2 + problem_spec.output_max_elements % 2
|
|
395
|
+
)
|
|
396
|
+
raise AssertionError(f"Internal error: Unknown FFT type {fft_type}")
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _allocate_for_fft(
|
|
400
|
+
global_input_shape: Sequence[int],
|
|
401
|
+
shape: Sequence[int],
|
|
402
|
+
distribution: Slab | Sequence[Box],
|
|
403
|
+
input_dtype,
|
|
404
|
+
memory_space: Literal["cpu", "cuda"],
|
|
405
|
+
package: ModuleType,
|
|
406
|
+
fft_type: Literal["C2C", "C2R", "R2C"],
|
|
407
|
+
capacity: int,
|
|
408
|
+
rank: int,
|
|
409
|
+
nranks: int,
|
|
410
|
+
):
|
|
411
|
+
"""Allocate distributed tensor for the given distributed FFT operation. The same
|
|
412
|
+
capacity must be provided on every rank, and must be large enough for the specified
|
|
413
|
+
transform."""
|
|
414
|
+
if fft_type == "R2C" and isinstance(distribution, Slab):
|
|
415
|
+
partition_dim = distribution.partition_dim
|
|
416
|
+
|
|
417
|
+
# For input, the strides depend on the padding.
|
|
418
|
+
global_output_shape = list(global_input_shape)
|
|
419
|
+
global_output_shape[-1] = global_output_shape[-1] // 2 + 1 # this is the complex shape
|
|
420
|
+
global_input_shape_padded = list(global_output_shape)
|
|
421
|
+
global_input_shape_padded[-1] *= 2
|
|
422
|
+
|
|
423
|
+
padded_shape, _ = _calculate_slab_shape_strides(global_input_shape_padded, partition_dim, rank, nranks)
|
|
424
|
+
a = _allocate_with_padded_buffer(padded_shape, capacity, input_dtype, memory_space, package)
|
|
425
|
+
|
|
426
|
+
# Return a view strided on the last axis.
|
|
427
|
+
return tensor_wrapper.wrap_operand(a.tensor[..., : shape[-1]])
|
|
428
|
+
else:
|
|
429
|
+
# These might not be the most efficient input strides for the R2C FFT (the whole
|
|
430
|
+
# input is packed at the beginning of the buffer with no strides), but to support
|
|
431
|
+
# other strides we probably need the user to pass them.
|
|
432
|
+
return _allocate_with_padded_buffer(shape, capacity, input_dtype, memory_space, package)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
_SUPPORTED_PACKAGES = ("numpy", "cupy", "torch")
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
@utils.docstring_decorator(SHARED_FFT_DOCUMENTATION, skip_missing=True)
|
|
439
|
+
def allocate_operand(
|
|
440
|
+
shape: Sequence[int],
|
|
441
|
+
package: ModuleType,
|
|
442
|
+
*,
|
|
443
|
+
input_dtype=None,
|
|
444
|
+
distribution: Distribution | Sequence[Box],
|
|
445
|
+
memory_space: Literal["cpu", "cuda"] | None = None,
|
|
446
|
+
fft_type: Literal["C2C", "C2R", "R2C"] | None = None,
|
|
447
|
+
logger: logging.Logger | None = None,
|
|
448
|
+
):
|
|
449
|
+
"""Return uninitialized operand of the given shape and type, to use as input for
|
|
450
|
+
distributed FFT. The resulting tensor is backed by a buffer large enough for the
|
|
451
|
+
specified FFT (the buffer can hold both the input and output -distributed FFT is
|
|
452
|
+
inplace-, accounting for both the input and output distribution).
|
|
453
|
+
For CUDA memory space, the tensor is allocated on the symmetric heap, on the
|
|
454
|
+
device on which nvmath.distributed was initialized.
|
|
455
|
+
**This is a collective operation and must be called by all processes**.
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
shape: Shape of the tensor to allocate.
|
|
459
|
+
|
|
460
|
+
package: Python package determining the tensor type (e.g. numpy, cupy, torch).
|
|
461
|
+
|
|
462
|
+
input_dtype: Tensor dtype in a form recognized by the package. If None, will use
|
|
463
|
+
the package's default dtype.
|
|
464
|
+
|
|
465
|
+
distribution: {distribution}
|
|
466
|
+
|
|
467
|
+
memory_space: The memory space (``'cpu'`` or ``'cuda'``) on which to allocate
|
|
468
|
+
the tensor. If not provided, this is inferred for packages that support
|
|
469
|
+
a single memory space like numpy and cupy. For other packages it must be
|
|
470
|
+
provided.
|
|
471
|
+
|
|
472
|
+
fft_type: The type of FFT to perform. Available options include ``'C2C'``,
|
|
473
|
+
``'C2R'``, and ``'R2C'``. The default is ``'C2C'`` for complex input and
|
|
474
|
+
``'R2C'`` for real input.
|
|
475
|
+
|
|
476
|
+
logger (logging.Logger): Python Logger object. The root logger will be used if a
|
|
477
|
+
logger object is not provided.
|
|
478
|
+
"""
|
|
479
|
+
|
|
480
|
+
package_name = package.__name__
|
|
481
|
+
if package_name not in _SUPPORTED_PACKAGES:
|
|
482
|
+
raise ValueError(f"The package must be one of {_SUPPORTED_PACKAGES}. Got {package}.")
|
|
483
|
+
|
|
484
|
+
if memory_space is None:
|
|
485
|
+
if package_name == "cupy":
|
|
486
|
+
memory_space = "cuda"
|
|
487
|
+
elif package_name == "numpy":
|
|
488
|
+
memory_space = "cpu"
|
|
489
|
+
else:
|
|
490
|
+
raise ValueError(f"You must provide memory_space for package {package}")
|
|
491
|
+
|
|
492
|
+
if memory_space not in ("cuda", "cpu"):
|
|
493
|
+
raise ValueError(f"memory_space must be 'cuda' or 'cpu'. Got {memory_space}")
|
|
494
|
+
|
|
495
|
+
if (package_name == "cupy" and memory_space == "cpu") or (package_name == "numpy" and memory_space == "cuda"):
|
|
496
|
+
raise ValueError(f"'{memory_space}' memory space is not compatible with package {package_name}")
|
|
497
|
+
|
|
498
|
+
distributed_ctx = nvmath.distributed.get_context()
|
|
499
|
+
if distributed_ctx is None:
|
|
500
|
+
raise RuntimeError(
|
|
501
|
+
"nvmath.distributed has not been initialized. Refer to "
|
|
502
|
+
"https://docs.nvidia.com/cuda/nvmath-python/latest/distributed-apis/runtime.html"
|
|
503
|
+
" for more information."
|
|
504
|
+
)
|
|
505
|
+
if not distributed_ctx.nvshmem_available:
|
|
506
|
+
raise RuntimeError("nvmath.distributed wasn't initialized with NVSHMEM backend")
|
|
507
|
+
process_group = distributed_ctx.process_group
|
|
508
|
+
rank = process_group.rank
|
|
509
|
+
nranks = process_group.nranks
|
|
510
|
+
|
|
511
|
+
if package_name in ("numpy", "cupy"):
|
|
512
|
+
if input_dtype is None:
|
|
513
|
+
# This mimics numpy and cupy
|
|
514
|
+
input_dtype = np.float64
|
|
515
|
+
|
|
516
|
+
input_dtype_name = np.dtype(input_dtype).name
|
|
517
|
+
elif package_name == "torch":
|
|
518
|
+
if input_dtype is None:
|
|
519
|
+
import torch
|
|
520
|
+
|
|
521
|
+
input_dtype = torch.get_default_dtype()
|
|
522
|
+
|
|
523
|
+
input_dtype_name = str(input_dtype).split(".")[-1]
|
|
524
|
+
|
|
525
|
+
package_name = cast(Literal["numpy", "cupy", "torch"], package_name)
|
|
526
|
+
|
|
527
|
+
if isinstance(distribution, Distribution):
|
|
528
|
+
distribution = distribution.to(Slab, ndim=len(shape), copy=True)
|
|
529
|
+
distribution = cast(Slab, distribution)
|
|
530
|
+
else:
|
|
531
|
+
# Must be a Box pair (this is checked in the ProblemSpec reducer).
|
|
532
|
+
distribution = tuple(cast(Box, box.copy()) for box in distribution)
|
|
533
|
+
|
|
534
|
+
options = FFTOptions(fft_type=fft_type)
|
|
535
|
+
problem_spec = _ProblemSpec(
|
|
536
|
+
distribution=distribution,
|
|
537
|
+
shape=list(shape),
|
|
538
|
+
is_C=True,
|
|
539
|
+
operand_dtype=input_dtype_name,
|
|
540
|
+
options=_ProblemSpec.Options(options),
|
|
541
|
+
package=package_name,
|
|
542
|
+
memory_space=memory_space,
|
|
543
|
+
global_size=math.prod(shape),
|
|
544
|
+
rank=rank,
|
|
545
|
+
)
|
|
546
|
+
if nranks > 1:
|
|
547
|
+
problem_spec = process_group.allreduce_object(problem_spec, op=_problem_spec_reducer)
|
|
548
|
+
else:
|
|
549
|
+
# This ensures error-checking with one rank.
|
|
550
|
+
problem_spec = _problem_spec_reducer(problem_spec, problem_spec)
|
|
551
|
+
if isinstance(problem_spec, Exception):
|
|
552
|
+
# There is an error or inconsistency in the problem spec across processes.
|
|
553
|
+
# Note that since this comes from an allreduce, all processes will have
|
|
554
|
+
# received the same exception.
|
|
555
|
+
raise problem_spec
|
|
556
|
+
|
|
557
|
+
fft_type = _get_default_fft_abstract_type(input_dtype_name, fft_type)
|
|
558
|
+
if (fft_type == "R2C" and "float" not in input_dtype_name) or (
|
|
559
|
+
fft_type in ("C2C", "C2R") and "complex" not in input_dtype_name
|
|
560
|
+
):
|
|
561
|
+
raise ValueError(f"input dtype {input_dtype_name} is not compatible with FFT type {fft_type}")
|
|
562
|
+
|
|
563
|
+
logger = logger if logger is not None else logging.getLogger()
|
|
564
|
+
logger.info(
|
|
565
|
+
f"Allocating {package.__name__} operand with shape {shape} and dtype "
|
|
566
|
+
f"{input_dtype_name} for FFT type {fft_type} on {memory_space}, with "
|
|
567
|
+
f"distribution {distribution}."
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
# Infer global shape.
|
|
571
|
+
if isinstance(distribution, Slab):
|
|
572
|
+
global_shape = cast(Slab, problem_spec.distribution)._global_shape_reduction_epilogue()
|
|
573
|
+
else:
|
|
574
|
+
global_shape = cast(Sequence[Box], problem_spec.distribution)[0]._global_shape_reduction_epilogue()
|
|
575
|
+
|
|
576
|
+
# Calculate max capacity for this transform.
|
|
577
|
+
capacity = _calculate_capacity(problem_spec, global_shape, fft_type, nranks)
|
|
578
|
+
|
|
579
|
+
return _allocate_for_fft(
|
|
580
|
+
global_shape, shape, distribution, input_dtype, memory_space, package, fft_type, capacity, rank, nranks
|
|
581
|
+
).tensor
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def _get_view(
|
|
585
|
+
array,
|
|
586
|
+
desired_shape: Sequence[int],
|
|
587
|
+
desired_dtype: str,
|
|
588
|
+
process_group,
|
|
589
|
+
collective_error_checking: bool,
|
|
590
|
+
):
|
|
591
|
+
"""Returns view of the array of the desired shape and dtype. If the given array doesn't
|
|
592
|
+
have the same dtype and number of elements, tries to return a view from the base array
|
|
593
|
+
(original array that owns the memory), where elements are taken from contiguous memory
|
|
594
|
+
starting from the beginning of the buffer."""
|
|
595
|
+
error = None
|
|
596
|
+
desired_size = math.prod(desired_shape) # number of elements
|
|
597
|
+
rank = process_group.rank
|
|
598
|
+
try:
|
|
599
|
+
if array.dtype == desired_dtype and array.size == desired_size:
|
|
600
|
+
if tuple(array.shape) != tuple(desired_shape):
|
|
601
|
+
result = array.reshape(desired_shape, copy=False)
|
|
602
|
+
else:
|
|
603
|
+
result = array
|
|
604
|
+
else:
|
|
605
|
+
|
|
606
|
+
def error_msg(base):
|
|
607
|
+
return (
|
|
608
|
+
f"[{rank}] Internal error: tensor doesn't have a base array large enough "
|
|
609
|
+
"for the required shape and dtype: base array shape and dtype is "
|
|
610
|
+
f"({base.shape}, {base.dtype}), desired shape and dtype is "
|
|
611
|
+
f"({desired_shape}, {desired_dtype}). Consider allocating the operand "
|
|
612
|
+
"for this FFT with nvmath.distributed.fft.allocate_operand()"
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
try:
|
|
616
|
+
base = array.tensor.base
|
|
617
|
+
except AttributeError:
|
|
618
|
+
base = array.tensor._base
|
|
619
|
+
|
|
620
|
+
if base is None:
|
|
621
|
+
base = array.tensor
|
|
622
|
+
|
|
623
|
+
dtype = array.name_to_dtype[desired_dtype]
|
|
624
|
+
itemsize = NAME_TO_ITEM_SIZE[desired_dtype]
|
|
625
|
+
nbytes_required = desired_size * itemsize
|
|
626
|
+
|
|
627
|
+
if array.name == "cuda":
|
|
628
|
+
layout = base.layout
|
|
629
|
+
assert layout.is_contiguous_c
|
|
630
|
+
if layout.memory_range_size_in_bytes < nbytes_required:
|
|
631
|
+
raise RuntimeError(error_msg(base))
|
|
632
|
+
elif base.nbytes < nbytes_required: # type: ignore
|
|
633
|
+
# Note: if this error occurs, it can easily happen on one process
|
|
634
|
+
# but not others.
|
|
635
|
+
raise RuntimeError(error_msg(base))
|
|
636
|
+
|
|
637
|
+
if len(base.shape) > 1:
|
|
638
|
+
# Flatten the base array.
|
|
639
|
+
base = base.reshape(-1) # type: ignore
|
|
640
|
+
|
|
641
|
+
v = base.view(dtype)[:desired_size] # type: ignore
|
|
642
|
+
result = array.__class__(v).reshape(desired_shape, copy=False)
|
|
643
|
+
except Exception as e:
|
|
644
|
+
error = e
|
|
645
|
+
|
|
646
|
+
if collective_error_checking:
|
|
647
|
+
error = process_group.allreduce_object(error, op=_reduce_exception)
|
|
648
|
+
if error:
|
|
649
|
+
raise error
|
|
650
|
+
|
|
651
|
+
return result
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _alloc_and_copy_to_exespace_mirror(
|
|
655
|
+
user_operand: DistributedTensor,
|
|
656
|
+
stream_holder: StreamHolder,
|
|
657
|
+
fft_abstract_type: Literal["C2C", "C2R", "R2C"],
|
|
658
|
+
global_shape: Sequence[int],
|
|
659
|
+
distribution: Slab | Sequence[Box],
|
|
660
|
+
capacity: int,
|
|
661
|
+
rank: int,
|
|
662
|
+
nranks: int,
|
|
663
|
+
) -> DistributedTensor:
|
|
664
|
+
"""
|
|
665
|
+
Allocate a CUDA symmetric-memory buffer, copy the CPU operand into it,
|
|
666
|
+
and return a reference to the new device-side :class:`DistributedTensor`.
|
|
667
|
+
"""
|
|
668
|
+
if user_operand.name == "numpy":
|
|
669
|
+
package: ModuleType = ndbuffer
|
|
670
|
+
dtype = user_operand.dtype
|
|
671
|
+
else:
|
|
672
|
+
import torch as package
|
|
673
|
+
|
|
674
|
+
dtype = user_operand.tensor.dtype
|
|
675
|
+
|
|
676
|
+
exespace_mirror_operand = _allocate_for_fft(
|
|
677
|
+
global_shape,
|
|
678
|
+
user_operand.shape,
|
|
679
|
+
distribution,
|
|
680
|
+
dtype,
|
|
681
|
+
"cuda",
|
|
682
|
+
package,
|
|
683
|
+
fft_abstract_type,
|
|
684
|
+
capacity,
|
|
685
|
+
rank,
|
|
686
|
+
nranks,
|
|
687
|
+
)
|
|
688
|
+
exespace_mirror_operand.copy_(user_operand, stream_holder)
|
|
689
|
+
return exespace_mirror_operand
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def _alloc_and_copy_to_exespace_mirror_or_identity(
|
|
693
|
+
user_operand: DistributedTensor,
|
|
694
|
+
stream_holder: StreamHolder,
|
|
695
|
+
execution_space: Literal["cuda", "cpu"],
|
|
696
|
+
memory_space: Literal["cuda", "cpu"],
|
|
697
|
+
device_id: int | Literal["cpu"],
|
|
698
|
+
fft_abstract_type: Literal["C2C", "C2R", "R2C"],
|
|
699
|
+
global_shape: Sequence[int],
|
|
700
|
+
distribution: Slab | Sequence[Box],
|
|
701
|
+
capacity: int,
|
|
702
|
+
rank: int,
|
|
703
|
+
nranks: int,
|
|
704
|
+
) -> tuple[DistributedTensor, DistributedTensor | None]:
|
|
705
|
+
"""
|
|
706
|
+
Allocate the exespace mirror for the given execution/memory space, or
|
|
707
|
+
return the user operand as-is (identity) when no mirror is needed.
|
|
708
|
+
|
|
709
|
+
Used during construction and after release_operand() when the exespace
|
|
710
|
+
mirror needs to be created from scratch.
|
|
711
|
+
|
|
712
|
+
Returns ``(operand, operand_backup)`` where:
|
|
713
|
+
- Same-space: ``operand`` is the user operand itself, ``operand_backup``
|
|
714
|
+
is ``None`` (no mirror needed).
|
|
715
|
+
- Cross-space: ``operand`` is the newly allocated device-side mirror,
|
|
716
|
+
``operand_backup`` is the original user operand (for result copy-back).
|
|
717
|
+
"""
|
|
718
|
+
if execution_space == memory_space:
|
|
719
|
+
return user_operand, None
|
|
720
|
+
assert execution_space == "cuda"
|
|
721
|
+
exespace_mirror_operand = _alloc_and_copy_to_exespace_mirror(
|
|
722
|
+
user_operand, stream_holder, fft_abstract_type, global_shape, distribution, capacity, rank, nranks
|
|
723
|
+
)
|
|
724
|
+
# Sanity-check: the allocation above is done on the device specified by the
|
|
725
|
+
# distributed context, which must match the device the FFT object expects.
|
|
726
|
+
assert exespace_mirror_operand.device_id == device_id
|
|
727
|
+
return exespace_mirror_operand, user_operand
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def _copy_to_exespace_mirror_or_identity(
|
|
731
|
+
exespace_mirror_operand: DistributedTensor,
|
|
732
|
+
new_operand: DistributedTensor,
|
|
733
|
+
stream_holder: StreamHolder,
|
|
734
|
+
execution_space: Literal["cuda", "cpu"],
|
|
735
|
+
memory_space: Literal["cuda", "cpu"],
|
|
736
|
+
) -> tuple[DistributedTensor, DistributedTensor | None]:
|
|
737
|
+
"""
|
|
738
|
+
Copy new operand data into an existing exespace mirror, or return the
|
|
739
|
+
new operand as-is (identity) when no mirror is needed.
|
|
740
|
+
|
|
741
|
+
Used by reset_operand() when the operand has NOT been released, so the
|
|
742
|
+
exespace mirror is still valid and can be reused via in-place copy.
|
|
743
|
+
|
|
744
|
+
Returns ``(operand, operand_backup)`` where:
|
|
745
|
+
- Same-space: ``operand`` is the new user operand, ``operand_backup``
|
|
746
|
+
is ``None`` (no mirror needed).
|
|
747
|
+
- Cross-space: ``operand`` is the existing mirror (updated in-place),
|
|
748
|
+
``operand_backup`` is the new user operand (for result copy-back).
|
|
749
|
+
"""
|
|
750
|
+
if execution_space == memory_space:
|
|
751
|
+
return new_operand, None
|
|
752
|
+
else:
|
|
753
|
+
exespace_mirror_operand.copy_(new_operand, stream_holder=stream_holder)
|
|
754
|
+
return exespace_mirror_operand, new_operand
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def _problem_spec_reducer(p1: _ProblemSpec, p2: _ProblemSpec):
|
|
758
|
+
try:
|
|
759
|
+
if isinstance(p1, Exception):
|
|
760
|
+
return p1 # propagate exception
|
|
761
|
+
|
|
762
|
+
if isinstance(p2, Exception):
|
|
763
|
+
return p2 # propagate exception
|
|
764
|
+
|
|
765
|
+
if len(p1.shape) != len(p2.shape):
|
|
766
|
+
return ValueError("The number of dimensions of the input operand is inconsistent across processes")
|
|
767
|
+
|
|
768
|
+
# Check if rank is 2-D or 3-D.
|
|
769
|
+
if len(p1.shape) not in (2, 3):
|
|
770
|
+
return ValueError(
|
|
771
|
+
"Distributed FFT is currently supported only for 2-D and 3-D tensors."
|
|
772
|
+
f" The number of dimensions of the operand is {len(p1.shape)}."
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
if p1.operand_dtype != p2.operand_dtype:
|
|
776
|
+
return ValueError("The operand dtype is inconsistent across processes")
|
|
777
|
+
|
|
778
|
+
if p1.package != p2.package:
|
|
779
|
+
return ValueError("operand doesn't belong to the same package on all processes")
|
|
780
|
+
|
|
781
|
+
if p1.memory_space != p2.memory_space:
|
|
782
|
+
return ValueError('operand is not on the same memory space ("cpu", "cuda") on all processes')
|
|
783
|
+
|
|
784
|
+
if p1.options != p2.options:
|
|
785
|
+
return ValueError(f"options are inconsistent across processes: {p1.options} != {p2.options}")
|
|
786
|
+
|
|
787
|
+
# Determine the memory layout shared by all processes.
|
|
788
|
+
p1.is_C &= p2.is_C
|
|
789
|
+
if not p1.is_C:
|
|
790
|
+
return ValueError("The input memory layout is not C on every process")
|
|
791
|
+
|
|
792
|
+
is_box_1 = not isinstance(p1.distribution, Slab)
|
|
793
|
+
is_box_2 = not isinstance(p2.distribution, Slab)
|
|
794
|
+
if is_box_1 != is_box_2:
|
|
795
|
+
return ValueError("distribution must be either Slab or box on all processes, not a mix of both")
|
|
796
|
+
|
|
797
|
+
fft_abstract_type = _get_default_fft_abstract_type(p1.operand_dtype, p1.options.fft_type)
|
|
798
|
+
|
|
799
|
+
if len(p1.shape) == 2 and not is_box_1:
|
|
800
|
+
if fft_abstract_type == "R2C" and p1.distribution != Slab.X:
|
|
801
|
+
return ValueError("2D FFT R2C only supports X-slab input")
|
|
802
|
+
elif fft_abstract_type == "C2R" and p1.distribution != Slab.Y:
|
|
803
|
+
return ValueError("2D FFT C2R only supports Y-slab input")
|
|
804
|
+
|
|
805
|
+
for p_spec in (p1, p2):
|
|
806
|
+
if p_spec.is_leaf:
|
|
807
|
+
if not is_box_1:
|
|
808
|
+
# Using cuFFTMp slab distribution.
|
|
809
|
+
slab = cast(Slab, p_spec.distribution)
|
|
810
|
+
if slab.partition_dim not in (0, 1):
|
|
811
|
+
return ValueError("The Slab partition dimension must be X or Y")
|
|
812
|
+
|
|
813
|
+
slab._global_shape_reduction_prologue(p_spec.rank, p_spec.shape)
|
|
814
|
+
else:
|
|
815
|
+
# Custom distribution given by input and output boxes on each process.
|
|
816
|
+
if not isinstance(p_spec.distribution, Sequence) or not all(
|
|
817
|
+
isinstance(d, Box) for d in p_spec.distribution
|
|
818
|
+
):
|
|
819
|
+
return ValueError("distribution must be a Slab or a Box pair")
|
|
820
|
+
|
|
821
|
+
if len(p_spec.distribution) != 2: # type: ignore
|
|
822
|
+
return ValueError("Must provide a Box pair on every process")
|
|
823
|
+
|
|
824
|
+
input_box, output_box = cast(Sequence[Box], p_spec.distribution)
|
|
825
|
+
|
|
826
|
+
p_spec.input_max_elements = math.prod(p_spec.shape)
|
|
827
|
+
input_box._global_shape_reduction_prologue(p_spec.rank, p_spec.shape)
|
|
828
|
+
|
|
829
|
+
output_box_shape = output_box.box_shape
|
|
830
|
+
p_spec.output_max_elements = math.prod(output_box_shape)
|
|
831
|
+
output_box._global_shape_reduction_prologue(p_spec.rank, output_box_shape)
|
|
832
|
+
|
|
833
|
+
if not is_box_1:
|
|
834
|
+
cast(Slab, p1.distribution)._global_shape_reduction(cast(Slab, p2.distribution))
|
|
835
|
+
else:
|
|
836
|
+
# Reduce input boxes.
|
|
837
|
+
p1_boxes = cast(Sequence[Box], p1.distribution)
|
|
838
|
+
p2_boxes = cast(Sequence[Box], p2.distribution)
|
|
839
|
+
p1_boxes[0]._global_shape_reduction(p2_boxes[0])
|
|
840
|
+
# Reduce output boxes.
|
|
841
|
+
p1_boxes[1]._global_shape_reduction(p2_boxes[1])
|
|
842
|
+
|
|
843
|
+
if p1 is not p2: # with nranks=1 p1 is p2
|
|
844
|
+
p1.global_size += p2.global_size
|
|
845
|
+
|
|
846
|
+
p1.input_max_elements = max(p1.input_max_elements, p2.input_max_elements)
|
|
847
|
+
p1.output_max_elements = max(p1.output_max_elements, p2.output_max_elements)
|
|
848
|
+
|
|
849
|
+
except Exception as e:
|
|
850
|
+
return e
|
|
851
|
+
p1.is_leaf = False
|
|
852
|
+
return p1
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
def _reduce_exception(e1, e2):
|
|
856
|
+
if e1 is not None:
|
|
857
|
+
return e1
|
|
858
|
+
return e2
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
class InvalidFFTState(Exception):
|
|
862
|
+
pass
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
@utils.docstring_decorator(SHARED_FFT_DOCUMENTATION, skip_missing=False)
|
|
866
|
+
class FFT:
|
|
867
|
+
"""
|
|
868
|
+
Create a stateful object that encapsulates the specified distributed FFT computations
|
|
869
|
+
and required resources. This object ensures the validity of resources during use and
|
|
870
|
+
releases them when they are no longer needed to prevent misuse.
|
|
871
|
+
|
|
872
|
+
This object encompasses all functionalities of function-form APIs :func:`fft`,
|
|
873
|
+
:func:`ifft`, :func:`rfft`, and :func:`irfft`, which are convenience wrappers around it.
|
|
874
|
+
The stateful object also allows for the amortization of preparatory costs when the same
|
|
875
|
+
FFT operation is to be performed on multiple operands with the same problem
|
|
876
|
+
specification (see :meth:`reset_operand` for more details).
|
|
877
|
+
|
|
878
|
+
Using the stateful object typically involves the following steps:
|
|
879
|
+
|
|
880
|
+
1. **Problem Specification**: Initialize the object with a defined operation and
|
|
881
|
+
options.
|
|
882
|
+
2. **Preparation**: Use :meth:`plan` to determine the best algorithmic implementation
|
|
883
|
+
for this specific FFT operation.
|
|
884
|
+
3. **Execution**: Perform the FFT computation with :meth:`execute`, which can be either
|
|
885
|
+
forward or inverse FFT transformation.
|
|
886
|
+
4. **Resource Management**: Ensure all resources are released either by explicitly
|
|
887
|
+
calling :meth:`free` or by managing the stateful object within a context manager.
|
|
888
|
+
|
|
889
|
+
Detailed information on each step described above can be obtained by passing in a
|
|
890
|
+
:class:`logging.Logger` object to :class:`FFTOptions` or by setting the appropriate
|
|
891
|
+
options in the root logger object, which is used by default:
|
|
892
|
+
|
|
893
|
+
>>> import logging
|
|
894
|
+
>>> logging.basicConfig(
|
|
895
|
+
... level=logging.INFO,
|
|
896
|
+
... format="%(asctime)s %(levelname)-8s %(message)s",
|
|
897
|
+
... datefmt="%m-%d %H:%M:%S",
|
|
898
|
+
... )
|
|
899
|
+
|
|
900
|
+
.. versionchanged:: 0.9.0
|
|
901
|
+
The `operand` parameter is now positional-only.
|
|
902
|
+
|
|
903
|
+
Args:
|
|
904
|
+
operand: {operand}
|
|
905
|
+
{operand_admonitions}
|
|
906
|
+
|
|
907
|
+
distribution: {distribution}
|
|
908
|
+
|
|
909
|
+
options: {options}
|
|
910
|
+
|
|
911
|
+
stream: {stream}
|
|
912
|
+
|
|
913
|
+
.. seealso::
|
|
914
|
+
:meth:`plan`, :meth:`reset_operand`, :meth:`execute`,
|
|
915
|
+
:meth:`release_operand`
|
|
916
|
+
|
|
917
|
+
Examples:
|
|
918
|
+
|
|
919
|
+
>>> import cupy as cp
|
|
920
|
+
>>> import nvmath.distributed
|
|
921
|
+
|
|
922
|
+
Get process group used to initialize nvmath.distributed (for information on
|
|
923
|
+
initializing nvmath.distributed, you can refer to the documentation or to the
|
|
924
|
+
FFT examples in `nvmath/examples/distributed/fft
|
|
925
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/fft>`_):
|
|
926
|
+
|
|
927
|
+
>>> process_group = nvmath.distributed.get_context().process_group
|
|
928
|
+
|
|
929
|
+
Get the number of processes:
|
|
930
|
+
|
|
931
|
+
>>> nranks = process_group.nranks
|
|
932
|
+
|
|
933
|
+
Create a 3-D complex128 ndarray on GPU symmetric memory, distributed according to
|
|
934
|
+
the Slab distribution on the X axis (the global shape is (128, 128, 128)):
|
|
935
|
+
|
|
936
|
+
>>> from nvmath.distributed.distribution import Slab
|
|
937
|
+
>>> shape = 128 // nranks, 128, 128
|
|
938
|
+
|
|
939
|
+
cuFFTMp uses the NVSHMEM PGAS model for distributed computation, which requires GPU
|
|
940
|
+
operands to be on the symmetric heap:
|
|
941
|
+
|
|
942
|
+
>>> a = nvmath.distributed.allocate_symmetric_memory(shape, cp, dtype=cp.complex128)
|
|
943
|
+
|
|
944
|
+
After allocating, we initialize the CuPy ndarray's memory:
|
|
945
|
+
|
|
946
|
+
>>> a[:] = cp.random.rand(*shape) + 1j * cp.random.rand(*shape)
|
|
947
|
+
|
|
948
|
+
We will define a 3-D C2C FFT operation, creating an FFT object encapsulating the
|
|
949
|
+
above problem specification. Each process provides their own local operand (which
|
|
950
|
+
is part of the PGAS space, but otherwise can be operated on as any other CuPy
|
|
951
|
+
ndarray for local operations) and specifies how the operand is distributed across
|
|
952
|
+
processes:
|
|
953
|
+
|
|
954
|
+
>>> f = nvmath.distributed.fft.FFT(a, distribution=Slab.X)
|
|
955
|
+
|
|
956
|
+
More information on distribution of operands can be found in the documentation:
|
|
957
|
+
https://docs.nvidia.com/cuda/nvmath-python/latest/distributed-apis/fft/index.html
|
|
958
|
+
|
|
959
|
+
Options can be provided above to control the behavior of the operation using the
|
|
960
|
+
`options` argument (see :class:`FFTOptions`).
|
|
961
|
+
|
|
962
|
+
Next, plan the FFT:
|
|
963
|
+
|
|
964
|
+
>>> f.plan()
|
|
965
|
+
|
|
966
|
+
Now execute the FFT, and obtain the result `r1` as a CuPy ndarray. Note that
|
|
967
|
+
distributed FFT computations are inplace, so operands a and r1 share the same
|
|
968
|
+
symmetric memory buffer:
|
|
969
|
+
|
|
970
|
+
>>> r1 = f.execute()
|
|
971
|
+
|
|
972
|
+
Finally, free the FFT object's resources. To avoid this explicit call, it's
|
|
973
|
+
recommended to use the FFT object as a context manager as shown below, if possible.
|
|
974
|
+
|
|
975
|
+
>>> f.free()
|
|
976
|
+
|
|
977
|
+
Any symmetric memory that is owned by the user must be deleted explicitly (this is
|
|
978
|
+
a collective call and must be called by all processes). Note that because operands
|
|
979
|
+
a and r1 share the same buffer, only one of them must be freed:
|
|
980
|
+
|
|
981
|
+
>>> nvmath.distributed.free_symmetric_memory(a)
|
|
982
|
+
|
|
983
|
+
Note that all :class:`FFT` methods execute on the current stream by default.
|
|
984
|
+
Alternatively, the `stream` argument can be used to run a method on a specified
|
|
985
|
+
stream.
|
|
986
|
+
|
|
987
|
+
Let's now look at the same problem with NumPy ndarrays on the CPU.
|
|
988
|
+
|
|
989
|
+
Create a 3-D complex128 NumPy ndarray on the CPU:
|
|
990
|
+
|
|
991
|
+
>>> import numpy as np
|
|
992
|
+
>>> shape = 128 // nranks, 128, 128
|
|
993
|
+
>>> a = np.random.rand(*shape) + 1j * np.random.rand(*shape)
|
|
994
|
+
|
|
995
|
+
Create an FFT object encapsulating the problem specification described earlier and
|
|
996
|
+
use it as a context manager.
|
|
997
|
+
|
|
998
|
+
>>> with nvmath.distributed.fft.FFT(a, distribution=Slab.X) as f:
|
|
999
|
+
... f.plan()
|
|
1000
|
+
...
|
|
1001
|
+
... # Execute the FFT to get the first result.
|
|
1002
|
+
... r1 = f.execute()
|
|
1003
|
+
|
|
1004
|
+
All the resources used by the object are released at the end of the block.
|
|
1005
|
+
|
|
1006
|
+
The operation was performed on the GPU, with the NumPy array temporarily copied to
|
|
1007
|
+
GPU symmetric memory and transformed on the GPU.
|
|
1008
|
+
|
|
1009
|
+
Further examples can be found in the `nvmath/examples/distributed/fft
|
|
1010
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/fft>`_
|
|
1011
|
+
directory.
|
|
1012
|
+
"""
|
|
1013
|
+
|
|
1014
|
+
def _free_internal_sheap(self, exception: Exception | None = None) -> bool:
|
|
1015
|
+
# This is a fail-safe to free NVSHMEM internal memory in case of invalid
|
|
1016
|
+
# state (FFT constructor fails). Since we might call nvshmem_free here, we're
|
|
1017
|
+
# assuming that all processes equally failed in the ctor, which might not be true,
|
|
1018
|
+
# but if it weren't true they would end up in deadlock most likely anyway.
|
|
1019
|
+
if (
|
|
1020
|
+
hasattr(self, "memory_space")
|
|
1021
|
+
and self.memory_space == "cpu"
|
|
1022
|
+
and self.operand is not None
|
|
1023
|
+
and self.operand.device == "cuda"
|
|
1024
|
+
):
|
|
1025
|
+
with utils.device_ctx(self.device_id):
|
|
1026
|
+
self.operand.free_symmetric()
|
|
1027
|
+
return True
|
|
1028
|
+
|
|
1029
|
+
@utils.atomic(_free_internal_sheap, method=True)
|
|
1030
|
+
def __init__(
|
|
1031
|
+
self,
|
|
1032
|
+
operand,
|
|
1033
|
+
/,
|
|
1034
|
+
*,
|
|
1035
|
+
distribution: Distribution | Sequence[Box],
|
|
1036
|
+
options: FFTOptions | dict[str, Any] | None = None,
|
|
1037
|
+
stream: AnyStream | None = None,
|
|
1038
|
+
):
|
|
1039
|
+
distributed_ctx = nvmath.distributed.get_context()
|
|
1040
|
+
if distributed_ctx is None:
|
|
1041
|
+
raise RuntimeError(
|
|
1042
|
+
"nvmath.distributed has not been initialized. Refer to "
|
|
1043
|
+
"https://docs.nvidia.com/cuda/nvmath-python/latest/distributed-apis/runtime.html"
|
|
1044
|
+
" for more information."
|
|
1045
|
+
)
|
|
1046
|
+
if not distributed_ctx.nvshmem_available:
|
|
1047
|
+
raise RuntimeError("nvmath.distributed wasn't initialized with NVSHMEM backend")
|
|
1048
|
+
self.process_group = process_group = distributed_ctx.process_group
|
|
1049
|
+
self.rank = rank = process_group.rank
|
|
1050
|
+
self.nranks = nranks = process_group.nranks
|
|
1051
|
+
|
|
1052
|
+
self.operand = operand = tensor_wrapper.wrap_operand(operand)
|
|
1053
|
+
self.options = options = cast(FFTOptions, utils.check_or_create_options(FFTOptions, options, "Distributed FFT options"))
|
|
1054
|
+
self.package = operand.name
|
|
1055
|
+
|
|
1056
|
+
if isinstance(distribution, Distribution):
|
|
1057
|
+
distribution = distribution.to(Slab, ndim=len(operand.shape), copy=True)
|
|
1058
|
+
distribution = cast(Slab, distribution)
|
|
1059
|
+
else:
|
|
1060
|
+
# Must be a Box pair (this is checked in the ProblemSpec reducer).
|
|
1061
|
+
distribution = tuple(cast(Box, box.copy()) for box in distribution)
|
|
1062
|
+
|
|
1063
|
+
is_C = sorted(operand.strides, reverse=True) == list(operand.strides)
|
|
1064
|
+
|
|
1065
|
+
# Merge the problem specification across processes to make sure that there are no
|
|
1066
|
+
# inconsistencies and to calculate the global shape. Importantly, this also does
|
|
1067
|
+
# collective error checking of the FFT input parameters, to ensure that all
|
|
1068
|
+
# processes fail on error of any one process, thus preventing deadlock.
|
|
1069
|
+
problem_spec = _ProblemSpec(
|
|
1070
|
+
distribution=distribution,
|
|
1071
|
+
shape=list(operand.shape),
|
|
1072
|
+
is_C=is_C,
|
|
1073
|
+
operand_dtype=operand.dtype,
|
|
1074
|
+
options=_ProblemSpec.Options(options),
|
|
1075
|
+
package=self.package,
|
|
1076
|
+
memory_space=operand.device,
|
|
1077
|
+
global_size=math.prod(operand.shape),
|
|
1078
|
+
rank=rank,
|
|
1079
|
+
)
|
|
1080
|
+
if nranks > 1:
|
|
1081
|
+
problem_spec = process_group.allreduce_object(problem_spec, op=_problem_spec_reducer)
|
|
1082
|
+
else:
|
|
1083
|
+
# Ensure we error-check with one rank.
|
|
1084
|
+
problem_spec = _problem_spec_reducer(problem_spec, problem_spec)
|
|
1085
|
+
if isinstance(problem_spec, Exception):
|
|
1086
|
+
# There is an error or inconsistency in the problem spec across processes.
|
|
1087
|
+
# Note that since this comes from an allreduce, all processes will have
|
|
1088
|
+
# received the same exception.
|
|
1089
|
+
raise problem_spec
|
|
1090
|
+
|
|
1091
|
+
self.operand_dim = len(operand.shape)
|
|
1092
|
+
|
|
1093
|
+
self.operand_data_type = operand.dtype
|
|
1094
|
+
self.fft_abstract_type = _get_default_fft_abstract_type(self.operand_data_type, options.fft_type)
|
|
1095
|
+
|
|
1096
|
+
self.result_data_type, self.compute_data_type = _get_fft_result_and_compute_types(operand.dtype, self.fft_abstract_type)
|
|
1097
|
+
|
|
1098
|
+
self.logger = options.logger if options.logger is not None else logging.getLogger()
|
|
1099
|
+
self.logger.info(f"The FFT type is {self.fft_abstract_type}.")
|
|
1100
|
+
self.logger.info(
|
|
1101
|
+
f"The input data type is {self.operand_data_type}, and the result data type is {self.result_data_type}."
|
|
1102
|
+
)
|
|
1103
|
+
|
|
1104
|
+
# cuFFTMp doesn't support complex32.
|
|
1105
|
+
if self.result_data_type == "complex32":
|
|
1106
|
+
raise TypeError(f"The result data type {self.result_data_type} is not supported.")
|
|
1107
|
+
|
|
1108
|
+
# Infer operand package, execution space, and memory space.
|
|
1109
|
+
execution_device_id: int = distributed_ctx.device_id
|
|
1110
|
+
if operand.device_id != "cpu": # exec space matches the mem space
|
|
1111
|
+
self.memory_space = "cuda"
|
|
1112
|
+
self.device_id = operand.device_id
|
|
1113
|
+
assert operand.device_id == execution_device_id
|
|
1114
|
+
else: # we need to move inputs cpu -> gpu and outputs gpu -> cpu
|
|
1115
|
+
self.memory_space = "cpu"
|
|
1116
|
+
self.device_id = execution_device_id
|
|
1117
|
+
self.execution_space = "cuda"
|
|
1118
|
+
self.operand_device_id = operand.device_id
|
|
1119
|
+
self.internal_op_package = self._internal_operand_package(self.package)
|
|
1120
|
+
stream_holder: StreamHolder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
|
|
1121
|
+
|
|
1122
|
+
if self.memory_space == "cuda" and not operand.is_symmetric_memory:
|
|
1123
|
+
raise TypeError("Distributed FFT requires GPU operand to be on symmetric memory")
|
|
1124
|
+
|
|
1125
|
+
self.logger.info(
|
|
1126
|
+
f"The input tensor's memory space is {self.memory_space}, and the execution space "
|
|
1127
|
+
f"is {self.execution_space}, with device {self.device_id}."
|
|
1128
|
+
)
|
|
1129
|
+
|
|
1130
|
+
self.logger.info(f"The specified stream for the FFT ctor is {stream_holder and stream_holder.obj}")
|
|
1131
|
+
|
|
1132
|
+
# Infer the global extents.
|
|
1133
|
+
if isinstance(distribution, Slab):
|
|
1134
|
+
self.global_extents = cast(Slab, problem_spec.distribution)._global_shape_reduction_epilogue()
|
|
1135
|
+
# Check that this process has the correct slab shape.
|
|
1136
|
+
error = None
|
|
1137
|
+
try:
|
|
1138
|
+
distribution._bind(self.global_extents, shape=self.operand.shape)
|
|
1139
|
+
except Exception as e:
|
|
1140
|
+
error = e
|
|
1141
|
+
error = process_group.allreduce_object(error, op=_reduce_exception)
|
|
1142
|
+
if error:
|
|
1143
|
+
raise error
|
|
1144
|
+
else:
|
|
1145
|
+
# Infer the global shape from the global input box.
|
|
1146
|
+
self.global_extents = cast(Sequence[Box], problem_spec.distribution)[0]._global_shape_reduction_epilogue()
|
|
1147
|
+
|
|
1148
|
+
# This can't throw error since the local operand shape was already checked
|
|
1149
|
+
# against the box shape in the ProblemSpec reducer.
|
|
1150
|
+
distribution[0]._bind(self.global_extents, shape=self.operand.shape)
|
|
1151
|
+
|
|
1152
|
+
# The global number of elements must be compatible with the global shape.
|
|
1153
|
+
if problem_spec.global_size != math.prod(self.global_extents):
|
|
1154
|
+
raise ValueError(
|
|
1155
|
+
f"The global number of elements is incompatible with the inferred global shape {self.global_extents}"
|
|
1156
|
+
)
|
|
1157
|
+
|
|
1158
|
+
for i in (0, 1):
|
|
1159
|
+
if self.global_extents[i] < nranks:
|
|
1160
|
+
raise ValueError(
|
|
1161
|
+
f"The FFT dimension {i} has global length {self.global_extents[i]} which "
|
|
1162
|
+
f"is smaller than the number of processes ({nranks})"
|
|
1163
|
+
)
|
|
1164
|
+
|
|
1165
|
+
self.logger.info(f"The global FFT extents are {self.global_extents}.")
|
|
1166
|
+
|
|
1167
|
+
# Calculate the required buffer capacity (in number of elements) for this transform.
|
|
1168
|
+
self.capacity: int = _calculate_capacity(problem_spec, self.global_extents, self.fft_abstract_type, nranks)
|
|
1169
|
+
|
|
1170
|
+
# Copy the operand to execution_space's device if needed.
|
|
1171
|
+
self.operand, self.operand_backup = _alloc_and_copy_to_exespace_mirror_or_identity(
|
|
1172
|
+
operand,
|
|
1173
|
+
stream_holder,
|
|
1174
|
+
self.execution_space, # type: ignore[arg-type]
|
|
1175
|
+
self.memory_space, # type: ignore[arg-type]
|
|
1176
|
+
self.device_id,
|
|
1177
|
+
self.fft_abstract_type,
|
|
1178
|
+
self.global_extents,
|
|
1179
|
+
distribution,
|
|
1180
|
+
self.capacity,
|
|
1181
|
+
rank,
|
|
1182
|
+
nranks,
|
|
1183
|
+
)
|
|
1184
|
+
|
|
1185
|
+
# Track whether the user has called release_operand(). This flag is
|
|
1186
|
+
# checked in _check_valid_operand to prevent execution after the user
|
|
1187
|
+
# has released their operand. It is cleared by reset_operand().
|
|
1188
|
+
self._operand_released = False
|
|
1189
|
+
|
|
1190
|
+
operand = self.operand
|
|
1191
|
+
# Capture operand layout for consistency checks when resetting operands.
|
|
1192
|
+
self.operand_layout = TensorLayout(shape=operand.shape, strides=operand.strides)
|
|
1193
|
+
|
|
1194
|
+
self.logger.info("The FFT will be performed in-place, with the result overwriting the input.")
|
|
1195
|
+
|
|
1196
|
+
# The result's package and device.
|
|
1197
|
+
self.result_class: DistributedTensor = operand.__class__
|
|
1198
|
+
|
|
1199
|
+
# Set blocking or non-blocking behavior.
|
|
1200
|
+
self.blocking = self.options.blocking is True or self.memory_space == "cpu"
|
|
1201
|
+
if self.blocking:
|
|
1202
|
+
self.call_prologue = "This call is blocking and will return only after the operation is complete."
|
|
1203
|
+
else:
|
|
1204
|
+
self.call_prologue = (
|
|
1205
|
+
"This call is non-blocking and will return immediately after the operation is launched on the device."
|
|
1206
|
+
)
|
|
1207
|
+
|
|
1208
|
+
if not isinstance(distribution, Slab):
|
|
1209
|
+
# Redistribute only applies to cuFFTMp's default slab distribution.
|
|
1210
|
+
self.options.redistribute = False
|
|
1211
|
+
self.logger.info("Redistribute option is ignored when using box distribution.")
|
|
1212
|
+
|
|
1213
|
+
# Set memory allocator.
|
|
1214
|
+
self.allocator = NvshmemMemoryManager(self.device_id, self.logger)
|
|
1215
|
+
|
|
1216
|
+
self.distribution: Slab | Sequence[Box] = distribution
|
|
1217
|
+
# Map possible distributions to the corresponding operand TensorLayout.
|
|
1218
|
+
self.distribution_layout: dict[Slab | Box, TensorLayout] = {}
|
|
1219
|
+
# The subformat is an identifier that cuFFTMp uses to refer to an operand
|
|
1220
|
+
# distribution. It can be one of:
|
|
1221
|
+
# - cufftMp.XtSubFormat.FORMAT_INPLACE (refers to Slab.X)
|
|
1222
|
+
# - cufftMp.XtSubFormat.FORMAT_INPLACE_SHUFFLED (refers to Slab.Y)
|
|
1223
|
+
# - cufft.XtSubFormat.FORMAT_DISTRIBUTED_INPUT (the input box at FFT plan time)
|
|
1224
|
+
# - cufft.XtSubFormat.FORMAT_DISTRIBUTED_OUTPUT (the output box at FFT plan time)
|
|
1225
|
+
self.subformat: int = -1
|
|
1226
|
+
if isinstance(distribution, Slab):
|
|
1227
|
+
self.distribution_layout[distribution] = self.operand_layout
|
|
1228
|
+
|
|
1229
|
+
if self.options.redistribute:
|
|
1230
|
+
from_axis, to_axis = ("X", "X") if distribution == Slab.X else ("Y", "Y")
|
|
1231
|
+
else:
|
|
1232
|
+
from_axis, to_axis = ("X", "Y") if distribution == Slab.X else ("Y", "X")
|
|
1233
|
+
self.logger.info(
|
|
1234
|
+
f"The operand distribution is Slab, with input partitioned on {from_axis} axis "
|
|
1235
|
+
f"and output on {to_axis} (redistribute={self.options.redistribute})."
|
|
1236
|
+
)
|
|
1237
|
+
else:
|
|
1238
|
+
input_box, output_box = distribution
|
|
1239
|
+
self.distribution_layout[input_box] = self.operand_layout
|
|
1240
|
+
|
|
1241
|
+
self.logger.info(f"The operand distribution is based on custom input box {input_box} and output box {output_box}.")
|
|
1242
|
+
|
|
1243
|
+
# Infer result shape and strides.
|
|
1244
|
+
|
|
1245
|
+
self.global_result_extents = list(self.global_extents)
|
|
1246
|
+
global_result_extents_padded = None
|
|
1247
|
+
if self.fft_abstract_type == "R2C":
|
|
1248
|
+
self.global_result_extents[-1] = self.global_result_extents[-1] // 2 + 1
|
|
1249
|
+
elif self.fft_abstract_type == "C2R":
|
|
1250
|
+
self.global_result_extents[-1] = (self.global_result_extents[-1] - 1) * 2
|
|
1251
|
+
if options.last_axis_parity == "odd":
|
|
1252
|
+
self.global_result_extents[-1] += 1
|
|
1253
|
+
global_result_extents_padded = list(self.global_result_extents)
|
|
1254
|
+
global_result_extents_padded[-1] = self.global_extents[-1] * 2
|
|
1255
|
+
|
|
1256
|
+
if not isinstance(distribution, Slab):
|
|
1257
|
+
global_boxes = cast(Sequence[Box], problem_spec.distribution)
|
|
1258
|
+
lower, upper = global_boxes[1]
|
|
1259
|
+
actual_global_result_extents = tuple(int(upper[i] - lower[i]) for i in range(self.operand_dim))
|
|
1260
|
+
if actual_global_result_extents != tuple(self.global_result_extents):
|
|
1261
|
+
raise ValueError(
|
|
1262
|
+
"The global box derived from the output boxes doesn't have the expected shape: "
|
|
1263
|
+
f"global_input_box={problem_spec.distribution[0]}, global_output_box={problem_spec.distribution[1]}" # type: ignore
|
|
1264
|
+
)
|
|
1265
|
+
|
|
1266
|
+
if self.options.redistribute:
|
|
1267
|
+
partition_dim = distribution.partition_dim # type: ignore
|
|
1268
|
+
if self.fft_abstract_type == "C2R":
|
|
1269
|
+
self.result_shape_padded, _ = _calculate_slab_shape_strides(
|
|
1270
|
+
global_result_extents_padded, partition_dim, rank, nranks
|
|
1271
|
+
)
|
|
1272
|
+
self.result_shape, self.result_strides = _calculate_slab_shape_strides(
|
|
1273
|
+
self.global_result_extents, partition_dim, rank, nranks, global_result_extents_padded
|
|
1274
|
+
)
|
|
1275
|
+
|
|
1276
|
+
# The input of the redistribute is the output of the FFT and will have these
|
|
1277
|
+
# strides. Note the special strides of the C2R output based on the output's
|
|
1278
|
+
# padded last axis.
|
|
1279
|
+
_, self.intermediate_strides = _calculate_slab_shape_strides(
|
|
1280
|
+
self.global_result_extents, 1 - partition_dim, rank, nranks, global_result_extents_padded
|
|
1281
|
+
)
|
|
1282
|
+
elif not isinstance(self.distribution, Slab):
|
|
1283
|
+
output_lower, output_upper = output_box
|
|
1284
|
+
self.result_shape = tuple(output_upper[i] - output_lower[i] for i in range(self.operand_dim))
|
|
1285
|
+
self.result_strides = calculate_strides(self.result_shape, reversed(range(self.operand_dim)))
|
|
1286
|
+
self.distribution_layout[output_box] = TensorLayout(shape=self.result_shape, strides=self.result_strides)
|
|
1287
|
+
output_box._bind(self.global_result_extents, shape=self.result_shape)
|
|
1288
|
+
else:
|
|
1289
|
+
result_partition_dim = 1 - distribution.partition_dim # type: ignore
|
|
1290
|
+
if self.fft_abstract_type == "C2R":
|
|
1291
|
+
self.result_shape_padded, _ = _calculate_slab_shape_strides(
|
|
1292
|
+
global_result_extents_padded, result_partition_dim, rank, nranks
|
|
1293
|
+
)
|
|
1294
|
+
self.result_shape, self.result_strides = _calculate_slab_shape_strides(
|
|
1295
|
+
self.global_result_extents, result_partition_dim, rank, nranks, global_result_extents_padded
|
|
1296
|
+
)
|
|
1297
|
+
self.distribution_layout[Slab.X if distribution == Slab.Y else Slab.Y] = TensorLayout(
|
|
1298
|
+
shape=self.result_shape, strides=self.result_strides
|
|
1299
|
+
)
|
|
1300
|
+
|
|
1301
|
+
# Obtain the result operand (the one that will be returned to the user with the
|
|
1302
|
+
# expected shape and dtype on this rank according to the FFT type and operand
|
|
1303
|
+
# distributions). Note that since the FFT is inplace, the result operand shares
|
|
1304
|
+
# the same buffer with the input operand.
|
|
1305
|
+
self._update_result_view_attributes(collective_error_checking=True)
|
|
1306
|
+
|
|
1307
|
+
# Create handle.
|
|
1308
|
+
with utils.device_ctx(self.device_id):
|
|
1309
|
+
self.handle = cufft.create()
|
|
1310
|
+
# Dummy handle to create a cufft descriptor with initial tiny data buffer.
|
|
1311
|
+
# We'll reuse this descriptor to call cufft.xt_exec_descriptor, by
|
|
1312
|
+
# setting the data pointer and subformat in the descriptor.
|
|
1313
|
+
self.memory_desc_handle = cufft.create()
|
|
1314
|
+
if self.options.redistribute:
|
|
1315
|
+
self.reshape_handle = cufft.create_reshape()
|
|
1316
|
+
|
|
1317
|
+
# Set stream for the FFT.
|
|
1318
|
+
with utils.device_ctx(self.device_id):
|
|
1319
|
+
cufft.set_stream(self.handle, stream_holder.ptr) # type: ignore[union-attr]
|
|
1320
|
+
|
|
1321
|
+
# Plan attributes.
|
|
1322
|
+
cufft.set_auto_allocation(self.handle, 0)
|
|
1323
|
+
|
|
1324
|
+
self.fft_planned = False
|
|
1325
|
+
# Descriptor to call cufft.xt_exec_descriptor (by setting the
|
|
1326
|
+
# data pointer and subformat in the descriptor before execute).
|
|
1327
|
+
self.memory_desc = None
|
|
1328
|
+
# Pointer to tiny data buffer of descriptor when first created.
|
|
1329
|
+
self.dummy_desc_data_ptr = None
|
|
1330
|
+
|
|
1331
|
+
# Workspace attributes.
|
|
1332
|
+
self.workspace_ptr: None | memory.MemoryPointer = None
|
|
1333
|
+
self.workspace_size = 0
|
|
1334
|
+
self._workspace_allocated_here = False
|
|
1335
|
+
self.reshaped_operand = None
|
|
1336
|
+
|
|
1337
|
+
# Attributes to establish stream ordering.
|
|
1338
|
+
self.workspace_stream = None
|
|
1339
|
+
self.last_compute_event = None
|
|
1340
|
+
|
|
1341
|
+
self.valid_state = True
|
|
1342
|
+
self.logger.info("The distributed FFT operation has been created.")
|
|
1343
|
+
|
|
1344
|
+
def __enter__(self):
|
|
1345
|
+
return self
|
|
1346
|
+
|
|
1347
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
1348
|
+
self.free()
|
|
1349
|
+
|
|
1350
|
+
def _check_valid_fft(self, *args, **kwargs):
|
|
1351
|
+
"""
|
|
1352
|
+
Check if FFT object is alive and well.
|
|
1353
|
+
"""
|
|
1354
|
+
if not self.valid_state:
|
|
1355
|
+
raise InvalidFFTState("The FFT object cannot be used after resources are free'd")
|
|
1356
|
+
|
|
1357
|
+
def _free_plan_resources(self, exception: Exception | None = None) -> bool:
|
|
1358
|
+
"""
|
|
1359
|
+
Free resources allocated in planning.
|
|
1360
|
+
"""
|
|
1361
|
+
if self.memory_desc is not None:
|
|
1362
|
+
with utils.device_ctx(self.device_id):
|
|
1363
|
+
cufft.xt_free(self.memory_desc)
|
|
1364
|
+
self.memory_desc = None
|
|
1365
|
+
|
|
1366
|
+
self.fft_planned = False
|
|
1367
|
+
return True
|
|
1368
|
+
|
|
1369
|
+
def _internal_operand_package(self, package_name):
|
|
1370
|
+
if self.execution_space == "cuda":
|
|
1371
|
+
return package_name if package_name != "numpy" else "cuda"
|
|
1372
|
+
else:
|
|
1373
|
+
return package_name if package_name != "cupy" else "cupy_host"
|
|
1374
|
+
|
|
1375
|
+
def _allocate_reshape_operand(self, exec_stream_holder: StreamHolder | None, log_debug):
|
|
1376
|
+
if log_debug:
|
|
1377
|
+
self.logger.debug("Beginning empty tensor creation to hold redistribute value...")
|
|
1378
|
+
self.logger.debug(
|
|
1379
|
+
f"The redistributed tensor shape = {self.result_shape} with strides = "
|
|
1380
|
+
f"{self.result_strides} and data type '{self.result_data_type}'."
|
|
1381
|
+
)
|
|
1382
|
+
|
|
1383
|
+
capacity_out_dtype = (
|
|
1384
|
+
self.capacity * 2
|
|
1385
|
+
if self.fft_abstract_type == "C2R"
|
|
1386
|
+
else self.capacity // 2
|
|
1387
|
+
if self.fft_abstract_type == "R2C"
|
|
1388
|
+
else self.capacity
|
|
1389
|
+
)
|
|
1390
|
+
# For C2R we preserve the last axis strides of the real output
|
|
1391
|
+
# when we reshape.
|
|
1392
|
+
result = _allocate_for_fft(
|
|
1393
|
+
self.global_result_extents,
|
|
1394
|
+
self.result_shape,
|
|
1395
|
+
self.distribution,
|
|
1396
|
+
self.result_operand.name_to_dtype[self.result_data_type],
|
|
1397
|
+
"cuda",
|
|
1398
|
+
self.result_operand.module,
|
|
1399
|
+
self.fft_abstract_type[::-1], # type: ignore
|
|
1400
|
+
capacity_out_dtype,
|
|
1401
|
+
self.rank,
|
|
1402
|
+
self.nranks,
|
|
1403
|
+
)
|
|
1404
|
+
if log_debug:
|
|
1405
|
+
self.logger.debug("The redistributed output (empty) tensor has been created.")
|
|
1406
|
+
return result
|
|
1407
|
+
|
|
1408
|
+
def _get_result_views(self, collective_error_checking):
|
|
1409
|
+
"""Compute result-operand views from the current operand buffer.
|
|
1410
|
+
|
|
1411
|
+
Returns ``(result, cpu_result)`` where *result* is a view of
|
|
1412
|
+
``self.operand`` shaped for the FFT output and *cpu_result* is the
|
|
1413
|
+
corresponding view of ``self.operand_backup`` (or ``None`` when there
|
|
1414
|
+
is no cross-space backup).
|
|
1415
|
+
"""
|
|
1416
|
+
if isinstance(self.distribution, Slab) and self.fft_abstract_type == "C2R":
|
|
1417
|
+
|
|
1418
|
+
def strided_view(x):
|
|
1419
|
+
v = _get_view(x, self.result_shape_padded, self.result_data_type, self.process_group, collective_error_checking)
|
|
1420
|
+
return v.__class__(v.tensor[..., : self.result_shape[-1]])
|
|
1421
|
+
|
|
1422
|
+
cpu_result = strided_view(self.operand_backup) if self.operand_backup is not None else None
|
|
1423
|
+
result = strided_view(self.operand)
|
|
1424
|
+
else:
|
|
1425
|
+
cpu_result = (
|
|
1426
|
+
_get_view(
|
|
1427
|
+
self.operand_backup, self.result_shape, self.result_data_type, self.process_group, collective_error_checking
|
|
1428
|
+
)
|
|
1429
|
+
if self.operand_backup is not None
|
|
1430
|
+
else None
|
|
1431
|
+
)
|
|
1432
|
+
result = _get_view(
|
|
1433
|
+
self.operand, self.result_shape, self.result_data_type, self.process_group, collective_error_checking
|
|
1434
|
+
)
|
|
1435
|
+
return result, cpu_result
|
|
1436
|
+
|
|
1437
|
+
def _update_result_view_attributes(self, *, collective_error_checking=False, keep_wrappers=False):
|
|
1438
|
+
"""Rebuild result-operand views from the current operand buffer.
|
|
1439
|
+
|
|
1440
|
+
When *keep_wrappers* is ``False`` (default), ``self.result_operand``
|
|
1441
|
+
and ``self.cpu_result_operand`` are replaced with new wrapper objects.
|
|
1442
|
+
When ``True``, only ``.tensor`` is rebound on the existing wrappers.
|
|
1443
|
+
"""
|
|
1444
|
+
result, cpu_result = self._get_result_views(collective_error_checking)
|
|
1445
|
+
if keep_wrappers:
|
|
1446
|
+
self.result_operand.tensor = result.tensor
|
|
1447
|
+
if cpu_result is not None:
|
|
1448
|
+
self.cpu_result_operand.tensor = cpu_result.tensor
|
|
1449
|
+
else:
|
|
1450
|
+
self.result_operand = result
|
|
1451
|
+
if cpu_result is not None:
|
|
1452
|
+
self.cpu_result_operand = cpu_result
|
|
1453
|
+
|
|
1454
|
+
@utils.precondition(_check_valid_fft)
|
|
1455
|
+
@utils.atomic(_free_plan_resources, method=True)
|
|
1456
|
+
def plan(self, *, stream: AnyStream | None = None):
|
|
1457
|
+
"""Plan the FFT.
|
|
1458
|
+
|
|
1459
|
+
Args:
|
|
1460
|
+
stream: {stream}
|
|
1461
|
+
"""
|
|
1462
|
+
log_info = self.logger.isEnabledFor(logging.INFO)
|
|
1463
|
+
log_debug = self.logger.isEnabledFor(logging.DEBUG)
|
|
1464
|
+
|
|
1465
|
+
if self.fft_planned:
|
|
1466
|
+
self.logger.debug("The FFT has already been planned, and redoing the plan is not supported.")
|
|
1467
|
+
return
|
|
1468
|
+
|
|
1469
|
+
stream_holder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
|
|
1470
|
+
self.workspace_stream = stream_holder.obj
|
|
1471
|
+
|
|
1472
|
+
# Set stream for the FFT.
|
|
1473
|
+
with utils.device_ctx(self.device_id):
|
|
1474
|
+
cufft.set_stream(self.handle, stream_holder.ptr)
|
|
1475
|
+
|
|
1476
|
+
check_inplace_overlapping_layout(self.operand)
|
|
1477
|
+
if self.operand_backup is not None:
|
|
1478
|
+
check_inplace_overlapping_layout(self.operand_backup)
|
|
1479
|
+
|
|
1480
|
+
if log_debug:
|
|
1481
|
+
self.logger.debug(
|
|
1482
|
+
f"The operand CUDA type is {NAME_TO_DATA_TYPE[self.operand_data_type].name}, and the result CUDA type is "
|
|
1483
|
+
f"{NAME_TO_DATA_TYPE[self.result_data_type].name}."
|
|
1484
|
+
)
|
|
1485
|
+
self.logger.debug(f"The CUDA type used for compute is {NAME_TO_DATA_TYPE[self.compute_data_type].name}.")
|
|
1486
|
+
if log_info:
|
|
1487
|
+
self.logger.info("Starting distributed FFT planning...")
|
|
1488
|
+
|
|
1489
|
+
planner = None
|
|
1490
|
+
if self.operand_dim == 2:
|
|
1491
|
+
planner = cufft.make_plan2d
|
|
1492
|
+
elif self.operand_dim == 3:
|
|
1493
|
+
planner = cufft.make_plan3d
|
|
1494
|
+
else:
|
|
1495
|
+
raise AssertionError("Internal error: unsupported dimensionality for distributed FFT in plan().")
|
|
1496
|
+
|
|
1497
|
+
if self.options.redistribute:
|
|
1498
|
+
# Plan a reshape of the FFT output back to the original slab distribution of the
|
|
1499
|
+
# FFT input.
|
|
1500
|
+
from_partition_dim, to_partition_dim = (1, 0) if self.distribution == Slab.X else (0, 1)
|
|
1501
|
+
# cuFFTMP reshape API only supports 3D, so we broadcast 2D operands.
|
|
1502
|
+
X, Y = self.global_result_extents[:2]
|
|
1503
|
+
Z = self.global_result_extents[2] if self.operand_dim == 3 else 1
|
|
1504
|
+
global_shape = (X, Y, Z)
|
|
1505
|
+
reshape_input_box = _calculate_local_box(global_shape, from_partition_dim, self.rank, self.nranks)
|
|
1506
|
+
reshape_output_box = _calculate_local_box(global_shape, to_partition_dim, self.rank, self.nranks)
|
|
1507
|
+
lower, upper = reshape_input_box
|
|
1508
|
+
reshape_input_strides = (
|
|
1509
|
+
self.intermediate_strides if self.operand_dim == 3 else tuple(self.intermediate_strides) + (1,)
|
|
1510
|
+
)
|
|
1511
|
+
reshape_output_strides = self.result_strides if self.operand_dim == 3 else tuple(self.result_strides) + (1,)
|
|
1512
|
+
|
|
1513
|
+
with utils.cuda_call_ctx(stream_holder, blocking=True, timing=log_info) as (
|
|
1514
|
+
self.last_compute_event,
|
|
1515
|
+
elapsed,
|
|
1516
|
+
):
|
|
1517
|
+
if isinstance(self.distribution, Slab):
|
|
1518
|
+
self.subformat = self.distribution._cufftmp_value
|
|
1519
|
+
else:
|
|
1520
|
+
if self.fft_abstract_type == "C2R":
|
|
1521
|
+
# C2R plans only support CUFFT_XT_FORMAT_DISTRIBUTED_OUTPUT,
|
|
1522
|
+
# i.e., (lower_input, upper_input) should describe the real data
|
|
1523
|
+
# distribution and (lower_output, upper_output) the complex data
|
|
1524
|
+
# distribution.
|
|
1525
|
+
lower_input, upper_input = self.distribution[1]
|
|
1526
|
+
lower_output, upper_output = self.distribution[0]
|
|
1527
|
+
strides_input = self.result_strides
|
|
1528
|
+
strides_output = self.operand_layout.strides
|
|
1529
|
+
else:
|
|
1530
|
+
lower_input, upper_input = self.distribution[0]
|
|
1531
|
+
lower_output, upper_output = self.distribution[1]
|
|
1532
|
+
strides_input = self.operand_layout.strides
|
|
1533
|
+
strides_output = self.result_strides
|
|
1534
|
+
|
|
1535
|
+
cufft.xt_set_distribution(
|
|
1536
|
+
self.handle,
|
|
1537
|
+
self.operand_dim,
|
|
1538
|
+
lower_input,
|
|
1539
|
+
upper_input,
|
|
1540
|
+
lower_output,
|
|
1541
|
+
upper_output,
|
|
1542
|
+
strides_input,
|
|
1543
|
+
strides_output,
|
|
1544
|
+
)
|
|
1545
|
+
self.box_to_subformat = {}
|
|
1546
|
+
self.box_to_subformat[Box(lower_input, upper_input)] = cufft.XtSubFormat.FORMAT_DISTRIBUTED_INPUT
|
|
1547
|
+
self.box_to_subformat[Box(lower_output, upper_output)] = cufft.XtSubFormat.FORMAT_DISTRIBUTED_OUTPUT
|
|
1548
|
+
self.subformat = (
|
|
1549
|
+
cufft.XtSubFormat.FORMAT_DISTRIBUTED_INPUT
|
|
1550
|
+
if self.fft_abstract_type != "C2R"
|
|
1551
|
+
else cufft.XtSubFormat.FORMAT_DISTRIBUTED_OUTPUT
|
|
1552
|
+
)
|
|
1553
|
+
|
|
1554
|
+
fft_concrete_type = _get_fft_concrete_type(self.operand_data_type, self.fft_abstract_type)
|
|
1555
|
+
self.logger.debug(f"The FFT concrete type is {fft_concrete_type.name}.")
|
|
1556
|
+
# NVSHMEM is already initialized (no need to pass MPI comm to the library).
|
|
1557
|
+
cufft.attach_comm(self.handle, cufft.MpCommType.COMM_NONE, 0)
|
|
1558
|
+
if self.fft_abstract_type == "C2R":
|
|
1559
|
+
self.workspace_size = planner(self.handle, *self.global_result_extents, fft_concrete_type)
|
|
1560
|
+
else:
|
|
1561
|
+
self.workspace_size = planner(self.handle, *self.global_extents, fft_concrete_type)
|
|
1562
|
+
|
|
1563
|
+
# Create memory descriptor using dummy handle.
|
|
1564
|
+
_ = planner(self.memory_desc_handle, *[1] * self.operand_dim, fft_concrete_type)
|
|
1565
|
+
self.memory_desc = cufft.xt_malloc(self.memory_desc_handle, cufft.XtSubFormat.FORMAT_INPLACE)
|
|
1566
|
+
|
|
1567
|
+
if self.options.redistribute:
|
|
1568
|
+
nullptr = 0
|
|
1569
|
+
cufft.make_reshape(
|
|
1570
|
+
self.reshape_handle,
|
|
1571
|
+
self.result_operand.itemsize,
|
|
1572
|
+
3,
|
|
1573
|
+
reshape_input_box[0],
|
|
1574
|
+
reshape_input_box[1],
|
|
1575
|
+
reshape_input_strides,
|
|
1576
|
+
reshape_output_box[0],
|
|
1577
|
+
reshape_output_box[1],
|
|
1578
|
+
reshape_output_strides,
|
|
1579
|
+
nullptr,
|
|
1580
|
+
cufft.MpCommType.COMM_NONE,
|
|
1581
|
+
)
|
|
1582
|
+
reshape_workspace_size = cufft.get_reshape_size(self.reshape_handle)
|
|
1583
|
+
self.workspace_size = max(self.workspace_size, reshape_workspace_size)
|
|
1584
|
+
|
|
1585
|
+
self.logger.debug(
|
|
1586
|
+
f"The workspace required on process {self.rank} for the distributed"
|
|
1587
|
+
f" FFT operation is {formatters.MemoryStr(self.workspace_size)}."
|
|
1588
|
+
)
|
|
1589
|
+
|
|
1590
|
+
# Store memory descriptor's buffer pointer, to be able to free it later.
|
|
1591
|
+
self.dummy_desc_data_ptr = cufft.set_descriptor_data(self.memory_desc, 0, self.subformat)
|
|
1592
|
+
|
|
1593
|
+
self.fft_planned = True
|
|
1594
|
+
|
|
1595
|
+
if log_info and elapsed.data is not None:
|
|
1596
|
+
self.logger.info(f"The FFT planning phase took {elapsed.data:.3f} ms to complete.")
|
|
1597
|
+
|
|
1598
|
+
def _validate_reset_operand(self, operand, input_distribution, stream):
|
|
1599
|
+
"""
|
|
1600
|
+
(private) Validate operand and distribution for reset_operand.
|
|
1601
|
+
|
|
1602
|
+
Performs all precondition checks (operand compatibility,
|
|
1603
|
+
distribution compatibility) without any side effects.
|
|
1604
|
+
|
|
1605
|
+
Returns ``(distribution, distribution_unchanged)``
|
|
1606
|
+
where *distribution* is in internal form (Slab or Box-tuple,
|
|
1607
|
+
**not** yet bound) and *distribution_unchanged* is a bool.
|
|
1608
|
+
|
|
1609
|
+
This method does **not** mutate ``self.distribution``,
|
|
1610
|
+
``self.subformat``, or the distribution object.
|
|
1611
|
+
"""
|
|
1612
|
+
wrapped = tensor_wrapper.wrap_operand(operand)
|
|
1613
|
+
|
|
1614
|
+
if self.package != wrapped.name:
|
|
1615
|
+
raise TypeError(f"Library package mismatch: '{self.package}' => '{wrapped.name}'")
|
|
1616
|
+
|
|
1617
|
+
utils.check_attribute_match(self.operand_data_type, wrapped.dtype, "data type")
|
|
1618
|
+
|
|
1619
|
+
if len(wrapped.shape) != self.operand_dim:
|
|
1620
|
+
raise ValueError(
|
|
1621
|
+
f"The reset operand number of dimensions ({len(wrapped.shape)}) does not "
|
|
1622
|
+
f"match the FFT number of dimensions ({self.operand_dim})"
|
|
1623
|
+
)
|
|
1624
|
+
|
|
1625
|
+
stream_holder: StreamHolder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
|
|
1626
|
+
self.logger.info(f"The specified stream for reset_operand() is {stream_holder and stream_holder.obj}.")
|
|
1627
|
+
|
|
1628
|
+
# In principle, we could support memory_space change,
|
|
1629
|
+
# but to handle it properly we need to update self.memory_space and
|
|
1630
|
+
# some dependent properties, like self.blocking, which may be error-prone
|
|
1631
|
+
# from the user perspective. It would prevent inplace optimizations as well.
|
|
1632
|
+
operand_device_id = wrapped.device_id
|
|
1633
|
+
if operand_device_id != self.operand_device_id:
|
|
1634
|
+
|
|
1635
|
+
def device_str(device_id: int | Literal["cpu"]) -> str:
|
|
1636
|
+
return f"cuda:{device_id}" if isinstance(device_id, int) else f"{device_id}"
|
|
1637
|
+
|
|
1638
|
+
raise ValueError(
|
|
1639
|
+
f"The new operand must be on the same device as the original one. "
|
|
1640
|
+
f"The new operand's device is {device_str(operand_device_id)}, "
|
|
1641
|
+
f"the original device is {device_str(self.operand_device_id)}"
|
|
1642
|
+
)
|
|
1643
|
+
|
|
1644
|
+
if self.memory_space == "cuda" and not wrapped.is_symmetric_memory:
|
|
1645
|
+
raise TypeError("Distributed FFT requires GPU operand to be on symmetric memory")
|
|
1646
|
+
|
|
1647
|
+
# Check for C memory layout.
|
|
1648
|
+
if sorted(wrapped.strides, reverse=True) != list(wrapped.strides):
|
|
1649
|
+
raise ValueError("The reset operand memory layout is not C")
|
|
1650
|
+
|
|
1651
|
+
# Check that the distribution of the reset operand is compatible.
|
|
1652
|
+
if input_distribution is None:
|
|
1653
|
+
raise ValueError("Please specify the distribution of the operand for reset_operand")
|
|
1654
|
+
|
|
1655
|
+
if isinstance(input_distribution, Distribution):
|
|
1656
|
+
distribution = input_distribution.to(Slab, ndim=self.operand_dim, copy=True)
|
|
1657
|
+
else:
|
|
1658
|
+
# Must be a Box pair.
|
|
1659
|
+
distribution = tuple(cast(Box, box.copy()) for box in input_distribution)
|
|
1660
|
+
|
|
1661
|
+
distribution_type_old = "slab" if isinstance(self.distribution, Slab) else "box"
|
|
1662
|
+
distribution_type_new = "slab" if isinstance(distribution, Slab) else "box"
|
|
1663
|
+
if distribution_type_old != distribution_type_new:
|
|
1664
|
+
raise ValueError(
|
|
1665
|
+
f"This FFT uses {distribution_type_old} distribution, but got "
|
|
1666
|
+
f"{distribution_type_new} distribution in reset_operand."
|
|
1667
|
+
)
|
|
1668
|
+
|
|
1669
|
+
if self.fft_abstract_type in ("R2C", "C2R") and self.distribution != distribution:
|
|
1670
|
+
raise ValueError(f"Can't change distribution with FFT type {self.fft_abstract_type}")
|
|
1671
|
+
|
|
1672
|
+
distribution_unchanged = self.distribution == distribution
|
|
1673
|
+
|
|
1674
|
+
if distribution_type_old == "slab":
|
|
1675
|
+
if self.options.redistribute and not distribution_unchanged:
|
|
1676
|
+
raise ValueError("Can't change distribution when using redistribute=True")
|
|
1677
|
+
else:
|
|
1678
|
+
distribution = cast(Sequence[Box], distribution)
|
|
1679
|
+
input_box, output_box = distribution
|
|
1680
|
+
if {input_box, output_box} != set(self.box_to_subformat):
|
|
1681
|
+
raise ValueError("The reset operand distribution must use the original (input, output) box pair (in any order)")
|
|
1682
|
+
|
|
1683
|
+
# Check the operand shape matches the distribution.
|
|
1684
|
+
d = distribution if isinstance(distribution, Slab) else distribution[0]
|
|
1685
|
+
expected_shape = d.shape(self.rank, self.global_extents)
|
|
1686
|
+
|
|
1687
|
+
if tuple(wrapped.shape) != expected_shape:
|
|
1688
|
+
raise ValueError(
|
|
1689
|
+
f"Expected operand shape {expected_shape} for {distribution} "
|
|
1690
|
+
f"with global shape {self.global_extents}, got {tuple(wrapped.shape)}"
|
|
1691
|
+
)
|
|
1692
|
+
|
|
1693
|
+
return distribution, distribution_unchanged
|
|
1694
|
+
|
|
1695
|
+
def _bind_and_apply_distribution(self, distribution):
|
|
1696
|
+
"""
|
|
1697
|
+
(private) Bind *distribution* to the global shape and apply it.
|
|
1698
|
+
|
|
1699
|
+
Attach the new distribution to the global shape so it knows the
|
|
1700
|
+
local partition on this rank, then update the cufftMp subformat
|
|
1701
|
+
identifier that tells the library which distribution to use
|
|
1702
|
+
during execution.
|
|
1703
|
+
|
|
1704
|
+
Shape validation is **not** performed here; the caller is
|
|
1705
|
+
responsible for verifying the operand shape beforehand (the
|
|
1706
|
+
checked path does this in ``_validate_reset_operand``).
|
|
1707
|
+
"""
|
|
1708
|
+
if isinstance(distribution, Slab):
|
|
1709
|
+
distribution._bind(self.global_extents)
|
|
1710
|
+
self.subformat = distribution._cufftmp_value
|
|
1711
|
+
else:
|
|
1712
|
+
distribution[0]._bind(self.global_extents)
|
|
1713
|
+
self.subformat = self.box_to_subformat[distribution[0]]
|
|
1714
|
+
self.distribution = distribution
|
|
1715
|
+
|
|
1716
|
+
def _log_distribution(self):
|
|
1717
|
+
"""
|
|
1718
|
+
(private) Log the current distribution axis / box information.
|
|
1719
|
+
"""
|
|
1720
|
+
if isinstance(self.distribution, Slab):
|
|
1721
|
+
if self.options.redistribute:
|
|
1722
|
+
from_axis, to_axis = ("X", "X") if self.distribution == Slab.X else ("Y", "Y")
|
|
1723
|
+
else:
|
|
1724
|
+
from_axis, to_axis = ("X", "Y") if self.distribution == Slab.X else ("Y", "X")
|
|
1725
|
+
self.logger.info(
|
|
1726
|
+
f"The operand distribution is Slab, with input partitioned on {from_axis} axis "
|
|
1727
|
+
f"and output on {to_axis} (redistribute={self.options.redistribute})."
|
|
1728
|
+
)
|
|
1729
|
+
else:
|
|
1730
|
+
self.logger.info("The operand distribution is based on custom input and output boxes given on each process.")
|
|
1731
|
+
|
|
1732
|
+
def _reset_operand_same_distribution_same_space(self, operand, *, enable_logging=True):
|
|
1733
|
+
"""
|
|
1734
|
+
(private) Reset operand when the distribution is unchanged
|
|
1735
|
+
and execution_space == memory_space.
|
|
1736
|
+
|
|
1737
|
+
This method is hit when the distribution does not change compared to
|
|
1738
|
+
the one used at plan time. This is always the case for R2C/C2R since
|
|
1739
|
+
these forbid distribution changes, and also covers C2C when the caller
|
|
1740
|
+
reuses the same distribution.
|
|
1741
|
+
|
|
1742
|
+
Because both the distribution and memory space are unchanged, this is
|
|
1743
|
+
the leanest path: the user operand already resides in the execution
|
|
1744
|
+
memory space (GPU symmetric memory), so we just swap the underlying
|
|
1745
|
+
tensor reference in the wrapper.
|
|
1746
|
+
"""
|
|
1747
|
+
log_info = enable_logging and self.logger.isEnabledFor(logging.INFO)
|
|
1748
|
+
|
|
1749
|
+
self.operand.tensor = operand
|
|
1750
|
+
|
|
1751
|
+
if log_info:
|
|
1752
|
+
self._log_distribution()
|
|
1753
|
+
self.logger.info(f"The reset operand shape = {self.operand.shape}, and strides = {self.operand.strides}.")
|
|
1754
|
+
self.logger.info(f"The result shape = {self.result_shape}, and strides = {self.result_strides}.")
|
|
1755
|
+
|
|
1756
|
+
# self.result_operand was set during plan() and may be the same
|
|
1757
|
+
# object as self.operand (C2C with reshape, where result shape
|
|
1758
|
+
# matches operand shape). In that case, the tensor swap above
|
|
1759
|
+
# already implies that self.result_operand is updated too.
|
|
1760
|
+
# Otherwise it is a separate wrapper and must be rebuilt.
|
|
1761
|
+
if self.result_operand is not self.operand:
|
|
1762
|
+
self._update_result_view_attributes(keep_wrappers=True)
|
|
1763
|
+
self._operand_released = False
|
|
1764
|
+
|
|
1765
|
+
def _reset_operand_same_distribution_cross_space(self, operand, stream, *, enable_logging=True):
|
|
1766
|
+
"""
|
|
1767
|
+
(private) Reset operand when the distribution is unchanged
|
|
1768
|
+
but execution_space != memory_space.
|
|
1769
|
+
|
|
1770
|
+
This method is hit when the distribution does not change compared to
|
|
1771
|
+
the one used at plan time. This is always the case for R2C/C2R since
|
|
1772
|
+
these forbid distribution changes, and also covers C2C when the caller
|
|
1773
|
+
reuses the same distribution.
|
|
1774
|
+
|
|
1775
|
+
The user operand lives on CPU while the FFT executes on CUDA.
|
|
1776
|
+
We have two attributes to deal with:
|
|
1777
|
+
|
|
1778
|
+
- ``self.operand``: points to the GPU mirror in symmetric memory, used by cufftMp.
|
|
1779
|
+
- ``self.operand_backup``: points to the user's CPU tensor, for result copy-back.
|
|
1780
|
+
"""
|
|
1781
|
+
log_info = enable_logging and self.logger.isEnabledFor(logging.INFO)
|
|
1782
|
+
|
|
1783
|
+
stream_holder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
|
|
1784
|
+
operand_wrapped = tensor_wrapper.wrap_operand(operand)
|
|
1785
|
+
if self._operand_released:
|
|
1786
|
+
# release_operand() freed the symmetric memory but kept the
|
|
1787
|
+
# TensorHolder wrappers alive. So here we allocate a fresh GPU buffer
|
|
1788
|
+
# and rebind .tensor on the existing wrappers.
|
|
1789
|
+
new_mirror = _alloc_and_copy_to_exespace_mirror(
|
|
1790
|
+
operand_wrapped,
|
|
1791
|
+
stream_holder,
|
|
1792
|
+
self.fft_abstract_type,
|
|
1793
|
+
self.global_extents,
|
|
1794
|
+
self.distribution,
|
|
1795
|
+
self.capacity,
|
|
1796
|
+
self.rank,
|
|
1797
|
+
self.nranks,
|
|
1798
|
+
)
|
|
1799
|
+
self.operand.tensor = new_mirror.tensor
|
|
1800
|
+
self.operand_backup.tensor = operand_wrapped.tensor
|
|
1801
|
+
else:
|
|
1802
|
+
# GPU mirror is alive, we copy the new CPU data into it and
|
|
1803
|
+
# update the backup to reference the new CPU tensor.
|
|
1804
|
+
self.operand.copy_(operand_wrapped, stream_holder=stream_holder)
|
|
1805
|
+
self.operand_backup.tensor = operand_wrapped.tensor
|
|
1806
|
+
|
|
1807
|
+
if log_info:
|
|
1808
|
+
self._log_distribution()
|
|
1809
|
+
self.logger.info(f"The reset operand shape = {self.operand.shape}, and strides = {self.operand.strides}.")
|
|
1810
|
+
self.logger.info(f"The result shape = {self.result_shape}, and strides = {self.result_strides}.")
|
|
1811
|
+
|
|
1812
|
+
# self.result_operand was set during plan() and may be the same
|
|
1813
|
+
# object as self.operand (C2C with reshape, where result shape matches
|
|
1814
|
+
# operand shape). In that case, the rebinds above already implies
|
|
1815
|
+
# that self.result_operand is updated too.
|
|
1816
|
+
# Otherwise it is a separate wrapper and must be rebuilt.
|
|
1817
|
+
if self.result_operand is not self.operand:
|
|
1818
|
+
self._update_result_view_attributes(keep_wrappers=True)
|
|
1819
|
+
self._operand_released = False
|
|
1820
|
+
|
|
1821
|
+
def _reset_operand_new_distribution(self, operand, stream, *, enable_logging=True):
|
|
1822
|
+
"""Reset operand when the distribution has changed (C2C only).
|
|
1823
|
+
|
|
1824
|
+
This method is hit when the caller switches between distributions
|
|
1825
|
+
(e.g. Slab.X ↔ Slab.Y). This path is only reachable for C2C
|
|
1826
|
+
transforms (R2C/C2R cannot change distribution by contract), so
|
|
1827
|
+
all logic below assumes C2C.
|
|
1828
|
+
|
|
1829
|
+
Because the distribution changed, the local operand shape may
|
|
1830
|
+
differ from the previous call. This method therefore recomputes
|
|
1831
|
+
the operand and result layouts and rebuilds the result views.
|
|
1832
|
+
|
|
1833
|
+
The caller must have already bound the new distribution and
|
|
1834
|
+
applied it (via ``_bind_and_apply_distribution``) before
|
|
1835
|
+
invoking this method.
|
|
1836
|
+
"""
|
|
1837
|
+
log_info = enable_logging and self.logger.isEnabledFor(logging.INFO)
|
|
1838
|
+
|
|
1839
|
+
if log_info:
|
|
1840
|
+
self._log_distribution()
|
|
1841
|
+
|
|
1842
|
+
# Phase 1: Update self.operand to point at the new data
|
|
1843
|
+
# ------------------------------------------------------
|
|
1844
|
+
# The new distribution may change the local shape. Both branches
|
|
1845
|
+
# below rebind .tensor on the existing TensorHolder wrappers rather
|
|
1846
|
+
# than replacing them. This is safe because the wrappers carry no
|
|
1847
|
+
# distribution-specific state — shape and strides are derived from
|
|
1848
|
+
# .tensor. In the same-space case the user already provides a tensor
|
|
1849
|
+
# on symmetric memory, so we just swap the reference. In the
|
|
1850
|
+
# cross-space case, we must copy the CPU data into the GPU mirror
|
|
1851
|
+
# (allocating one if it was released).
|
|
1852
|
+
if self.execution_space == self.memory_space:
|
|
1853
|
+
self.operand.tensor = operand
|
|
1854
|
+
else:
|
|
1855
|
+
# Cross-space: two wrappers track the operand:
|
|
1856
|
+
# - self.operand: points to the GPU mirror in symmetric memory, used by cufftMp
|
|
1857
|
+
# - self.operand_backup: points to the user's CPU tensor, for result copy-back
|
|
1858
|
+
stream_holder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
|
|
1859
|
+
operand_wrapped = tensor_wrapper.wrap_operand(operand)
|
|
1860
|
+
if self._operand_released:
|
|
1861
|
+
# release_operand() freed the symmetric memory but kept the
|
|
1862
|
+
# TensorHolder wrappers alive. So here we allocate a fresh GPU buffer
|
|
1863
|
+
# and rebind .tensor on the existing wrappers.
|
|
1864
|
+
new_mirror = _alloc_and_copy_to_exespace_mirror(
|
|
1865
|
+
operand_wrapped,
|
|
1866
|
+
stream_holder,
|
|
1867
|
+
self.fft_abstract_type,
|
|
1868
|
+
self.global_extents,
|
|
1869
|
+
self.distribution,
|
|
1870
|
+
self.capacity,
|
|
1871
|
+
self.rank,
|
|
1872
|
+
self.nranks,
|
|
1873
|
+
)
|
|
1874
|
+
self.operand.tensor = new_mirror.tensor
|
|
1875
|
+
self.operand_backup.tensor = operand_wrapped.tensor
|
|
1876
|
+
else:
|
|
1877
|
+
# Mirror is still alive, we need to get a view of it
|
|
1878
|
+
# with the new shape and copy the new CPU data in.
|
|
1879
|
+
operand_view = _get_view(
|
|
1880
|
+
self.operand,
|
|
1881
|
+
operand_wrapped.shape,
|
|
1882
|
+
operand_wrapped.dtype,
|
|
1883
|
+
self.process_group,
|
|
1884
|
+
collective_error_checking=False,
|
|
1885
|
+
)
|
|
1886
|
+
operand_view.copy_(operand_wrapped, stream_holder=stream_holder)
|
|
1887
|
+
self.operand.tensor = operand_view.tensor
|
|
1888
|
+
self.operand_backup.tensor = operand_wrapped.tensor
|
|
1889
|
+
|
|
1890
|
+
# Phase 2: Recompute layouts
|
|
1891
|
+
# --------------------------
|
|
1892
|
+
# The operand's local shape may have changed, so update the layout.
|
|
1893
|
+
self.operand_layout = TensorLayout(shape=self.operand.shape, strides=self.operand.strides)
|
|
1894
|
+
|
|
1895
|
+
if log_info:
|
|
1896
|
+
self.logger.info(
|
|
1897
|
+
f"The reset operand shape = {self.operand_layout.shape}, and strides = {self.operand_layout.strides}."
|
|
1898
|
+
)
|
|
1899
|
+
|
|
1900
|
+
# Determine result layout based on how cufftMp distributes the output:
|
|
1901
|
+
# - Box: result follows the output box's precomputed layout.
|
|
1902
|
+
# - Slab without redistribution: output is on the complementary axis.
|
|
1903
|
+
# - Slab with redistribution: output matches the input axis.
|
|
1904
|
+
if isinstance(self.distribution, tuple):
|
|
1905
|
+
output_box = self.distribution[1]
|
|
1906
|
+
result_layout = self.distribution_layout[output_box]
|
|
1907
|
+
output_box._bind(self.global_result_extents)
|
|
1908
|
+
elif not self.options.redistribute:
|
|
1909
|
+
result_layout = self.distribution_layout[Slab.X if self.distribution == Slab.Y else Slab.Y]
|
|
1910
|
+
else:
|
|
1911
|
+
result_layout = self.operand_layout
|
|
1912
|
+
|
|
1913
|
+
self.result_shape = result_layout.shape
|
|
1914
|
+
self.result_strides = result_layout.strides
|
|
1915
|
+
|
|
1916
|
+
if log_info:
|
|
1917
|
+
self.logger.info(f"The result shape = {self.result_shape}, and strides = {self.result_strides}.")
|
|
1918
|
+
|
|
1919
|
+
# Phase 3: Rebuild result views
|
|
1920
|
+
# ------------------------------
|
|
1921
|
+
# Rebuild self.result_operand (and self.cpu_result_operand in cross-space)
|
|
1922
|
+
# to reflect the updated buffer and result layout.
|
|
1923
|
+
# We replace the wrapper (keep_wrappers=False) because self.result_operand
|
|
1924
|
+
# may be the same object as self.operand. A distribution change can make
|
|
1925
|
+
# result_shape diverge, and rebinding .tensor on a shared object would
|
|
1926
|
+
# corrupt self.operand. Replacing the wrapper breaks the stale alias.
|
|
1927
|
+
self._update_result_view_attributes(keep_wrappers=False)
|
|
1928
|
+
|
|
1929
|
+
self._operand_released = False
|
|
1930
|
+
|
|
1931
|
+
@utils.precondition(_check_valid_fft)
|
|
1932
|
+
def reset_operand(self, operand, *, distribution: Distribution | Sequence[Box], stream: AnyStream | None = None):
|
|
1933
|
+
"""
|
|
1934
|
+
Reset the operand held by this :class:`FFT` instance to a new compatible
|
|
1935
|
+
operand for subsequent execution.
|
|
1936
|
+
|
|
1937
|
+
Args:
|
|
1938
|
+
operand: A tensor (ndarray-like object) compatible with the previous one.
|
|
1939
|
+
The new operand is considered compatible if all the
|
|
1940
|
+
following properties match with the previous one:
|
|
1941
|
+
|
|
1942
|
+
- The operand data type.
|
|
1943
|
+
- The package that the new operand belongs to.
|
|
1944
|
+
- The memory space of the new operand (CPU or GPU).
|
|
1945
|
+
- The device that the new operand belongs to if it is on GPU.
|
|
1946
|
+
- The operand shape must be consistent with the specified
|
|
1947
|
+
``distribution`` (see below).
|
|
1948
|
+
|
|
1949
|
+
distribution: {distribution} This argument is required.
|
|
1950
|
+
The distribution must be compatible with the one used at plan time:
|
|
1951
|
+
|
|
1952
|
+
- If the FFT was planned using a Slab distribution, the reset
|
|
1953
|
+
distribution must also be a Slab distribution. For C2C transforms,
|
|
1954
|
+
both ``Slab.X`` and ``Slab.Y`` are valid regardless of the slab
|
|
1955
|
+
axis at plan time. For R2C and C2R transforms, the distribution
|
|
1956
|
+
must be the same as at plan time.
|
|
1957
|
+
- If the FFT was planned using a box distribution, the reset
|
|
1958
|
+
distribution must use the same ``(input_box, output_box)`` pair
|
|
1959
|
+
specified at plan time (the order may be swapped).
|
|
1960
|
+
- If ``redistribute=True`` was specified in the options, the distribution
|
|
1961
|
+
cannot be changed.
|
|
1962
|
+
|
|
1963
|
+
stream: {stream}
|
|
1964
|
+
|
|
1965
|
+
Examples:
|
|
1966
|
+
|
|
1967
|
+
>>> import cupy as cp
|
|
1968
|
+
>>> import nvmath.distributed
|
|
1969
|
+
|
|
1970
|
+
Get process group used to initialize nvmath.distributed (for information on
|
|
1971
|
+
initializing nvmath.distributed, you can refer to the documentation or to the
|
|
1972
|
+
FFT examples in `nvmath/examples/distributed/fft
|
|
1973
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/fft>`_):
|
|
1974
|
+
|
|
1975
|
+
>>> process_group = nvmath.distributed.get_context().process_group
|
|
1976
|
+
>>> nranks = process_group.nranks
|
|
1977
|
+
|
|
1978
|
+
Create a 3-D complex128 ndarray on GPU symmetric memory, distributed according
|
|
1979
|
+
to the Slab distribution on the X axis (the global shape is (128, 128, 128)):
|
|
1980
|
+
|
|
1981
|
+
>>> from nvmath.distributed.distribution import Slab
|
|
1982
|
+
>>> shape = 128 // nranks, 128, 128
|
|
1983
|
+
>>> dtype = cp.complex128
|
|
1984
|
+
>>> a = nvmath.distributed.allocate_symmetric_memory(shape, cp, dtype=dtype)
|
|
1985
|
+
>>> a[:] = cp.random.rand(*shape) + 1j * cp.random.rand(*shape)
|
|
1986
|
+
|
|
1987
|
+
Create an FFT object as a context manager
|
|
1988
|
+
|
|
1989
|
+
>>> with nvmath.distributed.fft.FFT(a, distribution=Slab.X) as f:
|
|
1990
|
+
... # Plan the FFT
|
|
1991
|
+
... f.plan()
|
|
1992
|
+
...
|
|
1993
|
+
... # Execute the FFT to get the first result.
|
|
1994
|
+
... r1 = f.execute()
|
|
1995
|
+
...
|
|
1996
|
+
... # Reset the operand to a new CuPy ndarray.
|
|
1997
|
+
... b = nvmath.distributed.allocate_symmetric_memory(shape, cp, dtype=dtype)
|
|
1998
|
+
... b[:] = cp.random.rand(*shape) + 1j * cp.random.rand(*shape)
|
|
1999
|
+
... f.reset_operand(b, distribution=Slab.X)
|
|
2000
|
+
...
|
|
2001
|
+
... # Execute to get the new result corresponding to the updated operand.
|
|
2002
|
+
... r2 = f.execute()
|
|
2003
|
+
|
|
2004
|
+
With :meth:`reset_operand`, minimal overhead is achieved as problem
|
|
2005
|
+
specification and planning are only performed once.
|
|
2006
|
+
|
|
2007
|
+
For the particular example above, explicitly calling :meth:`reset_operand` is
|
|
2008
|
+
equivalent to updating the operand in-place, i.e, replacing
|
|
2009
|
+
``f.reset_operand(b, distribution=Slab.X)`` with ``a[:]=b``.
|
|
2010
|
+
Note that updating the operand in-place should be adopted with
|
|
2011
|
+
caution as it can only yield the expected result and
|
|
2012
|
+
incur no additional copies under the additional constraints below:
|
|
2013
|
+
|
|
2014
|
+
- The operand's distribution is the same.
|
|
2015
|
+
|
|
2016
|
+
For more details, please refer to `inplace update example
|
|
2017
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/fft/example06_stateful_reset_inplace.py>`_.
|
|
2018
|
+
|
|
2019
|
+
.. seealso::
|
|
2020
|
+
:meth:`release_operand`
|
|
2021
|
+
"""
|
|
2022
|
+
self.logger.info("Resetting operand...")
|
|
2023
|
+
|
|
2024
|
+
if operand is None:
|
|
2025
|
+
raise ValueError("reset_operand() requires a valid operand.")
|
|
2026
|
+
|
|
2027
|
+
distribution, distribution_unchanged = self._validate_reset_operand(operand, distribution, stream)
|
|
2028
|
+
|
|
2029
|
+
# When the distribution is unchanged, we can skip a lot of boilerplate
|
|
2030
|
+
# code and logic for re-layouting the operand and result.
|
|
2031
|
+
# This always applies to R2C/C2R since the distribution is fixed by design
|
|
2032
|
+
# and to C2C with the same distribution. Only C2C with a changed distribution
|
|
2033
|
+
# falls through to the full path.
|
|
2034
|
+
if distribution_unchanged:
|
|
2035
|
+
if self.execution_space == self.memory_space:
|
|
2036
|
+
self._reset_operand_same_distribution_same_space(operand)
|
|
2037
|
+
else:
|
|
2038
|
+
self._reset_operand_same_distribution_cross_space(operand, stream)
|
|
2039
|
+
else:
|
|
2040
|
+
self._bind_and_apply_distribution(distribution)
|
|
2041
|
+
self._reset_operand_new_distribution(operand, stream)
|
|
2042
|
+
|
|
2043
|
+
self.logger.info("The operand has been reset to the specified operand.")
|
|
2044
|
+
|
|
2045
|
+
def reset_operand_unchecked(
|
|
2046
|
+
self,
|
|
2047
|
+
operand,
|
|
2048
|
+
*,
|
|
2049
|
+
distribution: Distribution | Sequence[Box],
|
|
2050
|
+
stream: AnyStream | None = None,
|
|
2051
|
+
):
|
|
2052
|
+
"""
|
|
2053
|
+
{reset_operand_unchecked}
|
|
2054
|
+
"""
|
|
2055
|
+
# Convert to internal form before comparing so that equivalent
|
|
2056
|
+
# distributions of different types are recognized as equal.
|
|
2057
|
+
if isinstance(distribution, Distribution):
|
|
2058
|
+
distribution = distribution.to(Slab, ndim=self.operand_dim, copy=True)
|
|
2059
|
+
else:
|
|
2060
|
+
distribution = tuple(cast(Box, box.copy()) for box in distribution)
|
|
2061
|
+
|
|
2062
|
+
# When the distribution is unchanged, we can skip a lot of boilerplate
|
|
2063
|
+
# code and logic for re-layouting the operand and result.
|
|
2064
|
+
# This always applies to R2C/C2R since the distribution is fixed by design
|
|
2065
|
+
# and to C2C with the same distribution. Only C2C with a changed distribution
|
|
2066
|
+
# falls through to the full path.
|
|
2067
|
+
if self.distribution == distribution:
|
|
2068
|
+
if self.execution_space == self.memory_space:
|
|
2069
|
+
self._reset_operand_same_distribution_same_space(operand, enable_logging=False)
|
|
2070
|
+
else:
|
|
2071
|
+
self._reset_operand_same_distribution_cross_space(operand, stream, enable_logging=False)
|
|
2072
|
+
else:
|
|
2073
|
+
self._bind_and_apply_distribution(distribution)
|
|
2074
|
+
self._reset_operand_new_distribution(operand, stream, enable_logging=False)
|
|
2075
|
+
|
|
2076
|
+
@utils.precondition(_check_valid_fft)
|
|
2077
|
+
def release_operand(self):
|
|
2078
|
+
"""
|
|
2079
|
+
{release_operand}
|
|
2080
|
+
"""
|
|
2081
|
+
if self._operand_released:
|
|
2082
|
+
self.logger.info("Operand has already been released; nothing to do.")
|
|
2083
|
+
return
|
|
2084
|
+
|
|
2085
|
+
# Note that if/when possible, we keep the TensorHolder wrappers alive
|
|
2086
|
+
# and only release the internal tensor reference. This is useful when
|
|
2087
|
+
# reset_operand_unchecked is called subsequently because it can reuse
|
|
2088
|
+
# the existing wrappers, saving overhead.
|
|
2089
|
+
if self.execution_space == self.memory_space:
|
|
2090
|
+
# Same-space: self.operand is the user's tensor, and
|
|
2091
|
+
# self.result_operand is a view of it (or the same object
|
|
2092
|
+
# for C2C when shape/dtype match). Both must be released.
|
|
2093
|
+
self.operand.tensor = None
|
|
2094
|
+
self.result_operand.tensor = None
|
|
2095
|
+
else:
|
|
2096
|
+
# Cross space:
|
|
2097
|
+
# self.operand_backup = user's tensor
|
|
2098
|
+
# self.cpu_result_operand = view of self.operand_backup (for result copy-back)
|
|
2099
|
+
# self.operand = internal nvshmem device mirror
|
|
2100
|
+
# self.result_operand = view of self.operand (shares nvshmem buffer)
|
|
2101
|
+
# Release user references and free the nvshmem mirror.
|
|
2102
|
+
# Cross-space execution is always blocking, so no synchronization
|
|
2103
|
+
# is needed before freeing the operand.
|
|
2104
|
+
self.operand_backup.tensor = None
|
|
2105
|
+
self.cpu_result_operand.tensor = None
|
|
2106
|
+
# Free nvshmem before clearing self.operand.tensor — self.result_operand
|
|
2107
|
+
# may alias self.operand (C2C with matching shape/dtype), so clearing
|
|
2108
|
+
# result_operand.tensor first would also null self.operand.tensor.
|
|
2109
|
+
with utils.device_ctx(self.device_id):
|
|
2110
|
+
self.operand.free_symmetric()
|
|
2111
|
+
self.operand.tensor = None
|
|
2112
|
+
if self.result_operand is not self.operand:
|
|
2113
|
+
self.result_operand.tensor = None
|
|
2114
|
+
|
|
2115
|
+
self._operand_released = True
|
|
2116
|
+
self.logger.info("User-provided operand has been released.")
|
|
2117
|
+
|
|
2118
|
+
def _check_planned(self, *args, **kwargs):
|
|
2119
|
+
""" """
|
|
2120
|
+
what = kwargs["what"]
|
|
2121
|
+
if not self.fft_planned:
|
|
2122
|
+
raise RuntimeError(f"{what} cannot be performed before plan() has been called.")
|
|
2123
|
+
|
|
2124
|
+
def _check_valid_operand(self, *args, **kwargs):
|
|
2125
|
+
""" """
|
|
2126
|
+
what = kwargs["what"]
|
|
2127
|
+
if self._operand_released:
|
|
2128
|
+
raise RuntimeError(
|
|
2129
|
+
f"{what} cannot be performed after the operand has been released. Use reset_operand() to provide a new "
|
|
2130
|
+
f"operand before performing the {what.lower()}."
|
|
2131
|
+
)
|
|
2132
|
+
|
|
2133
|
+
def _free_workspace_memory(self, exception: Exception | None = None) -> bool:
|
|
2134
|
+
"""
|
|
2135
|
+
Free workspace by releasing the MemoryPointer object and reshape operand.
|
|
2136
|
+
"""
|
|
2137
|
+
if self.workspace_ptr is None:
|
|
2138
|
+
return True
|
|
2139
|
+
|
|
2140
|
+
with utils.device_ctx(self.device_id):
|
|
2141
|
+
# Calling nvshmem_free on memory that's still in use is not safe
|
|
2142
|
+
# (nvshmem_free is not stream-ordered), so we need to wait for the
|
|
2143
|
+
# computation to finish.
|
|
2144
|
+
if self.workspace_stream is not None:
|
|
2145
|
+
self.workspace_stream.sync()
|
|
2146
|
+
self.workspace_ptr.free()
|
|
2147
|
+
if self.reshaped_operand is not None:
|
|
2148
|
+
self.reshaped_operand.free_symmetric()
|
|
2149
|
+
self.workspace_ptr = None
|
|
2150
|
+
self.reshaped_operand = None
|
|
2151
|
+
self.logger.debug("[_free_workspace_memory] The workspace has been released.")
|
|
2152
|
+
|
|
2153
|
+
return True
|
|
2154
|
+
|
|
2155
|
+
@utils.precondition(_check_valid_fft)
|
|
2156
|
+
@utils.precondition(_check_planned, "Workspace memory allocation")
|
|
2157
|
+
@utils.atomic(_free_workspace_memory, method=True)
|
|
2158
|
+
def _allocate_workspace_memory(self, stream_holder: StreamHolder):
|
|
2159
|
+
"""
|
|
2160
|
+
Allocate workspace memory using the specified allocator.
|
|
2161
|
+
"""
|
|
2162
|
+
|
|
2163
|
+
assert self._workspace_allocated_here is False, "Internal Error."
|
|
2164
|
+
|
|
2165
|
+
self.logger.debug("Allocating workspace for performing the FFT...")
|
|
2166
|
+
with utils.device_ctx(self.device_id), stream_holder.ctx:
|
|
2167
|
+
try:
|
|
2168
|
+
self.workspace_ptr = self.allocator.memalloc(self.workspace_size) # type: ignore[union-attr]
|
|
2169
|
+
if self.options.redistribute:
|
|
2170
|
+
self.reshaped_operand = self._allocate_reshape_operand(
|
|
2171
|
+
stream_holder, self.logger.isEnabledFor(logging.DEBUG)
|
|
2172
|
+
)
|
|
2173
|
+
self._workspace_allocated_here = True
|
|
2174
|
+
except TypeError as e:
|
|
2175
|
+
message = (
|
|
2176
|
+
"The method 'memalloc' in the allocator object must conform to the interface in the "
|
|
2177
|
+
"'BaseCUDAMemoryManager' protocol."
|
|
2178
|
+
)
|
|
2179
|
+
raise TypeError(message) from e
|
|
2180
|
+
raw_workspace_ptr = utils.get_ptr_from_memory_pointer(self.workspace_ptr)
|
|
2181
|
+
cufft.set_work_area(self.handle, raw_workspace_ptr)
|
|
2182
|
+
|
|
2183
|
+
self.workspace_stream = stream_holder.obj
|
|
2184
|
+
self.logger.debug(
|
|
2185
|
+
f"Finished allocating device workspace of size {formatters.MemoryStr(self.workspace_size)} in the context "
|
|
2186
|
+
f"of stream {self.workspace_stream}."
|
|
2187
|
+
)
|
|
2188
|
+
|
|
2189
|
+
def _allocate_workspace_memory_perhaps(self, stream_holder: StreamHolder):
|
|
2190
|
+
"""
|
|
2191
|
+
Allocate workspace memory using the specified allocator, if it hasn't already been
|
|
2192
|
+
done.
|
|
2193
|
+
"""
|
|
2194
|
+
if self.execution_space != "cuda" or self.workspace_ptr is not None:
|
|
2195
|
+
return
|
|
2196
|
+
|
|
2197
|
+
return self._allocate_workspace_memory(stream_holder)
|
|
2198
|
+
|
|
2199
|
+
@utils.precondition(_check_valid_fft)
|
|
2200
|
+
def _free_workspace_memory_perhaps(self, release_workspace):
|
|
2201
|
+
"""
|
|
2202
|
+
Free workspace memory if if 'release_workspace' is True.
|
|
2203
|
+
"""
|
|
2204
|
+
if not release_workspace:
|
|
2205
|
+
return
|
|
2206
|
+
|
|
2207
|
+
# Establish ordering wrt the computation and free workspace if it's more than the
|
|
2208
|
+
# specified cache limit.
|
|
2209
|
+
if self.last_compute_event is not None:
|
|
2210
|
+
with utils.device_ctx(self.device_id):
|
|
2211
|
+
self.workspace_stream.wait(self.last_compute_event)
|
|
2212
|
+
self.logger.debug("Established ordering with respect to the computation before releasing the workspace.")
|
|
2213
|
+
self.last_compute_event = None
|
|
2214
|
+
|
|
2215
|
+
self.logger.debug("[_free_workspace_memory_perhaps] The workspace memory will be released.")
|
|
2216
|
+
self._free_workspace_memory()
|
|
2217
|
+
|
|
2218
|
+
return True
|
|
2219
|
+
|
|
2220
|
+
def _release_workspace_memory_perhaps(self, exception: Exception | None = None) -> bool:
|
|
2221
|
+
"""
|
|
2222
|
+
Free workspace memory if it was allocated in this call
|
|
2223
|
+
(self._workspace_allocated_here == True) when an exception occurs.
|
|
2224
|
+
"""
|
|
2225
|
+
release_workspace = self._workspace_allocated_here
|
|
2226
|
+
self.logger.debug(
|
|
2227
|
+
f"[_release_workspace_memory_perhaps] The release_workspace flag is set to {release_workspace} based upon "
|
|
2228
|
+
"the value of 'workspace_allocated_here'."
|
|
2229
|
+
)
|
|
2230
|
+
self._free_workspace_memory_perhaps(release_workspace)
|
|
2231
|
+
self._workspace_allocated_here = False
|
|
2232
|
+
return True
|
|
2233
|
+
|
|
2234
|
+
@utils.precondition(_check_valid_fft)
|
|
2235
|
+
@utils.precondition(_check_planned, "Execution")
|
|
2236
|
+
@utils.precondition(_check_valid_operand, "Execution")
|
|
2237
|
+
@utils.atomic(_release_workspace_memory_perhaps, method=True)
|
|
2238
|
+
def execute(
|
|
2239
|
+
self,
|
|
2240
|
+
*,
|
|
2241
|
+
direction: FFTDirection | None = None,
|
|
2242
|
+
stream: AnyStream | None = None,
|
|
2243
|
+
release_workspace: bool = False,
|
|
2244
|
+
sync_symmetric_memory: bool = True,
|
|
2245
|
+
):
|
|
2246
|
+
"""
|
|
2247
|
+
Execute the FFT operation.
|
|
2248
|
+
|
|
2249
|
+
Args:
|
|
2250
|
+
direction: {direction}
|
|
2251
|
+
|
|
2252
|
+
stream: {stream}
|
|
2253
|
+
|
|
2254
|
+
release_workspace: A value of `True` specifies that the FFT object
|
|
2255
|
+
should release workspace memory back to the symmetric memory pool on
|
|
2256
|
+
function return, while a value of `False` specifies that the object
|
|
2257
|
+
should retain the memory. This option may be set to `True` if the
|
|
2258
|
+
application performs other operations that consume a lot of memory between
|
|
2259
|
+
successive calls to the (same or different) :meth:`execute` API, but incurs
|
|
2260
|
+
an overhead due to obtaining and releasing workspace memory from and
|
|
2261
|
+
to the symmetric memory pool on every call. The default is `False`.
|
|
2262
|
+
**NOTE: All processes must use the same value or the application can
|
|
2263
|
+
deadlock.**
|
|
2264
|
+
|
|
2265
|
+
sync_symmetric_memory: {sync_symmetric_memory}
|
|
2266
|
+
|
|
2267
|
+
Returns:
|
|
2268
|
+
The transformed operand, which remains on the same device and utilizes the same
|
|
2269
|
+
package as the input operand. The data type and shape of the transformed operand
|
|
2270
|
+
depend on the type of input operand, and choice of distribution and redistribute
|
|
2271
|
+
option:
|
|
2272
|
+
|
|
2273
|
+
- For C2C FFT, the data type remains identical to the input.
|
|
2274
|
+
- For R2C and C2R FFT, the data type differs from the input. The global output
|
|
2275
|
+
shape differs from the global input shape, which affects the shape of the
|
|
2276
|
+
result on every process.
|
|
2277
|
+
- For slab distribution with redistribute=True, the shape on this process is the
|
|
2278
|
+
slab shape according to the same distribution as the input operand.
|
|
2279
|
+
- For slab distribution with redistribute=False, the shape on this process is
|
|
2280
|
+
the complementary slab shape.
|
|
2281
|
+
- For custom box distribution, the shape will depend on the output box of
|
|
2282
|
+
each process.
|
|
2283
|
+
|
|
2284
|
+
For GPU operands, the result will be in symmetric memory and the user is
|
|
2285
|
+
responsible for explicitly deallocating it (for example, using
|
|
2286
|
+
``nvmath.distributed.free_symmetric_memory(tensor)``).
|
|
2287
|
+
"""
|
|
2288
|
+
|
|
2289
|
+
log_info = self.logger.isEnabledFor(logging.INFO)
|
|
2290
|
+
log_debug = self.logger.isEnabledFor(logging.DEBUG)
|
|
2291
|
+
|
|
2292
|
+
if direction is None:
|
|
2293
|
+
direction = _get_fft_default_direction(self.fft_abstract_type)
|
|
2294
|
+
else:
|
|
2295
|
+
direction = _get_validate_direction(direction, self.fft_abstract_type)
|
|
2296
|
+
|
|
2297
|
+
stream_holder: StreamHolder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
|
|
2298
|
+
|
|
2299
|
+
# Set stream for the FFT.
|
|
2300
|
+
with utils.device_ctx(self.device_id):
|
|
2301
|
+
cufft.set_stream(self.handle, stream_holder.ptr)
|
|
2302
|
+
|
|
2303
|
+
# Allocate workspace if needed.
|
|
2304
|
+
self._allocate_workspace_memory_perhaps(stream_holder)
|
|
2305
|
+
# cuFFTMp only supports inplace transform.
|
|
2306
|
+
result_ptr = self.operand.data_ptr
|
|
2307
|
+
|
|
2308
|
+
if log_info:
|
|
2309
|
+
self.logger.info(
|
|
2310
|
+
f"Starting distributed FFT {self.fft_abstract_type} calculation in the {direction.name} direction..." # type: ignore[union-attr]
|
|
2311
|
+
)
|
|
2312
|
+
self.logger.info(f"{self.call_prologue}")
|
|
2313
|
+
|
|
2314
|
+
with utils.cuda_call_ctx(stream_holder, self.blocking, timing=log_info) as (
|
|
2315
|
+
self.last_compute_event,
|
|
2316
|
+
elapsed,
|
|
2317
|
+
):
|
|
2318
|
+
if log_debug:
|
|
2319
|
+
self.logger.debug("The cuFFTMp execution function is 'xt_exec_descriptor'.")
|
|
2320
|
+
if sync_symmetric_memory:
|
|
2321
|
+
nvshmem.sync_all_on_stream(stream_holder.ptr)
|
|
2322
|
+
if log_info:
|
|
2323
|
+
self.logger.info(
|
|
2324
|
+
"sync_symmetric_memory is enabled (this may incur redundant multi-GPU "
|
|
2325
|
+
"synchronization, please refer to the documentation for more information)"
|
|
2326
|
+
)
|
|
2327
|
+
elif log_info:
|
|
2328
|
+
self.logger.info("sync_symmetric_memory is disabled")
|
|
2329
|
+
cufft.set_descriptor_data(self.memory_desc, result_ptr, self.subformat)
|
|
2330
|
+
cufft.xt_exec_descriptor(self.handle, self.memory_desc, self.memory_desc, direction)
|
|
2331
|
+
if self.options.redistribute:
|
|
2332
|
+
raw_workspace_ptr = utils.get_ptr_from_memory_pointer(self.workspace_ptr)
|
|
2333
|
+
assert self.reshaped_operand is not None
|
|
2334
|
+
cufft.exec_reshape_async(
|
|
2335
|
+
self.reshape_handle, self.reshaped_operand.data_ptr, result_ptr, raw_workspace_ptr, stream_holder.ptr
|
|
2336
|
+
)
|
|
2337
|
+
# Copy back to original GPU operand.
|
|
2338
|
+
self.result_operand.copy_(self.reshaped_operand, stream_holder=stream_holder)
|
|
2339
|
+
|
|
2340
|
+
if log_info and elapsed.data is not None:
|
|
2341
|
+
reshape_addendum = "along with output redistribution" if self.options.redistribute else ""
|
|
2342
|
+
self.logger.info(f"The distributed FFT calculation {reshape_addendum} took {elapsed.data:.3f} ms to complete.")
|
|
2343
|
+
|
|
2344
|
+
# Establish ordering wrt the computation and free workspace if it's more than the
|
|
2345
|
+
# specified cache limit.
|
|
2346
|
+
self._free_workspace_memory_perhaps(release_workspace)
|
|
2347
|
+
|
|
2348
|
+
# reset workspace allocation tracking to False at the end of the methods where
|
|
2349
|
+
# workspace memory is potentially allocated. This is necessary to prevent any
|
|
2350
|
+
# exceptions raised before method entry from using stale tracking values.
|
|
2351
|
+
self._workspace_allocated_here = False
|
|
2352
|
+
|
|
2353
|
+
# Return the result.
|
|
2354
|
+
if self.memory_space == self.execution_space:
|
|
2355
|
+
out = self.result_operand
|
|
2356
|
+
else:
|
|
2357
|
+
self.cpu_result_operand.copy_(self.result_operand, stream_holder=stream_holder)
|
|
2358
|
+
out = self.cpu_result_operand
|
|
2359
|
+
return out.tensor
|
|
2360
|
+
|
|
2361
|
+
def free(self):
|
|
2362
|
+
"""Free FFT resources.
|
|
2363
|
+
|
|
2364
|
+
It is recommended that the :class:`FFT` object be used within a context, but if it
|
|
2365
|
+
is not possible then this method must be called explicitly to ensure that the FFT
|
|
2366
|
+
resources (especially internal library objects) are properly cleaned up.
|
|
2367
|
+
"""
|
|
2368
|
+
|
|
2369
|
+
if not self.valid_state:
|
|
2370
|
+
return
|
|
2371
|
+
|
|
2372
|
+
try:
|
|
2373
|
+
# Future operations on the workspace stream should be ordered after the
|
|
2374
|
+
# computation.
|
|
2375
|
+
if self.last_compute_event is not None and self.workspace_stream is not None:
|
|
2376
|
+
with utils.device_ctx(self.device_id):
|
|
2377
|
+
self.workspace_stream.wait(self.last_compute_event)
|
|
2378
|
+
self.last_compute_event = None
|
|
2379
|
+
|
|
2380
|
+
self._free_workspace_memory()
|
|
2381
|
+
|
|
2382
|
+
with utils.device_ctx(self.device_id):
|
|
2383
|
+
if self.memory_desc is not None:
|
|
2384
|
+
if self.dummy_desc_data_ptr is not None:
|
|
2385
|
+
cufft.set_descriptor_data(self.memory_desc, self.dummy_desc_data_ptr, self.subformat)
|
|
2386
|
+
cufft.xt_free(self.memory_desc)
|
|
2387
|
+
self.memory_desc = None
|
|
2388
|
+
|
|
2389
|
+
if self.handle is not None:
|
|
2390
|
+
cufft.destroy(self.handle)
|
|
2391
|
+
if self.options.redistribute:
|
|
2392
|
+
cufft.destroy_reshape(self.reshape_handle)
|
|
2393
|
+
self.handle = None
|
|
2394
|
+
self.reshape_handle = None
|
|
2395
|
+
|
|
2396
|
+
if self.memory_desc_handle is not None:
|
|
2397
|
+
cufft.destroy(self.memory_desc_handle)
|
|
2398
|
+
self.memory_desc_handle = None
|
|
2399
|
+
|
|
2400
|
+
if self.memory_space == "cpu" and not self._operand_released:
|
|
2401
|
+
# In this case, self.operand is an internal GPU operand owned by FFT.
|
|
2402
|
+
# Since the execution when user passes CPU operands is blocking, it's
|
|
2403
|
+
# safe to call nvshmem_free here without additional synchronization.
|
|
2404
|
+
# If _operand_released is True, release_operand() already freed it.
|
|
2405
|
+
self.operand.free_symmetric()
|
|
2406
|
+
|
|
2407
|
+
# Set all attributes to None except for logger and valid_state
|
|
2408
|
+
_keep = {"logger", "valid_state"}
|
|
2409
|
+
for attr in list(vars(self)):
|
|
2410
|
+
if attr not in _keep:
|
|
2411
|
+
setattr(self, attr, None)
|
|
2412
|
+
|
|
2413
|
+
except Exception as e:
|
|
2414
|
+
self.logger.critical("Internal error: only part of the FFT object's resources have been released.")
|
|
2415
|
+
self.logger.critical(str(e))
|
|
2416
|
+
raise e
|
|
2417
|
+
finally:
|
|
2418
|
+
self.valid_state = False
|
|
2419
|
+
|
|
2420
|
+
self.logger.info("The FFT object's resources have been released.")
|
|
2421
|
+
|
|
2422
|
+
|
|
2423
|
+
def _fft(
|
|
2424
|
+
operand,
|
|
2425
|
+
/,
|
|
2426
|
+
*,
|
|
2427
|
+
distribution: Distribution | Sequence[Box],
|
|
2428
|
+
direction: FFTDirection | None = None,
|
|
2429
|
+
sync_symmetric_memory: bool = True,
|
|
2430
|
+
options: FFTOptions | dict[str, Any] | None = None,
|
|
2431
|
+
stream: AnyStream | None = None,
|
|
2432
|
+
check_dtype: str | None = None,
|
|
2433
|
+
):
|
|
2434
|
+
r"""
|
|
2435
|
+
fft({function_signature})
|
|
2436
|
+
|
|
2437
|
+
Perform an N-D *complex-to-complex* (C2C) distributed FFT on the provided complex
|
|
2438
|
+
operand.
|
|
2439
|
+
|
|
2440
|
+
Args:
|
|
2441
|
+
operand: {operand}
|
|
2442
|
+
{operand_admonitions}
|
|
2443
|
+
|
|
2444
|
+
distribution: {distribution}
|
|
2445
|
+
|
|
2446
|
+
sync_symmetric_memory: {sync_symmetric_memory}
|
|
2447
|
+
|
|
2448
|
+
options: {options}
|
|
2449
|
+
|
|
2450
|
+
stream: {stream}
|
|
2451
|
+
|
|
2452
|
+
Returns:
|
|
2453
|
+
A transformed operand that retains the same data type as the input. The resulting
|
|
2454
|
+
shape will depend on the choice of distribution and redistribute option. The operand
|
|
2455
|
+
remains on the same device and uses the same package as the input operand.
|
|
2456
|
+
|
|
2457
|
+
.. seealso::
|
|
2458
|
+
:func:`ifft`, :func:`irfft`, :func:`rfft`, :class:`FFT`
|
|
2459
|
+
|
|
2460
|
+
Examples:
|
|
2461
|
+
|
|
2462
|
+
>>> import cupy as cp
|
|
2463
|
+
>>> import nvmath.distributed
|
|
2464
|
+
|
|
2465
|
+
Get process group used to initialize nvmath.distributed (for information on
|
|
2466
|
+
initializing nvmath.distributed, you can refer to the documentation or to the
|
|
2467
|
+
FFT examples in `nvmath/examples/distributed/fft
|
|
2468
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/fft>`_):
|
|
2469
|
+
|
|
2470
|
+
>>> process_group = nvmath.distributed.get_context().process_group
|
|
2471
|
+
>>> nranks = process_group.nranks
|
|
2472
|
+
|
|
2473
|
+
Create a 3-D complex128 ndarray on GPU symmetric memory, distributed according to
|
|
2474
|
+
the Slab distribution on the Y axis (the global shape is (256, 256, 256)):
|
|
2475
|
+
|
|
2476
|
+
>>> from nvmath.distributed.distribution import Slab
|
|
2477
|
+
>>> shape = 256, 256 // nranks, 256
|
|
2478
|
+
>>> dtype = cp.complex128
|
|
2479
|
+
>>> a = nvmath.distributed.allocate_symmetric_memory(shape, cp, dtype=dtype)
|
|
2480
|
+
>>> a[:] = cp.random.rand(*shape, dtype=cp.float64) + 1j * cp.random.rand(
|
|
2481
|
+
... *shape, dtype=cp.float64
|
|
2482
|
+
... )
|
|
2483
|
+
|
|
2484
|
+
Perform a 3-D C2C FFT using :func:`fft`. The result `r` is also a CuPy complex128
|
|
2485
|
+
ndarray:
|
|
2486
|
+
|
|
2487
|
+
>>> r = nvmath.distributed.fft.fft(a, distribution=Slab.Y)
|
|
2488
|
+
|
|
2489
|
+
See :class:`FFTOptions` for the complete list of available options.
|
|
2490
|
+
|
|
2491
|
+
The package current stream is used by default, but a stream can be explicitly
|
|
2492
|
+
provided to the FFT operation. This can be done if the FFT operand is computed on a
|
|
2493
|
+
different stream, for example:
|
|
2494
|
+
|
|
2495
|
+
>>> s = cp.cuda.Stream()
|
|
2496
|
+
>>> with s:
|
|
2497
|
+
... a = nvmath.distributed.allocate_symmetric_memory(shape, cp, dtype=dtype)
|
|
2498
|
+
... a[:] = cp.random.rand(*shape) + 1j * cp.random.rand(*shape)
|
|
2499
|
+
>>> r = nvmath.distributed.fft.fft(a, stream=s)
|
|
2500
|
+
|
|
2501
|
+
The operation above runs on stream `s` and is ordered with respect to the input
|
|
2502
|
+
computation.
|
|
2503
|
+
|
|
2504
|
+
Create a NumPy ndarray on the CPU.
|
|
2505
|
+
|
|
2506
|
+
>>> import numpy as np
|
|
2507
|
+
>>> b = np.random.rand(*shape) + 1j * np.random.rand(*shape)
|
|
2508
|
+
|
|
2509
|
+
Provide the NumPy ndarray to :func:`fft`, with the result also being a NumPy
|
|
2510
|
+
ndarray:
|
|
2511
|
+
|
|
2512
|
+
>>> r = nvmath.distributed.fft.fft(b, distribution=Slab.Y)
|
|
2513
|
+
|
|
2514
|
+
Notes:
|
|
2515
|
+
- This function only takes complex operand for C2C transformation. If the user
|
|
2516
|
+
wishes to perform full FFT transformation on real input, please cast the input to
|
|
2517
|
+
the corresponding complex data type.
|
|
2518
|
+
- This function is a convenience wrapper around :class:`FFT` and is specifically
|
|
2519
|
+
meant for *single* use. The same computation can be performed with the stateful
|
|
2520
|
+
API using the default `direction` argument in :meth:`FFT.execute`.
|
|
2521
|
+
|
|
2522
|
+
Further examples can be found in the `nvmath/examples/distributed/fft
|
|
2523
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/fft>`_
|
|
2524
|
+
directory.
|
|
2525
|
+
"""
|
|
2526
|
+
if check_dtype is not None:
|
|
2527
|
+
assert check_dtype in {"real", "complex"}, "internal error"
|
|
2528
|
+
wrapped = tensor_wrapper.wrap_operand(operand)
|
|
2529
|
+
if ("complex" in wrapped.dtype) != (check_dtype == "complex"):
|
|
2530
|
+
raise ValueError(f"This function expects {check_dtype} operand, found {wrapped.dtype}")
|
|
2531
|
+
|
|
2532
|
+
with FFT(operand, distribution=distribution, options=options, stream=stream) as fftobj:
|
|
2533
|
+
# Plan the FFT.
|
|
2534
|
+
fftobj.plan(stream=stream)
|
|
2535
|
+
|
|
2536
|
+
# Execute the FFT.
|
|
2537
|
+
result = fftobj.execute(direction=direction, stream=stream, sync_symmetric_memory=sync_symmetric_memory)
|
|
2538
|
+
|
|
2539
|
+
return result
|
|
2540
|
+
|
|
2541
|
+
|
|
2542
|
+
# Forward C2C FFT Function.
|
|
2543
|
+
fft = functools.wraps(_fft)(functools.partial(_fft, direction=FFTDirection.FORWARD, check_dtype="complex"))
|
|
2544
|
+
if fft.__doc__ is not None:
|
|
2545
|
+
fft.__doc__ = fft.__doc__.format(**SHARED_FFT_DOCUMENTATION) # type: ignore
|
|
2546
|
+
fft.__name__ = "fft"
|
|
2547
|
+
|
|
2548
|
+
|
|
2549
|
+
# Forward R2C FFT Function
|
|
2550
|
+
@utils.docstring_decorator(SHARED_FFT_DOCUMENTATION, skip_missing=False)
|
|
2551
|
+
def rfft(
|
|
2552
|
+
operand,
|
|
2553
|
+
/,
|
|
2554
|
+
*,
|
|
2555
|
+
distribution: Distribution | Sequence[Box],
|
|
2556
|
+
sync_symmetric_memory: bool = True,
|
|
2557
|
+
options: FFTOptions | dict[str, Any] | None = None,
|
|
2558
|
+
stream: AnyStream | None = None,
|
|
2559
|
+
):
|
|
2560
|
+
r"""
|
|
2561
|
+
rfft({function_signature})
|
|
2562
|
+
|
|
2563
|
+
Perform an N-D *real-to-complex* (R2C) distributed FFT on the provided real operand.
|
|
2564
|
+
|
|
2565
|
+
Args:
|
|
2566
|
+
operand: {operand}
|
|
2567
|
+
{operand_admonitions}
|
|
2568
|
+
|
|
2569
|
+
distribution: {distribution}
|
|
2570
|
+
|
|
2571
|
+
sync_symmetric_memory: {sync_symmetric_memory}
|
|
2572
|
+
|
|
2573
|
+
options: {options}
|
|
2574
|
+
|
|
2575
|
+
stream: {stream}
|
|
2576
|
+
|
|
2577
|
+
Returns:
|
|
2578
|
+
A complex tensor whose shape will depend on the choice of distribution and
|
|
2579
|
+
redistribute option. The operand remains on the same device and belongs to the
|
|
2580
|
+
same package as the input operand. The global extent of the last transformed axis
|
|
2581
|
+
in the result will be ``global_extent[-1] // 2 + 1``.
|
|
2582
|
+
|
|
2583
|
+
.. seealso::
|
|
2584
|
+
:func:`fft`, :func:`irfft`, :class:`FFT`.
|
|
2585
|
+
"""
|
|
2586
|
+
wrapped_operand = tensor_wrapper.wrap_operand(operand)
|
|
2587
|
+
# check if input operand if real type
|
|
2588
|
+
if "complex" in wrapped_operand.dtype:
|
|
2589
|
+
raise RuntimeError(f"rfft expects a real input, but got {wrapped_operand.dtype}. Please use fft for complex input.")
|
|
2590
|
+
|
|
2591
|
+
return _fft(
|
|
2592
|
+
operand,
|
|
2593
|
+
distribution=distribution,
|
|
2594
|
+
sync_symmetric_memory=sync_symmetric_memory,
|
|
2595
|
+
options=options,
|
|
2596
|
+
stream=stream,
|
|
2597
|
+
check_dtype="real",
|
|
2598
|
+
)
|
|
2599
|
+
|
|
2600
|
+
|
|
2601
|
+
# Inverse C2C FFT Function.
|
|
2602
|
+
ifft = functools.wraps(_fft)(functools.partial(_fft, direction=FFTDirection.INVERSE, check_dtype="complex"))
|
|
2603
|
+
ifft.__doc__ = """
|
|
2604
|
+
ifft({function_signature})
|
|
2605
|
+
|
|
2606
|
+
Perform an N-D *complex-to-complex* (C2C) inverse FFT on the provided complex operand.
|
|
2607
|
+
The direction is implicitly inverse.
|
|
2608
|
+
|
|
2609
|
+
Args:
|
|
2610
|
+
operand: {operand}
|
|
2611
|
+
{operand_admonitions}
|
|
2612
|
+
|
|
2613
|
+
distribution: {distribution}
|
|
2614
|
+
|
|
2615
|
+
sync_symmetric_memory: {sync_symmetric_memory}
|
|
2616
|
+
|
|
2617
|
+
options: {options}
|
|
2618
|
+
|
|
2619
|
+
stream: {stream}
|
|
2620
|
+
|
|
2621
|
+
Returns:
|
|
2622
|
+
A transformed operand that retains the same data type as the input. The resulting
|
|
2623
|
+
shape will depend on the choice of distribution and redistribute option. The operand
|
|
2624
|
+
remains on the same device and uses the same package as the input operand.
|
|
2625
|
+
|
|
2626
|
+
.. seealso::
|
|
2627
|
+
:func:`fft`, :func:`irfft`, :class:`FFT`.
|
|
2628
|
+
|
|
2629
|
+
Notes:
|
|
2630
|
+
- This function only takes complex operand for C2C transformation. If the user wishes
|
|
2631
|
+
to perform full FFT transformation on real input, please cast the input to the
|
|
2632
|
+
corresponding complex data type.
|
|
2633
|
+
- This function is a convenience wrapper around :class:`FFT` and is specifically
|
|
2634
|
+
meant for *single* use. The same computation can be performed with the stateful
|
|
2635
|
+
API by passing the argument ``direction='inverse'`` when calling
|
|
2636
|
+
:meth:`FFT.execute`.
|
|
2637
|
+
""".format(**SHARED_FFT_DOCUMENTATION)
|
|
2638
|
+
ifft.__name__ = "ifft"
|
|
2639
|
+
|
|
2640
|
+
|
|
2641
|
+
# Inverse C2R FFT Function.
|
|
2642
|
+
@utils.docstring_decorator(SHARED_FFT_DOCUMENTATION, skip_missing=False)
|
|
2643
|
+
def irfft(
|
|
2644
|
+
operand,
|
|
2645
|
+
/,
|
|
2646
|
+
*,
|
|
2647
|
+
distribution: Distribution | Sequence[Box],
|
|
2648
|
+
sync_symmetric_memory: bool = True,
|
|
2649
|
+
options: FFTOptions | dict[str, Any] | None = None,
|
|
2650
|
+
stream: AnyStream | None = None,
|
|
2651
|
+
):
|
|
2652
|
+
"""
|
|
2653
|
+
irfft({function_signature})
|
|
2654
|
+
|
|
2655
|
+
Perform an N-D *complex-to-real* (C2R) distributed FFT on the provided complex operand.
|
|
2656
|
+
The direction is implicitly inverse.
|
|
2657
|
+
|
|
2658
|
+
Args:
|
|
2659
|
+
operand: {operand}
|
|
2660
|
+
{operand_admonitions}
|
|
2661
|
+
|
|
2662
|
+
distribution: {distribution}
|
|
2663
|
+
|
|
2664
|
+
sync_symmetric_memory: {sync_symmetric_memory}
|
|
2665
|
+
|
|
2666
|
+
options: {options}
|
|
2667
|
+
|
|
2668
|
+
stream: {stream}
|
|
2669
|
+
|
|
2670
|
+
Returns:
|
|
2671
|
+
A real tensor whose shape will depend on the choice of distribution and redistribute
|
|
2672
|
+
option. The operand remains on the same device and belongs to the same package as
|
|
2673
|
+
the input operand. The global extent of the last transformed axis in the result
|
|
2674
|
+
will be ``(global_extent[-1] - 1) * 2`` if :attr:`FFTOptions.last_axis_parity` is
|
|
2675
|
+
``even``, or ``global_extent[-1] * 2 - 1`` if :attr:`FFTOptions.last_axis_parity`
|
|
2676
|
+
is ``odd``.
|
|
2677
|
+
|
|
2678
|
+
.. seealso::
|
|
2679
|
+
:func:`fft`, :func:`ifft`, :class:`FFT`.
|
|
2680
|
+
|
|
2681
|
+
Example:
|
|
2682
|
+
|
|
2683
|
+
>>> import cupy as cp
|
|
2684
|
+
>>> import nvmath.distributed
|
|
2685
|
+
|
|
2686
|
+
Get process group used to initialize nvmath.distributed (for information on
|
|
2687
|
+
initializing nvmath.distributed, you can refer to the documentation or to the
|
|
2688
|
+
FFT examples in `nvmath/examples/distributed/fft
|
|
2689
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/fft>`_):
|
|
2690
|
+
|
|
2691
|
+
>>> process_group = nvmath.distributed.get_context().process_group
|
|
2692
|
+
>>> nranks = process_group.nranks
|
|
2693
|
+
>>> from nvmath.distributed.fft import Slab
|
|
2694
|
+
|
|
2695
|
+
Create a 3-D symmetric complex128 ndarray on GPU symmetric memory:
|
|
2696
|
+
|
|
2697
|
+
>>> shape = 512 // nranks, 768, 256
|
|
2698
|
+
>>> a = nvmath.distributed.allocate_operand(
|
|
2699
|
+
... shape, cp, input_dtype=cp.float64, distribution=Slab.X, fft_type="R2C"
|
|
2700
|
+
... )
|
|
2701
|
+
>>> a[:] = cp.random.rand(*shape, dtype=cp.float64)
|
|
2702
|
+
>>> b = nvmath.distributed.fft.rfft(a, distribution=Slab.X)
|
|
2703
|
+
|
|
2704
|
+
Perform a 3-D C2R FFT using the :func:`irfft` wrapper. The result `r` is a CuPy
|
|
2705
|
+
float64 ndarray:
|
|
2706
|
+
|
|
2707
|
+
>>> r = nvmath.distributed.fft.irfft(b, distribution=Slab.X)
|
|
2708
|
+
>>> r.dtype
|
|
2709
|
+
dtype('float64')
|
|
2710
|
+
|
|
2711
|
+
Notes:
|
|
2712
|
+
|
|
2713
|
+
- This function performs an inverse C2R N-D FFT, which is similar to `irfftn` but
|
|
2714
|
+
different from `irfft` in various numerical packages.
|
|
2715
|
+
- This function is a convenience wrapper around :class:`FFT` and is specifically
|
|
2716
|
+
meant for *single* use. The same computation can be performed with the stateful
|
|
2717
|
+
API by setting :attr:`FFTOptions.fft_type` to ``'C2R'`` and passing the argument
|
|
2718
|
+
``direction='inverse'`` when calling :meth:`FFT.execute`.
|
|
2719
|
+
- **The input to this function must be Hermitian-symmetric, otherwise the result is
|
|
2720
|
+
undefined.** While the symmetry requirement is partially captured by the different
|
|
2721
|
+
global extents in the last transformed dimension between the input and result,
|
|
2722
|
+
there are additional `constraints
|
|
2723
|
+
<https://docs.nvidia.com/cuda/cufft/#fourier-transform-types>`_. In addition,
|
|
2724
|
+
if the input to `irfft` was generated using an R2C FFT with an odd global last
|
|
2725
|
+
axis size, :attr:`FFTOptions.last_axis_parity` must be set to ``odd`` to recover
|
|
2726
|
+
the original signal.
|
|
2727
|
+
- For more details, please refer to `R2C/C2R example
|
|
2728
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/fft/example01_cupy_r2c_c2r.py>`_
|
|
2729
|
+
and `odd C2R example
|
|
2730
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/fft/example01_torch_r2c_c2r.py>`_.
|
|
2731
|
+
"""
|
|
2732
|
+
options = cast(FFTOptions, utils.check_or_create_options(FFTOptions, options, "Distributed FFT options"))
|
|
2733
|
+
options.fft_type = "C2R"
|
|
2734
|
+
return _fft(
|
|
2735
|
+
operand,
|
|
2736
|
+
distribution=distribution,
|
|
2737
|
+
direction=FFTDirection.INVERSE,
|
|
2738
|
+
sync_symmetric_memory=sync_symmetric_memory,
|
|
2739
|
+
options=options,
|
|
2740
|
+
stream=stream,
|
|
2741
|
+
check_dtype="complex",
|
|
2742
|
+
)
|