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,1024 @@
|
|
|
1
|
+
# Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations # allows typehint of class methods to return the self class
|
|
6
|
+
|
|
7
|
+
import copy
|
|
8
|
+
import math
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
from enum import IntEnum
|
|
12
|
+
from typing import cast
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
import nvmath.distributed as dist
|
|
17
|
+
from nvmath.bindings import cufftMp # type: ignore
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"ProcessGrid",
|
|
21
|
+
"Distribution",
|
|
22
|
+
"Slab",
|
|
23
|
+
"Box",
|
|
24
|
+
"BlockCyclic",
|
|
25
|
+
"BlockNonCyclic",
|
|
26
|
+
"BindDistributionError",
|
|
27
|
+
"ConvertDistributionError",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ProcessGrid:
|
|
32
|
+
"""
|
|
33
|
+
N-dimensional grid of processes used by some distributions like the PBLAS block-cyclic
|
|
34
|
+
distribution.
|
|
35
|
+
|
|
36
|
+
Example 2D process grid for 4 processes, with processes arranged in column-major order::
|
|
37
|
+
|
|
38
|
+
---------
|
|
39
|
+
| 0 | 2 |
|
|
40
|
+
---------
|
|
41
|
+
| 1 | 3 |
|
|
42
|
+
---------
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
class Layout(IntEnum):
|
|
46
|
+
"""Process-grid layout used by distributed nvmath routines."""
|
|
47
|
+
|
|
48
|
+
COL_MAJOR = 0
|
|
49
|
+
ROW_MAJOR = 1
|
|
50
|
+
|
|
51
|
+
_layout: Layout | None
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
*,
|
|
56
|
+
shape: Sequence[int] | None = None,
|
|
57
|
+
layout: ProcessGrid.Layout | None = None,
|
|
58
|
+
process_array=None,
|
|
59
|
+
):
|
|
60
|
+
"""
|
|
61
|
+
Create a new ProcessGrid object.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
shape: Shape of the process grid.
|
|
65
|
+
|
|
66
|
+
layout: Layout of the process grid (column-major or row-major). This is optional
|
|
67
|
+
for 1D grid or when a custom grid is provided.
|
|
68
|
+
|
|
69
|
+
process_array: optional ndarray specifying custom arrangement of processes.
|
|
70
|
+
"""
|
|
71
|
+
self._nranks = _get_process_group().nranks
|
|
72
|
+
|
|
73
|
+
if process_array is None:
|
|
74
|
+
if shape is None:
|
|
75
|
+
raise ValueError("shape must be provided when process_array=None")
|
|
76
|
+
self._shape = tuple(shape)
|
|
77
|
+
if layout is None:
|
|
78
|
+
if self._is_1d_distribution():
|
|
79
|
+
layout = ProcessGrid.Layout.ROW_MAJOR # layout doesn't matter in this case.
|
|
80
|
+
else:
|
|
81
|
+
raise ValueError("layout must be provided when process_array=None and partitioning on multiple dimensions")
|
|
82
|
+
if not isinstance(layout, ProcessGrid.Layout):
|
|
83
|
+
raise TypeError(f"layout must be of type ProcessGrid.Layout, got {layout}")
|
|
84
|
+
self._layout = layout
|
|
85
|
+
self._process_array = None
|
|
86
|
+
else:
|
|
87
|
+
self._shape = tuple(process_array.shape)
|
|
88
|
+
if shape is not None and self._shape != tuple(shape):
|
|
89
|
+
raise ValueError(f"shape {shape} and process_array.shape ({process_array.shape}) don't match")
|
|
90
|
+
# TODO: Can set layout to COL_MAJOR or ROW_MAJOR automatically if the
|
|
91
|
+
# process_array matches.
|
|
92
|
+
if layout is not None:
|
|
93
|
+
raise NotImplementedError
|
|
94
|
+
self._layout = None
|
|
95
|
+
self._process_array = process_array
|
|
96
|
+
|
|
97
|
+
if math.prod(self._shape) != self._nranks:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"Number of grid elements ({math.prod(self._shape)}) must equal the number of processes ({self._nranks})"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def shape(self) -> tuple[int, ...]:
|
|
104
|
+
"""Shape of process grid."""
|
|
105
|
+
return self._shape
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def layout(self) -> ProcessGrid.Layout | None:
|
|
109
|
+
"""Layout of process grid if row-major or column-major, otherwise None."""
|
|
110
|
+
return self._layout
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def process_array(self):
|
|
114
|
+
return self._process_array
|
|
115
|
+
|
|
116
|
+
def __str__(self):
|
|
117
|
+
return f"ProcessGrid(shape={self._shape}, layout={self._layout.name}, process_array={self._process_array})"
|
|
118
|
+
|
|
119
|
+
def __hash__(self):
|
|
120
|
+
# NOTE: The layout isn't considered for the hash, to allow process grids
|
|
121
|
+
# partitioned on a single dimension with the same shape but different layout
|
|
122
|
+
# to be the same dictionary key.
|
|
123
|
+
return hash(self._shape)
|
|
124
|
+
|
|
125
|
+
def __eq__(self, other):
|
|
126
|
+
if self._process_array is None and other._process_array is None:
|
|
127
|
+
if self._shape == other._shape:
|
|
128
|
+
return self._is_1d_distribution() or self._layout == other._layout
|
|
129
|
+
return False
|
|
130
|
+
raise NotImplementedError
|
|
131
|
+
|
|
132
|
+
def _is_1d_distribution(self) -> bool:
|
|
133
|
+
"""True if process grid partitions on a single dimension."""
|
|
134
|
+
return self._nranks in self._shape
|
|
135
|
+
|
|
136
|
+
def _is_row_wise(self) -> bool:
|
|
137
|
+
"""True if 2D process grid partitioned only on rows."""
|
|
138
|
+
return self._shape == (self._nranks, 1)
|
|
139
|
+
|
|
140
|
+
def _is_col_wise(self) -> bool:
|
|
141
|
+
"""True if 2D process grid partitioned only on columns."""
|
|
142
|
+
return self._shape == (1, self._nranks)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class BindDistributionError(Exception):
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class ConvertDistributionError(Exception):
|
|
150
|
+
"""Errors converting a distribution instance to another distribution type"""
|
|
151
|
+
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class Distribution(ABC):
|
|
156
|
+
"""Specifies how a tensor is distributed across processes."""
|
|
157
|
+
|
|
158
|
+
def __init__(self):
|
|
159
|
+
self._bound = False
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
@abstractmethod
|
|
163
|
+
def ndim(self) -> int | None:
|
|
164
|
+
"""The number of dimensions of a distributed tensor for which this distribution
|
|
165
|
+
applies; None if it doesn't apply to any specific number of dimensions."""
|
|
166
|
+
raise NotImplementedError
|
|
167
|
+
|
|
168
|
+
@abstractmethod
|
|
169
|
+
def shape(self, rank: int, global_shape: Sequence[int] | None = None) -> tuple[int, ...]:
|
|
170
|
+
"""Get the local shape of data on the given rank according to this distribution.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
rank: the process rank for which to calculate the local shape.
|
|
174
|
+
|
|
175
|
+
global_shape: Global shape of data. Required if the distribution is
|
|
176
|
+
not bound to a global shape, otherwise not required.
|
|
177
|
+
"""
|
|
178
|
+
raise NotImplementedError
|
|
179
|
+
|
|
180
|
+
@abstractmethod
|
|
181
|
+
def to(
|
|
182
|
+
self,
|
|
183
|
+
cls: type[Distribution],
|
|
184
|
+
/,
|
|
185
|
+
*,
|
|
186
|
+
ndim: int | None = None,
|
|
187
|
+
copy: bool = False,
|
|
188
|
+
) -> Distribution:
|
|
189
|
+
"""Convert this distribution object to an equivalent distribution of the given type.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
cls: the target distribution type.
|
|
193
|
+
|
|
194
|
+
ndim: dimensionality of the target distribution. It must be greater than or
|
|
195
|
+
equal to the source dimensionality; it can never be smaller. When it is
|
|
196
|
+
larger, the distribution is promoted by appending trailing size-1
|
|
197
|
+
dimensions. Providing ``ndim`` is required when the
|
|
198
|
+
source distribution has no associated dimensionality of its own.
|
|
199
|
+
|
|
200
|
+
copy: Returns a copy if the source and target type are the same.
|
|
201
|
+
|
|
202
|
+
Raises:
|
|
203
|
+
ConvertDistributionError: if the conversion is not possible.
|
|
204
|
+
"""
|
|
205
|
+
raise NotImplementedError
|
|
206
|
+
|
|
207
|
+
def _to_checks(self, cls: type[Distribution], ndim: int | None) -> None:
|
|
208
|
+
if cls is Distribution or not issubclass(cls, Distribution):
|
|
209
|
+
raise ValueError(f"{cls} is not a valid distribution")
|
|
210
|
+
if ndim is not None and self.ndim is not None and ndim < self.ndim:
|
|
211
|
+
raise ValueError(f"ndim argument ({ndim}) must be >= the distribution's dimensionality ({self.ndim})")
|
|
212
|
+
|
|
213
|
+
@abstractmethod
|
|
214
|
+
def _bind(
|
|
215
|
+
self,
|
|
216
|
+
global_shape: Sequence[int],
|
|
217
|
+
*,
|
|
218
|
+
shape: Sequence[int] | None = None,
|
|
219
|
+
) -> Distribution:
|
|
220
|
+
"""Binds this distribution object to a global shape, which determines how a
|
|
221
|
+
distributed tensor with that shape must be partitioned among processes (the local
|
|
222
|
+
shape on each process). You can also provide the local shape on this process to
|
|
223
|
+
check if it fits the distribution (the function will raise an exception if not).
|
|
224
|
+
**The exception may be raised on some ranks but not others (it's up to the caller
|
|
225
|
+
to handle this)**.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
global_shape: global shape of the data.
|
|
229
|
+
|
|
230
|
+
shape: shape of the data on this process.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
self
|
|
234
|
+
|
|
235
|
+
Raises:
|
|
236
|
+
BindDistributionError: if distribution is already bound or local shape doesn't
|
|
237
|
+
fit the distribution.
|
|
238
|
+
"""
|
|
239
|
+
raise NotImplementedError
|
|
240
|
+
|
|
241
|
+
def _binding_str(self):
|
|
242
|
+
return f"[bound: global_shape={self._data_global_shape}, shape={self._data_shape}]" if self._bound else ""
|
|
243
|
+
|
|
244
|
+
def copy(self) -> Distribution:
|
|
245
|
+
"""This is a common implementation for those distributions that only require
|
|
246
|
+
a shallow copy."""
|
|
247
|
+
return copy.copy(self)
|
|
248
|
+
|
|
249
|
+
def _local_shape_checks(self, rank: int, nranks: int, global_shape: Sequence[int] | None = None):
|
|
250
|
+
if not isinstance(rank, int):
|
|
251
|
+
raise ValueError(f"rank must be an integer, got {rank}")
|
|
252
|
+
|
|
253
|
+
if rank < 0 or rank > nranks - 1:
|
|
254
|
+
raise ValueError(f"This is not a valid process rank: got rank={rank} with nranks={nranks}")
|
|
255
|
+
|
|
256
|
+
if global_shape is None and not self._bound:
|
|
257
|
+
raise RuntimeError("This distribution is unbound: please specify a global shape")
|
|
258
|
+
|
|
259
|
+
if global_shape is not None and self._bound and tuple(global_shape) != self._data_global_shape: # type: ignore
|
|
260
|
+
raise ValueError(
|
|
261
|
+
"This distribution is already bound to a different global shape: provided "
|
|
262
|
+
f"{global_shape}, bound to {self._data_global_shape}" # type: ignore
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# ------------------------------------------------------------------------------------
|
|
266
|
+
# Methods to infer global shape of an operand that is distributed according
|
|
267
|
+
# to this distribution. This is intended to be used in a ProcessGroup.allreduce_object()
|
|
268
|
+
# reduction (or equivalent type of reduction) in the following manner:
|
|
269
|
+
# - self refers to the distribution instance that is part of the reduction.
|
|
270
|
+
# - self._global_shape_reduction_prologue() is called at the start of the reduction on
|
|
271
|
+
# each process (in other words, at the reduction tree leaves).
|
|
272
|
+
# - self._global_shape_reduction() is called at every step of the reduction.
|
|
273
|
+
# - _global_shape_reduction_epilogue() is called after the reduction completes, and
|
|
274
|
+
# returns the global shape of the operand.
|
|
275
|
+
# IMPORTANT: This only calculates the global shape of *one operand*. If the
|
|
276
|
+
# operation has multiple operands, you must provide a separate distribution
|
|
277
|
+
# instance for each operand (even if the distribution characteristics are the same).
|
|
278
|
+
|
|
279
|
+
@abstractmethod
|
|
280
|
+
def _global_shape_reduction_prologue(self, rank: int, local_shape: Sequence[int]) -> None:
|
|
281
|
+
"""Initialize the global shape reduction.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
rank: rank of this process
|
|
285
|
+
|
|
286
|
+
local_shape: local shape of operand on this process.
|
|
287
|
+
"""
|
|
288
|
+
raise NotImplementedError
|
|
289
|
+
|
|
290
|
+
@abstractmethod
|
|
291
|
+
def _global_shape_reduction(self, other: Distribution) -> None:
|
|
292
|
+
raise NotImplementedError
|
|
293
|
+
|
|
294
|
+
@abstractmethod
|
|
295
|
+
def _global_shape_reduction_epilogue(self) -> tuple[int, ...]:
|
|
296
|
+
"""Return inferred global shape (to be called after the reduction completes"""
|
|
297
|
+
raise NotImplementedError
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _calculate_local_box(global_shape, partition_dim, rank, nranks):
|
|
301
|
+
"""Given a global shape of data that is partitioned across ranks along the
|
|
302
|
+
`partition_dim` dimension according to the Slab distribution, return
|
|
303
|
+
the local box of this rank (as lower and upper coordinates in the global shape).
|
|
304
|
+
"""
|
|
305
|
+
lower = [0] * len(global_shape)
|
|
306
|
+
N = nranks
|
|
307
|
+
S = global_shape[partition_dim]
|
|
308
|
+
|
|
309
|
+
num_ranks_with_remainder = S % N
|
|
310
|
+
num_preceding_ranks_with_remainder = min(num_ranks_with_remainder, rank)
|
|
311
|
+
num_preceding_ranks_without_remainder = rank - num_preceding_ranks_with_remainder
|
|
312
|
+
|
|
313
|
+
lower[partition_dim] = num_preceding_ranks_with_remainder * (S // N + 1) + num_preceding_ranks_without_remainder * (S // N)
|
|
314
|
+
upper = list(global_shape)
|
|
315
|
+
upper[partition_dim] = lower[partition_dim] + (S // N + 1 if rank < num_ranks_with_remainder else S // N)
|
|
316
|
+
|
|
317
|
+
return lower, upper
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class Slab(Distribution):
|
|
321
|
+
"""
|
|
322
|
+
Slab distribution
|
|
323
|
+
|
|
324
|
+
Data is partitioned across processes on a single axis, such that:
|
|
325
|
+
|
|
326
|
+
- The shape of the slab on the first s_p % P processes is
|
|
327
|
+
(s_0, ..., s_p // P + 1, ..., s_{n-1})
|
|
328
|
+
- The shape of the slab on the remaining processes is (s_0, ..., s_p // P, ..., s_{n-1})
|
|
329
|
+
- Process 0 owns the first slab according to the global index order, process 1 owns
|
|
330
|
+
the second slab and so on.
|
|
331
|
+
|
|
332
|
+
where:
|
|
333
|
+
|
|
334
|
+
- s_i is the size of dimension i of the global array
|
|
335
|
+
- p is the partition dimension
|
|
336
|
+
- n is the number of dimensions of the array
|
|
337
|
+
- P is the number of processes
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
X: Slab
|
|
341
|
+
"""Slab distribution on axis 0."""
|
|
342
|
+
|
|
343
|
+
Y: Slab
|
|
344
|
+
"""Slab distribution on axis 1."""
|
|
345
|
+
|
|
346
|
+
Z: Slab
|
|
347
|
+
"""Slab distribution on axis 2."""
|
|
348
|
+
|
|
349
|
+
def __init__(
|
|
350
|
+
self,
|
|
351
|
+
partition_dim: int,
|
|
352
|
+
ndim: int | None = None,
|
|
353
|
+
):
|
|
354
|
+
super().__init__()
|
|
355
|
+
if not isinstance(partition_dim, int) or partition_dim < 0:
|
|
356
|
+
raise ValueError(f"partition_dim must be integer >= 0, got {partition_dim}")
|
|
357
|
+
|
|
358
|
+
if ndim is not None:
|
|
359
|
+
if not isinstance(ndim, int) or ndim < 1:
|
|
360
|
+
raise ValueError(f"ndim must be integer >= 1, got {ndim}")
|
|
361
|
+
if partition_dim >= ndim:
|
|
362
|
+
raise ValueError("partition_dim must be < ndim")
|
|
363
|
+
|
|
364
|
+
self._partition_dim = partition_dim
|
|
365
|
+
self._ndim = ndim
|
|
366
|
+
|
|
367
|
+
def __eq__(self, other):
|
|
368
|
+
if not isinstance(other, Slab):
|
|
369
|
+
return False
|
|
370
|
+
if self._partition_dim != other._partition_dim:
|
|
371
|
+
return False
|
|
372
|
+
if self._ndim is None or other._ndim is None:
|
|
373
|
+
return True
|
|
374
|
+
return self._ndim == other._ndim
|
|
375
|
+
|
|
376
|
+
def __hash__(self):
|
|
377
|
+
return self._partition_dim
|
|
378
|
+
|
|
379
|
+
def __str__(self):
|
|
380
|
+
return f"Slab(partition_dim={self._partition_dim}, ndim={self._ndim})" + self._binding_str()
|
|
381
|
+
|
|
382
|
+
@property
|
|
383
|
+
def name(self) -> str:
|
|
384
|
+
match self._partition_dim:
|
|
385
|
+
case 0:
|
|
386
|
+
return "Slab.X"
|
|
387
|
+
case 1:
|
|
388
|
+
return "Slab.Y"
|
|
389
|
+
case 2:
|
|
390
|
+
return "Slab.Z"
|
|
391
|
+
case _:
|
|
392
|
+
return "Slab"
|
|
393
|
+
|
|
394
|
+
@property
|
|
395
|
+
def partition_dim(self) -> int:
|
|
396
|
+
"""Slab partition dimension"""
|
|
397
|
+
return self._partition_dim
|
|
398
|
+
|
|
399
|
+
@property
|
|
400
|
+
def ndim(self) -> int | None:
|
|
401
|
+
return self._ndim
|
|
402
|
+
|
|
403
|
+
def shape(self, rank: int, global_shape: Sequence[int] | None = None) -> tuple[int, ...]:
|
|
404
|
+
n = _get_process_group().nranks
|
|
405
|
+
self._local_shape_checks(rank, n, global_shape)
|
|
406
|
+
if global_shape is None:
|
|
407
|
+
global_shape = self._data_global_shape
|
|
408
|
+
|
|
409
|
+
S = global_shape[self._partition_dim]
|
|
410
|
+
partition_dim_local_size = S // n + bool(rank < S % n)
|
|
411
|
+
slab_shape = list(global_shape)
|
|
412
|
+
slab_shape[self._partition_dim] = partition_dim_local_size
|
|
413
|
+
return tuple(slab_shape)
|
|
414
|
+
|
|
415
|
+
@property
|
|
416
|
+
def _cufftmp_value(self):
|
|
417
|
+
if self._partition_dim not in (0, 1):
|
|
418
|
+
raise TypeError(f"Unsupported distribution {self} for cuFFTMp: partition dimension must be X or Y")
|
|
419
|
+
return cufftMp.XtSubFormat.FORMAT_INPLACE if self._partition_dim == 0 else cufftMp.XtSubFormat.FORMAT_INPLACE_SHUFFLED
|
|
420
|
+
|
|
421
|
+
def _bind(self, global_shape, *, shape=None) -> Slab:
|
|
422
|
+
if self._bound:
|
|
423
|
+
raise BindDistributionError(f"{self} is already bound")
|
|
424
|
+
|
|
425
|
+
if self._ndim is not None and len(global_shape) != self._ndim:
|
|
426
|
+
raise ValueError(f"The given shape doesn't have the same dimensionality as this {self} distribution")
|
|
427
|
+
|
|
428
|
+
if self._partition_dim >= len(global_shape):
|
|
429
|
+
raise ValueError("partition_dim must be < ndim")
|
|
430
|
+
|
|
431
|
+
rank = _get_process_group().rank
|
|
432
|
+
slab_shape = self.shape(rank, global_shape)
|
|
433
|
+
|
|
434
|
+
if shape is not None and tuple(shape) != slab_shape:
|
|
435
|
+
raise BindDistributionError(
|
|
436
|
+
f"The given shapes (global_shape={global_shape}, shape={shape}) don't fit distribution {str(self)}"
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
self._ndim = len(global_shape)
|
|
440
|
+
self._data_global_shape = tuple(global_shape)
|
|
441
|
+
self._data_shape = slab_shape
|
|
442
|
+
self._bound = True
|
|
443
|
+
return self
|
|
444
|
+
|
|
445
|
+
def to(self, cls, /, *, ndim=None, copy=False):
|
|
446
|
+
super()._to_checks(cls, ndim)
|
|
447
|
+
process_group = _get_process_group()
|
|
448
|
+
rank, nranks = process_group.rank, process_group.nranks
|
|
449
|
+
|
|
450
|
+
if ndim is None:
|
|
451
|
+
ndim = self._ndim
|
|
452
|
+
elif self._partition_dim >= ndim:
|
|
453
|
+
raise ValueError(f"ndim ({ndim}) must be greater than the partition dimension ({self._partition_dim})")
|
|
454
|
+
|
|
455
|
+
if cls is Slab:
|
|
456
|
+
if ndim is not None and ndim != self._ndim:
|
|
457
|
+
d = Slab(partition_dim=self._partition_dim, ndim=ndim)
|
|
458
|
+
if self._bound:
|
|
459
|
+
d._bind(self._data_global_shape + (1,) * (ndim - self._ndim))
|
|
460
|
+
return d
|
|
461
|
+
elif copy:
|
|
462
|
+
d = cast(Slab, self.copy())
|
|
463
|
+
d._ndim = ndim
|
|
464
|
+
return d
|
|
465
|
+
return self
|
|
466
|
+
elif issubclass(cls, BlockCyclic):
|
|
467
|
+
if ndim is None:
|
|
468
|
+
raise ConvertDistributionError("Can't convert Slab distribution to BlockCyclic: unknown dimensionality")
|
|
469
|
+
|
|
470
|
+
process_grid_shape = tuple(1 if x != self._partition_dim else nranks for x in range(ndim))
|
|
471
|
+
# layout doesn't matter when partitioning on a single axis.
|
|
472
|
+
process_grid = ProcessGrid(shape=process_grid_shape, layout=ProcessGrid.Layout.ROW_MAJOR)
|
|
473
|
+
if not self._bound:
|
|
474
|
+
return BlockNonCyclic(process_grid)
|
|
475
|
+
else:
|
|
476
|
+
data_global_shape = self._data_global_shape + (1,) * (ndim - self._ndim)
|
|
477
|
+
data_shape = self._data_shape + (1,) * (ndim - self._ndim)
|
|
478
|
+
b = BlockNonCyclic(process_grid)
|
|
479
|
+
return b._bind(data_global_shape, shape=data_shape)
|
|
480
|
+
elif cls is Box:
|
|
481
|
+
if not self._bound:
|
|
482
|
+
raise ConvertDistributionError("Unbound Slab distribution can't be converted to Box.")
|
|
483
|
+
|
|
484
|
+
data_global_shape = self._data_global_shape + (1,) * (ndim - self._ndim)
|
|
485
|
+
lower, upper = _calculate_local_box(data_global_shape, self._partition_dim, rank, nranks)
|
|
486
|
+
return Box(lower, upper)._bind(data_global_shape)
|
|
487
|
+
|
|
488
|
+
def _global_shape_reduction_prologue(self, rank: int, local_shape: Sequence[int]) -> None:
|
|
489
|
+
if self.ndim is not None and self.ndim != len(local_shape):
|
|
490
|
+
raise ValueError(
|
|
491
|
+
f"The dimensionality of {self} doesn't match the dimensionality of the local operand ({len(local_shape)})"
|
|
492
|
+
)
|
|
493
|
+
self._ndim = len(local_shape)
|
|
494
|
+
self._global_shape = list(local_shape) # this is the actual reduction variable
|
|
495
|
+
|
|
496
|
+
def _global_shape_reduction(self, other: Distribution) -> None:
|
|
497
|
+
if self is other:
|
|
498
|
+
return
|
|
499
|
+
|
|
500
|
+
if self != other: # partition_dim and dimensionality must match
|
|
501
|
+
raise ValueError(f"The distribution is inconsistent across processes: {self} != {other}")
|
|
502
|
+
|
|
503
|
+
ndim = self.ndim
|
|
504
|
+
assert ndim is not None, "Internal error"
|
|
505
|
+
if any(self._global_shape[i] != other._global_shape[i] for i in range(ndim) if i != self._partition_dim): # type: ignore[attr-defined]
|
|
506
|
+
raise ValueError("The problem size is inconsistent across processes")
|
|
507
|
+
|
|
508
|
+
self._global_shape[self._partition_dim] += other._global_shape[self._partition_dim] # type: ignore[attr-defined]
|
|
509
|
+
|
|
510
|
+
def _global_shape_reduction_epilogue(self) -> tuple[int, ...]:
|
|
511
|
+
return tuple(self._global_shape)
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
# Define alternate forms for the user to specify Slab on X, Y or Z.
|
|
515
|
+
# NOTE: dimensionality is left unspecified when using these (but will be set when the
|
|
516
|
+
# distribution is bound to data).
|
|
517
|
+
Slab.X = Slab(0)
|
|
518
|
+
Slab.Y = Slab(1)
|
|
519
|
+
Slab.Z = Slab(2)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
class Box(Distribution):
|
|
523
|
+
"""Box distribution"""
|
|
524
|
+
|
|
525
|
+
def __init__(
|
|
526
|
+
self,
|
|
527
|
+
lower: Sequence[int],
|
|
528
|
+
upper: Sequence[int],
|
|
529
|
+
):
|
|
530
|
+
super().__init__()
|
|
531
|
+
if len(lower) != len(upper):
|
|
532
|
+
raise ValueError("lower and upper coordinates must have the same dimensionality")
|
|
533
|
+
for coords in (lower, upper):
|
|
534
|
+
if not all(isinstance(x, int) for x in coords):
|
|
535
|
+
raise ValueError("lower and upper coordinates must be integer")
|
|
536
|
+
if not all(upper[i] > lower[i] for i in range(len(upper))):
|
|
537
|
+
raise ValueError(
|
|
538
|
+
f"The upper coordinates must be larger than the lower coordinates, but got lower={lower} upper={upper}"
|
|
539
|
+
)
|
|
540
|
+
self._lower = tuple(lower)
|
|
541
|
+
self._upper = tuple(upper)
|
|
542
|
+
|
|
543
|
+
@property
|
|
544
|
+
def lower(self) -> tuple[int, ...]:
|
|
545
|
+
"""Box lower coordinates"""
|
|
546
|
+
return self._lower
|
|
547
|
+
|
|
548
|
+
@property
|
|
549
|
+
def upper(self) -> tuple[int, ...]:
|
|
550
|
+
"""Box upper coordinates"""
|
|
551
|
+
return self._upper
|
|
552
|
+
|
|
553
|
+
@property
|
|
554
|
+
def ndim(self) -> int:
|
|
555
|
+
return len(self._lower)
|
|
556
|
+
|
|
557
|
+
@property
|
|
558
|
+
def box_shape(self) -> tuple[int, ...]:
|
|
559
|
+
return tuple(self._upper[i] - self._lower[i] for i in range(self.ndim))
|
|
560
|
+
|
|
561
|
+
def shape(self, rank: int, global_shape: Sequence[int] | None = None) -> tuple[int, ...]:
|
|
562
|
+
process_group = _get_process_group()
|
|
563
|
+
if rank != process_group.rank:
|
|
564
|
+
raise RuntimeError("Can't calculate local shape of peer process with Box distribution")
|
|
565
|
+
nranks = process_group.nranks
|
|
566
|
+
self._local_shape_checks(rank, nranks, global_shape)
|
|
567
|
+
return tuple(self._upper[i] - self._lower[i] for i in range(self.ndim))
|
|
568
|
+
|
|
569
|
+
def __str__(self):
|
|
570
|
+
return f"Box(lower={self._lower}, upper={self._upper})" + self._binding_str()
|
|
571
|
+
|
|
572
|
+
def __eq__(self, other):
|
|
573
|
+
if not isinstance(other, Box):
|
|
574
|
+
return False
|
|
575
|
+
return self._lower == other._lower and self._upper == other._upper
|
|
576
|
+
|
|
577
|
+
def __hash__(self):
|
|
578
|
+
return hash((self._lower, self._upper))
|
|
579
|
+
|
|
580
|
+
def __iter__(self):
|
|
581
|
+
# To allow unpacking
|
|
582
|
+
yield self._lower
|
|
583
|
+
yield self._upper
|
|
584
|
+
|
|
585
|
+
def __getitem__(self, index):
|
|
586
|
+
if index not in (0, 1):
|
|
587
|
+
return IndexError(f"Index must be 0 or 1, got {index}")
|
|
588
|
+
return self._lower if index == 0 else self._upper
|
|
589
|
+
|
|
590
|
+
def _bind(self, global_shape, *, shape=None) -> Distribution:
|
|
591
|
+
if self._bound:
|
|
592
|
+
raise BindDistributionError(f"{self} is already bound")
|
|
593
|
+
my_shape = tuple(self._upper[i] - self._lower[i] for i in range(self.ndim))
|
|
594
|
+
if shape is not None and tuple(shape) != my_shape:
|
|
595
|
+
raise BindDistributionError(
|
|
596
|
+
f"The given shapes don't fit this Box distribution: {global_shape} "
|
|
597
|
+
f"and {shape}, lower={self._lower} upper={self._upper}"
|
|
598
|
+
)
|
|
599
|
+
self._data_global_shape = tuple(global_shape)
|
|
600
|
+
self._data_shape = my_shape
|
|
601
|
+
self._bound = True
|
|
602
|
+
return self
|
|
603
|
+
|
|
604
|
+
def to(self, cls, /, *, ndim=None, copy=False):
|
|
605
|
+
super()._to_checks(cls, ndim)
|
|
606
|
+
if cls is Box:
|
|
607
|
+
if ndim is not None and ndim > self.ndim:
|
|
608
|
+
# Promote to higher dimensionality, assuming that the size of appended
|
|
609
|
+
# dimensions is 1.
|
|
610
|
+
new_lower = self._lower + (0,) * (ndim - self.ndim)
|
|
611
|
+
new_upper = self._upper + (1,) * (ndim - self.ndim)
|
|
612
|
+
d = Box(new_lower, new_upper)
|
|
613
|
+
if self._bound:
|
|
614
|
+
d._bind(self._data_global_shape + (1,) * (ndim - self.ndim))
|
|
615
|
+
return d
|
|
616
|
+
return self.copy() if copy else self
|
|
617
|
+
raise NotImplementedError
|
|
618
|
+
|
|
619
|
+
def _global_shape_reduction_prologue(self, rank: int, local_shape: Sequence[int]) -> None:
|
|
620
|
+
if self.ndim != len(local_shape):
|
|
621
|
+
raise ValueError(
|
|
622
|
+
f"The dimensionality of {self} doesn't match the dimensionality of the local operand ({len(local_shape)})"
|
|
623
|
+
)
|
|
624
|
+
if self.box_shape != tuple(local_shape):
|
|
625
|
+
raise ValueError(f"The operand shape {local_shape} does not match the box shape {self.box_shape}")
|
|
626
|
+
|
|
627
|
+
def _global_shape_reduction(self, other: Distribution) -> None:
|
|
628
|
+
if type(other) is not Box:
|
|
629
|
+
raise ValueError(f"Inconsistent distribution type across processes: {type(self)} != {type(other)}")
|
|
630
|
+
# This calculates the smallest box that encompasses the box of `self` and the
|
|
631
|
+
# box of `other`.
|
|
632
|
+
self._lower = tuple(np.minimum(np.array(self._lower), np.array(other._lower)).tolist()) # type: ignore
|
|
633
|
+
self._upper = tuple(np.maximum(np.array(self._upper), np.array(other._upper)).tolist()) # type: ignore
|
|
634
|
+
|
|
635
|
+
def _global_shape_reduction_epilogue(self) -> tuple[int, ...]:
|
|
636
|
+
global_shape = self.box_shape
|
|
637
|
+
return global_shape
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _numroc(n: int, nb: int, iproc: int, isrcproc: int, nprocs: int) -> int:
|
|
641
|
+
"""Computes the number of rows or columns of a matrix distributed with BlockCyclic
|
|
642
|
+
distribution, owned by the process indicated by iproc argument."""
|
|
643
|
+
|
|
644
|
+
# Figure PROC's distance from source process
|
|
645
|
+
mydist = (nprocs + iproc - isrcproc) % nprocs
|
|
646
|
+
|
|
647
|
+
# Figure the total number of whole NB blocks N is split up into
|
|
648
|
+
nblocks = n // nb
|
|
649
|
+
|
|
650
|
+
# Figure the minimum number of rows/cols a process can have
|
|
651
|
+
numroc = (nblocks // nprocs) * nb
|
|
652
|
+
|
|
653
|
+
# See if there are any extra blocks
|
|
654
|
+
extrablks = nblocks % nprocs
|
|
655
|
+
|
|
656
|
+
if mydist < extrablks:
|
|
657
|
+
# If I have extra blocks
|
|
658
|
+
return numroc + nb
|
|
659
|
+
elif mydist == extrablks:
|
|
660
|
+
# If I have the last block, it may be a partial block
|
|
661
|
+
return numroc + (n % nb)
|
|
662
|
+
else:
|
|
663
|
+
# first M-1 blocks are full blocks
|
|
664
|
+
return numroc
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
class BlockCyclic(Distribution):
|
|
668
|
+
"""Block-cyclic distribution"""
|
|
669
|
+
|
|
670
|
+
def __init__(
|
|
671
|
+
self,
|
|
672
|
+
process_grid: ProcessGrid,
|
|
673
|
+
block_sizes: Sequence[int],
|
|
674
|
+
*,
|
|
675
|
+
first_process: Sequence[int] | None = None,
|
|
676
|
+
):
|
|
677
|
+
super().__init__()
|
|
678
|
+
if block_sizes is None or not all(isinstance(x, int) for x in block_sizes):
|
|
679
|
+
raise ValueError(f"Must provide a sequence of integer block sizes, got {block_sizes}")
|
|
680
|
+
if len(block_sizes) != len(process_grid.shape):
|
|
681
|
+
raise ValueError(
|
|
682
|
+
f"Number of block sizes ({len(block_sizes)}) doesn't match dimensionality ({len(process_grid.shape)})"
|
|
683
|
+
)
|
|
684
|
+
self._process_grid = process_grid
|
|
685
|
+
self._block_sizes = tuple(block_sizes)
|
|
686
|
+
if first_process is None:
|
|
687
|
+
self._first_process = (0,) * self.ndim
|
|
688
|
+
else:
|
|
689
|
+
if not all(isinstance(x, int) for x in first_process):
|
|
690
|
+
raise ValueError(f"first_process must be a sequence of integer coordinates, got {first_process}")
|
|
691
|
+
for i, x in enumerate(first_process):
|
|
692
|
+
if x < 0 or x >= process_grid.shape[i]:
|
|
693
|
+
raise ValueError(
|
|
694
|
+
f"first_process {first_process} is not a valid index into the process grid of "
|
|
695
|
+
f"shape {process_grid.shape}"
|
|
696
|
+
)
|
|
697
|
+
self._first_process = tuple(first_process)
|
|
698
|
+
|
|
699
|
+
@property
|
|
700
|
+
def process_grid(self) -> ProcessGrid:
|
|
701
|
+
"""The process grid of this BlockCyclic distribution"""
|
|
702
|
+
return self._process_grid
|
|
703
|
+
|
|
704
|
+
@property
|
|
705
|
+
def ndim(self) -> int:
|
|
706
|
+
return len(self._process_grid.shape)
|
|
707
|
+
|
|
708
|
+
@property
|
|
709
|
+
def block_sizes(self) -> tuple[int, ...]:
|
|
710
|
+
"""The block sizes of this BlockCyclic distribution"""
|
|
711
|
+
return self._block_sizes
|
|
712
|
+
|
|
713
|
+
@property
|
|
714
|
+
def first_process(self) -> tuple[int, ...]:
|
|
715
|
+
"""Index in the process grid of the process who owns the first block of the
|
|
716
|
+
distributed tensor."""
|
|
717
|
+
return self._first_process
|
|
718
|
+
|
|
719
|
+
def __str__(self):
|
|
720
|
+
return (
|
|
721
|
+
f"{self.__class__.__name__}(process_grid={self._process_grid}, block_sizes={self._block_sizes})"
|
|
722
|
+
+ self._binding_str()
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
def __eq__(self, other):
|
|
726
|
+
if not isinstance(other, BlockCyclic):
|
|
727
|
+
return False
|
|
728
|
+
return (
|
|
729
|
+
self._process_grid == other._process_grid
|
|
730
|
+
and self._block_sizes == other._block_sizes
|
|
731
|
+
and self._first_process == other._first_process
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
def _is_1d_distribution(self) -> bool:
|
|
735
|
+
"""True if process grid partitions on a single dimension."""
|
|
736
|
+
return self._process_grid._is_1d_distribution()
|
|
737
|
+
|
|
738
|
+
def _is_row_wise(self) -> bool:
|
|
739
|
+
"""True if 2D process grid partitioned only on rows."""
|
|
740
|
+
return self._process_grid._is_row_wise()
|
|
741
|
+
|
|
742
|
+
def _is_col_wise(self) -> bool:
|
|
743
|
+
"""True if 2D process grid partitioned only on columns."""
|
|
744
|
+
return self._process_grid._is_col_wise()
|
|
745
|
+
|
|
746
|
+
def shape(self, rank: int, global_shape: Sequence[int] | None = None) -> tuple[int, ...]:
|
|
747
|
+
self._local_shape_checks(rank, self._process_grid._nranks, global_shape)
|
|
748
|
+
if global_shape is None:
|
|
749
|
+
global_shape = self._data_global_shape
|
|
750
|
+
return self._calc_local_shape(rank, self._block_sizes, global_shape)
|
|
751
|
+
|
|
752
|
+
def _calc_local_shape(self, rank, block_sizes, global_shape):
|
|
753
|
+
nprow, npcol = self._process_grid._shape
|
|
754
|
+
layout = self._process_grid._layout
|
|
755
|
+
if layout is not None:
|
|
756
|
+
myprow = rank % nprow if layout == ProcessGrid.Layout.COL_MAJOR else rank // npcol
|
|
757
|
+
mypcol = rank // nprow if layout == ProcessGrid.Layout.COL_MAJOR else rank % npcol
|
|
758
|
+
index = (myprow, mypcol)
|
|
759
|
+
else:
|
|
760
|
+
raise NotImplementedError
|
|
761
|
+
nrows = _numroc(global_shape[0], block_sizes[0], index[0], self._first_process[0], nprow)
|
|
762
|
+
ncols = _numroc(global_shape[1], block_sizes[1], index[1], self._first_process[1], npcol)
|
|
763
|
+
return (nrows, ncols)
|
|
764
|
+
|
|
765
|
+
def _bind(self, global_shape, *, shape=None) -> Distribution:
|
|
766
|
+
if self._bound:
|
|
767
|
+
raise BindDistributionError(f"{self} is already bound")
|
|
768
|
+
|
|
769
|
+
if self.ndim != 2:
|
|
770
|
+
raise NotImplementedError
|
|
771
|
+
|
|
772
|
+
if len(global_shape) != self.ndim:
|
|
773
|
+
raise ValueError(
|
|
774
|
+
f"Dimensionality of shapes ({len(global_shape)}) doesn't match dimensionality "
|
|
775
|
+
f"of this distribution ({self.ndim})"
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
rank = _get_process_group().rank
|
|
779
|
+
nrows, ncols = self._calc_local_shape(rank, self._block_sizes, global_shape)
|
|
780
|
+
if shape is not None and tuple(shape) != (nrows, ncols):
|
|
781
|
+
raise BindDistributionError(
|
|
782
|
+
f"The local shape {shape} on process {rank} is not the expected one based "
|
|
783
|
+
f"on the global shape {global_shape}, process grid {self._process_grid} and "
|
|
784
|
+
f"block sizes {self._block_sizes}: expected shape is {(nrows, ncols)}"
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
self._data_global_shape = global_shape
|
|
788
|
+
self._data_shape = (nrows, ncols)
|
|
789
|
+
self._bound = True
|
|
790
|
+
return self
|
|
791
|
+
|
|
792
|
+
def to(self, cls, /, *, ndim=None, copy=False):
|
|
793
|
+
super()._to_checks(cls, ndim)
|
|
794
|
+
nranks = _get_process_group().nranks
|
|
795
|
+
if ndim is None:
|
|
796
|
+
ndim = self.ndim
|
|
797
|
+
if issubclass(cls, BlockCyclic):
|
|
798
|
+
if type(self) is BlockCyclic and cls is BlockNonCyclic:
|
|
799
|
+
# TODO: BlockCyclic to BlockNonCyclic
|
|
800
|
+
raise NotImplementedError
|
|
801
|
+
if ndim > self.ndim:
|
|
802
|
+
if self._process_grid.process_array is not None:
|
|
803
|
+
raise NotImplementedError
|
|
804
|
+
d = self.copy()
|
|
805
|
+
d._process_grid = ProcessGrid(
|
|
806
|
+
shape=self._process_grid.shape + (1,) * (ndim - self.ndim),
|
|
807
|
+
layout=self._process_grid.layout,
|
|
808
|
+
)
|
|
809
|
+
d._first_process += (0,) * (ndim - self.ndim)
|
|
810
|
+
d._block_sizes += (1,) * (ndim - self.ndim)
|
|
811
|
+
if self._bound:
|
|
812
|
+
d._bound = False
|
|
813
|
+
d._bind(self._data_global_shape + (1,) * (ndim - self.ndim))
|
|
814
|
+
return d
|
|
815
|
+
return self.copy() if copy else self
|
|
816
|
+
elif cls is Slab:
|
|
817
|
+
if not self._bound:
|
|
818
|
+
# Without binding, it's a stretch to assume that this is compatible with
|
|
819
|
+
# Slab (the "cyclic" nature means that it's much more likely that it isn't).
|
|
820
|
+
raise ConvertDistributionError(
|
|
821
|
+
"Unbound BlockCyclic distribution can't be converted to Slab. "
|
|
822
|
+
"Consider using BlockNonCyclic if there is no cyclic distribution of blocks."
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
if not self._is_1d_distribution():
|
|
826
|
+
raise ConvertDistributionError(
|
|
827
|
+
"Can't convert this block distribution to Slab: partitioning must be on a single dimension"
|
|
828
|
+
)
|
|
829
|
+
|
|
830
|
+
# Data must be divisible on partition_dim and the block size must correspond
|
|
831
|
+
# to the slab size.
|
|
832
|
+
partition_dim = self._process_grid.shape.index(nranks)
|
|
833
|
+
|
|
834
|
+
if self._data_global_shape[partition_dim] % nranks != 0:
|
|
835
|
+
raise ConvertDistributionError(
|
|
836
|
+
"Can't convert this distribution to Slab: data doesn't divide evenly on partition dimension."
|
|
837
|
+
f" Global shape is {self._data_global_shape}, partition dimension is "
|
|
838
|
+
f"{partition_dim} and number of processes is {nranks}."
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
if self._block_sizes[partition_dim] != self._data_global_shape[partition_dim] // nranks:
|
|
842
|
+
raise ConvertDistributionError(
|
|
843
|
+
"This distribution can't be converted to Slab, because the block size in the "
|
|
844
|
+
f"partition dimension {partition_dim} is not a factor of the global extent in "
|
|
845
|
+
f"that dimension {self._data_global_shape[partition_dim]}"
|
|
846
|
+
)
|
|
847
|
+
|
|
848
|
+
data_global_shape = self._data_global_shape + (1,) * (ndim - self.ndim)
|
|
849
|
+
data_shape = self._data_shape + (1,) * (ndim - self.ndim)
|
|
850
|
+
d = Slab(partition_dim, ndim=ndim)
|
|
851
|
+
return d._bind(data_global_shape, shape=data_shape)
|
|
852
|
+
elif cls is Box:
|
|
853
|
+
# TODO: conversion to Box for non-1D distribution, and non-uniform partition
|
|
854
|
+
# sizes.
|
|
855
|
+
# Note that Box doesn't support cyclic.
|
|
856
|
+
if self._bound:
|
|
857
|
+
try:
|
|
858
|
+
slab = self.to(Slab, ndim=ndim)
|
|
859
|
+
except ConvertDistributionError:
|
|
860
|
+
raise NotImplementedError from None
|
|
861
|
+
return slab.to(Box)
|
|
862
|
+
raise NotImplementedError
|
|
863
|
+
|
|
864
|
+
def _global_shape_reduction_prologue(self, rank: int, local_shape: Sequence[int]) -> None:
|
|
865
|
+
if self.ndim != 2:
|
|
866
|
+
raise NotImplementedError("Global shape reduction for BlockCyclic with number of dimensions != 2 not implemented")
|
|
867
|
+
|
|
868
|
+
# To calculate the global shape when using 2D block distribution, we
|
|
869
|
+
# ignore the rows of processes that aren't in column 0 of the process
|
|
870
|
+
# grid, and the columns of processes that aren't in row 0 of the process
|
|
871
|
+
# grid (by setting the rows/columns to 0). We could do the same for 1D,
|
|
872
|
+
# but by preserving the shape info for 1D we can do some extra checks
|
|
873
|
+
# in _global_shape_reduction.
|
|
874
|
+
self._global_shape = list(local_shape)
|
|
875
|
+
if not self._process_grid._is_1d_distribution():
|
|
876
|
+
nprow, npcol = self._process_grid.shape
|
|
877
|
+
myprow = rank % nprow if self._process_grid.layout == ProcessGrid.Layout.COL_MAJOR else rank // npcol
|
|
878
|
+
mypcol = rank // nprow if self._process_grid.layout == ProcessGrid.Layout.COL_MAJOR else rank % npcol
|
|
879
|
+
if myprow != 0:
|
|
880
|
+
# Ignore my ncols in global shape calculation (because of overlap with
|
|
881
|
+
# ncols of process row 0).
|
|
882
|
+
self._global_shape[1] = 0
|
|
883
|
+
if mypcol != 0:
|
|
884
|
+
# Ignore my nrows in global shape calculation (because of overlap with
|
|
885
|
+
# nrows of process col 0).
|
|
886
|
+
self._global_shape[0] = 0
|
|
887
|
+
|
|
888
|
+
def _global_shape_reduction(self, other: Distribution) -> None:
|
|
889
|
+
if self is other:
|
|
890
|
+
return
|
|
891
|
+
|
|
892
|
+
if self != other:
|
|
893
|
+
raise ValueError(f"The distribution is inconsistent across processes: {self} != {other}")
|
|
894
|
+
|
|
895
|
+
partitioned_dims = (0,) if self._process_grid._is_row_wise() else (1,) if self._process_grid._is_col_wise() else (0, 1)
|
|
896
|
+
|
|
897
|
+
if len(partitioned_dims) == 1 and any(
|
|
898
|
+
self._global_shape[i] != other._global_shape[i] # type: ignore[attr-defined]
|
|
899
|
+
for i in (0, 1)
|
|
900
|
+
if i != partitioned_dims[0]
|
|
901
|
+
):
|
|
902
|
+
raise ValueError(
|
|
903
|
+
"The problem size is inconsistent across processes:"
|
|
904
|
+
+ str(self._global_shape)
|
|
905
|
+
+ " vs "
|
|
906
|
+
+ str(other._global_shape) # type: ignore[attr-defined]
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
# Reduce the partitioned dimensions to get the global size.
|
|
910
|
+
for dim in partitioned_dims:
|
|
911
|
+
self._global_shape[dim] += other._global_shape[dim] # type: ignore[attr-defined]
|
|
912
|
+
|
|
913
|
+
def _global_shape_reduction_epilogue(self) -> tuple[int, ...]:
|
|
914
|
+
return tuple(self._global_shape)
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
class BlockNonCyclic(BlockCyclic):
|
|
918
|
+
"""Block distribution without cycles"""
|
|
919
|
+
|
|
920
|
+
def __init__(
|
|
921
|
+
self,
|
|
922
|
+
process_grid: ProcessGrid,
|
|
923
|
+
*,
|
|
924
|
+
first_process: Sequence[int] | None = None,
|
|
925
|
+
):
|
|
926
|
+
super().__init__(process_grid, (-1,) * len(process_grid.shape), first_process=first_process)
|
|
927
|
+
|
|
928
|
+
def __eq__(self, other):
|
|
929
|
+
if isinstance(other, BlockNonCyclic) and (not other._bound or not self._bound):
|
|
930
|
+
# Don't compare block sizes since one of them is not bound.
|
|
931
|
+
return self._process_grid == other._process_grid and self._first_process == other._first_process
|
|
932
|
+
return super().__eq__(other)
|
|
933
|
+
|
|
934
|
+
def shape(self, rank: int, global_shape: Sequence[int] | None = None) -> tuple[int, ...]:
|
|
935
|
+
self._local_shape_checks(rank, self._process_grid._nranks, global_shape)
|
|
936
|
+
if global_shape is None:
|
|
937
|
+
global_shape = self._data_global_shape
|
|
938
|
+
if self._bound:
|
|
939
|
+
block_sizes = self._block_sizes
|
|
940
|
+
else:
|
|
941
|
+
block_sizes = self._infer_block_sizes(global_shape)
|
|
942
|
+
return self._calc_local_shape(rank, block_sizes, global_shape)
|
|
943
|
+
|
|
944
|
+
def _infer_block_sizes(self, global_shape):
|
|
945
|
+
if all(x % self._process_grid._shape[i] == 0 for i, x in enumerate(global_shape)):
|
|
946
|
+
block_sizes = tuple(x // self._process_grid._shape[i] for i, x in enumerate(global_shape))
|
|
947
|
+
else:
|
|
948
|
+
# The logic to bind this global shape to this distribution isn't implemented yet
|
|
949
|
+
# (which doesn't necessarily mean that it isn't possible to fit the data to this
|
|
950
|
+
# distribution).
|
|
951
|
+
raise NotImplementedError(
|
|
952
|
+
"BlockNonCyclic is currently only implemented for uniform partition sizes. "
|
|
953
|
+
"Use BlockCyclic with explicit block sizes instead."
|
|
954
|
+
)
|
|
955
|
+
return block_sizes
|
|
956
|
+
|
|
957
|
+
def _bind(self, global_shape, *, shape=None) -> Distribution:
|
|
958
|
+
if self._bound:
|
|
959
|
+
raise BindDistributionError(f"{self} is already bound")
|
|
960
|
+
|
|
961
|
+
if len(global_shape) != self.ndim:
|
|
962
|
+
raise ValueError(
|
|
963
|
+
f"Dimensionality of shapes ({len(global_shape)}) doesn't match dimensionality "
|
|
964
|
+
f"of this distribution ({self.ndim})"
|
|
965
|
+
)
|
|
966
|
+
|
|
967
|
+
# Now we assign block sizes to this distribution based on the shape of the data.
|
|
968
|
+
# NOTE: For dimensions that aren't partitioned, there are multiple blocks sizes that
|
|
969
|
+
# are valid and still fit the Block[Non]Cyclic model. This is because, in addition
|
|
970
|
+
# to block size being the full length L of the dimension, any L//N is also a valid
|
|
971
|
+
# block size (so a single block in that dimension is equivalent to N contiguous
|
|
972
|
+
# blocks in that dimension). The importance of this is that cuBLASMp actually
|
|
973
|
+
# requires configurations with these block sizes, since block sizes have to match
|
|
974
|
+
# across matrices A, B and C but they might be distributed differently.
|
|
975
|
+
# In other words, for distributed matmul with BlockNonCyclic, block sizes have to be
|
|
976
|
+
# inferred jointly with matrices A, B and C, but that is outside the scope of this
|
|
977
|
+
# method.
|
|
978
|
+
|
|
979
|
+
block_sizes = self._infer_block_sizes(global_shape)
|
|
980
|
+
if shape is not None and block_sizes != tuple(shape):
|
|
981
|
+
raise BindDistributionError("Data doesn't fit BlockNonCyclic distribution")
|
|
982
|
+
|
|
983
|
+
self._block_sizes = block_sizes
|
|
984
|
+
return super()._bind(global_shape, shape=shape)
|
|
985
|
+
|
|
986
|
+
def to(self, cls, /, *, ndim=None, copy=False):
|
|
987
|
+
super()._to_checks(cls, ndim)
|
|
988
|
+
nranks = _get_process_group().nranks
|
|
989
|
+
if ndim is None:
|
|
990
|
+
ndim = self.ndim
|
|
991
|
+
if issubclass(cls, BlockCyclic):
|
|
992
|
+
return super().to(cls, ndim=ndim, copy=copy)
|
|
993
|
+
elif cls is Slab:
|
|
994
|
+
if not self._is_1d_distribution():
|
|
995
|
+
raise ConvertDistributionError(
|
|
996
|
+
"Can't convert this block distribution to Slab: partitioning must be on a single dimension"
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
partition_dim = self._process_grid.shape.index(nranks)
|
|
1000
|
+
d = Slab(partition_dim, ndim=ndim)
|
|
1001
|
+
# For bound=False, we can allow the conversion and let Slab._bind() catch
|
|
1002
|
+
# any potential errors later.
|
|
1003
|
+
if self._bound:
|
|
1004
|
+
data_global_shape = self._data_global_shape + (1,) * (ndim - self.ndim)
|
|
1005
|
+
data_shape = self._data_shape + (1,) * (ndim - self.ndim)
|
|
1006
|
+
if data_global_shape[partition_dim] % nranks != 0:
|
|
1007
|
+
raise ConvertDistributionError(
|
|
1008
|
+
"Can't convert this distribution to Slab: data doesn't divide evenly on partition dimension"
|
|
1009
|
+
)
|
|
1010
|
+
d._bind(data_global_shape, shape=data_shape)
|
|
1011
|
+
return d
|
|
1012
|
+
elif cls is Box:
|
|
1013
|
+
return super().to(Box, ndim=ndim, copy=copy)
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
def _get_process_group():
|
|
1017
|
+
distributed_ctx = dist.get_context()
|
|
1018
|
+
if distributed_ctx is None:
|
|
1019
|
+
raise RuntimeError(
|
|
1020
|
+
"nvmath.distributed has not been initialized. Refer to "
|
|
1021
|
+
"https://docs.nvidia.com/cuda/nvmath-python/latest/distributed-apis/runtime.html"
|
|
1022
|
+
" for more information."
|
|
1023
|
+
)
|
|
1024
|
+
return distributed_ctx.process_group
|