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,3573 @@
|
|
|
1
|
+
# Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
__all__ = ["MatmulComputeType", "Matmul", "matmul"]
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import typing
|
|
10
|
+
from collections import defaultdict, namedtuple
|
|
11
|
+
from collections.abc import Sequence
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Literal, cast
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
from cuda.core import Device
|
|
17
|
+
|
|
18
|
+
import nvmath.distributed
|
|
19
|
+
from nvmath import memory
|
|
20
|
+
from nvmath._internal.layout import is_contiguous_and_dense
|
|
21
|
+
from nvmath._utils import CudaDataType
|
|
22
|
+
from nvmath.bindings import (
|
|
23
|
+
cublas,
|
|
24
|
+
cublasMp, # type: ignore
|
|
25
|
+
)
|
|
26
|
+
from nvmath.distributed._internal import tensor_wrapper
|
|
27
|
+
from nvmath.distributed._internal.tensor_ifc import DistributedTensor
|
|
28
|
+
from nvmath.distributed.distribution import BlockCyclic, BlockNonCyclic, ConvertDistributionError, Distribution, ProcessGrid
|
|
29
|
+
from nvmath.distributed.linalg._internal import matmul_desc_ifc
|
|
30
|
+
from nvmath.distributed.linalg._internal.epilog_protocol import (
|
|
31
|
+
EPILOG_INPUT_HANDLERS_MAP,
|
|
32
|
+
EPILOG_MINIMUM_VERSIONS_MAP,
|
|
33
|
+
EPILOG_OUTPUT_HANDLERS_MAP,
|
|
34
|
+
EpilogOutputHandler,
|
|
35
|
+
)
|
|
36
|
+
from nvmath.distributed.linalg.advanced import MatmulEpilog, _configuration
|
|
37
|
+
from nvmath.internal import formatters, typemaps, utils
|
|
38
|
+
from nvmath.linalg._internal.typemaps import (
|
|
39
|
+
COMPUTE_TYPE_TO_DEFAULT_SCALE_TYPE,
|
|
40
|
+
NAMES_TO_DEFAULT_COMPUTE_TYPE,
|
|
41
|
+
NAMES_TO_DEFAULT_SCALE_TYPE,
|
|
42
|
+
SCALE_TYPE_TO_DEFAULT_COMPUTE_TYPE,
|
|
43
|
+
SUPPORTED_TYPES,
|
|
44
|
+
)
|
|
45
|
+
from nvmath.linalg._internal.utils import (
|
|
46
|
+
calculate_strides,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
from ._configuration import MatmulOptions, matrix_qualifiers_dtype
|
|
50
|
+
|
|
51
|
+
MatmulComputeType = cublas.ComputeType
|
|
52
|
+
|
|
53
|
+
EpilogInputTraits = namedtuple("EpilogInputTraits", ["dtype", "extents", "strides"])
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class MatrixLayout:
|
|
58
|
+
"""An internal data class for capturing the local tensor layout."""
|
|
59
|
+
|
|
60
|
+
shape: Sequence[int]
|
|
61
|
+
strides: Sequence[int]
|
|
62
|
+
is_transpose: bool = False
|
|
63
|
+
is_conjugate: bool = False # Used to support is_conjugate via conjugate_transpose.
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class MMTraits:
|
|
68
|
+
"""An internal data class for capturing the matrix multiplication traits. The
|
|
69
|
+
result traits are captured separately, because we need to wait for the
|
|
70
|
+
epilog to be provided.
|
|
71
|
+
|
|
72
|
+
Note: For FP4, these are logical sizes (packed dimension is unpacked).
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
M: int # global matrix size M
|
|
76
|
+
N: int # global matrix size N
|
|
77
|
+
K: int # global matrix size K
|
|
78
|
+
a_layout: MatrixLayout # local
|
|
79
|
+
b_layout: MatrixLayout # local
|
|
80
|
+
c_layout: MatrixLayout | None # local
|
|
81
|
+
d_layout: MatrixLayout | None # local
|
|
82
|
+
# NOTE: cuBLASMp doesn't support batched matmul so batch size is currently always 0.
|
|
83
|
+
batch_count: int = 0
|
|
84
|
+
batch_shape: Sequence[int] = field(default_factory=list)
|
|
85
|
+
batch_axis_order: Sequence[int] = field(default_factory=list)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class _ProblemSpec:
|
|
90
|
+
"""This is used in a custom reduction to check that the Matmul problem specification
|
|
91
|
+
is consistent across processes, and to infer global information (e.g. global shape
|
|
92
|
+
of distributed matrices)."""
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class Options:
|
|
96
|
+
"""
|
|
97
|
+
This is used for _ProblemSpec instead of MatmulOptions because it's going
|
|
98
|
+
to be serialized as part of the custom reduction of the _ProblemSpec, and
|
|
99
|
+
we want to control which fields are included (for example we don't need
|
|
100
|
+
the logger).
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def __init__(self, options: MatmulOptions):
|
|
104
|
+
self.inplace = options.inplace
|
|
105
|
+
self.compute_type = options.compute_type
|
|
106
|
+
self.scale_type = options.scale_type
|
|
107
|
+
self.result_type = options.result_type
|
|
108
|
+
self.result_amax = options.result_amax
|
|
109
|
+
self.block_scaling = options.block_scaling
|
|
110
|
+
self.blocking = options.blocking
|
|
111
|
+
|
|
112
|
+
inplace: bool = False
|
|
113
|
+
compute_type: int | None = None
|
|
114
|
+
scale_type: int | None = None
|
|
115
|
+
result_type: int | None = None
|
|
116
|
+
result_amax: bool = False
|
|
117
|
+
block_scaling: bool = False
|
|
118
|
+
blocking: Literal[True, "auto"] = "auto"
|
|
119
|
+
|
|
120
|
+
shapes: list[list[int]] # shape of each operand
|
|
121
|
+
is_F: list[bool] # Is F memory layout (of each operand)
|
|
122
|
+
operand_dtypes: list[str] # dtype of each operand
|
|
123
|
+
packages: list[Literal["numpy", "cupy", "torch"]] # package of each operand
|
|
124
|
+
memory_spaces: list[Literal["cuda", "cpu"]] # memory space of each operand
|
|
125
|
+
distributions: list[Distribution] # distribution of A, B and C/D
|
|
126
|
+
options: Options # Matmul options
|
|
127
|
+
device_ids: list[int | Literal["cpu"]] # device_id of each operand
|
|
128
|
+
compute_capability: tuple[int, ...] # compute capability of the execution space device
|
|
129
|
+
alpha: float
|
|
130
|
+
beta: float
|
|
131
|
+
qualifiers: np.ndarray
|
|
132
|
+
nranks: int
|
|
133
|
+
rank: int # only valid if is_leaf=True
|
|
134
|
+
lib_version: int
|
|
135
|
+
b_min_size: int # minimum number of local elements in B
|
|
136
|
+
b_max_size: int # maximum number of local elements in B
|
|
137
|
+
|
|
138
|
+
# is_leaf=True means that this is the _ProblemSpec of a process before reducing
|
|
139
|
+
# with that of another process.
|
|
140
|
+
is_leaf: bool = True
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _problem_spec_reducer(p1: _ProblemSpec, p2: _ProblemSpec):
|
|
144
|
+
try:
|
|
145
|
+
if isinstance(p1, Exception):
|
|
146
|
+
return p1 # propagate exception
|
|
147
|
+
|
|
148
|
+
if isinstance(p2, Exception):
|
|
149
|
+
return p2 # propagate exception
|
|
150
|
+
|
|
151
|
+
if not (p1.lib_version == p2.lib_version >= 800):
|
|
152
|
+
return ValueError("cublasMp >= 0.8.0 required")
|
|
153
|
+
|
|
154
|
+
num_operands = len(p1.operand_dtypes)
|
|
155
|
+
if num_operands != len(p2.operand_dtypes):
|
|
156
|
+
return ValueError("The number of operands doesn't match across processes")
|
|
157
|
+
|
|
158
|
+
if num_operands not in (2, 3):
|
|
159
|
+
return ValueError("The number of operands must be 2 or 3")
|
|
160
|
+
|
|
161
|
+
def check_dtype(dtype, operand_name: str):
|
|
162
|
+
if dtype not in SUPPORTED_TYPES:
|
|
163
|
+
raise ValueError(f"The dtype of operand {operand_name} ({dtype}) is not supported.")
|
|
164
|
+
|
|
165
|
+
operand_name = "ABC"
|
|
166
|
+
|
|
167
|
+
for i in range(num_operands):
|
|
168
|
+
if p1.operand_dtypes[i] != p2.operand_dtypes[i]:
|
|
169
|
+
return ValueError(
|
|
170
|
+
f"Operand {operand_name[i]} dtype does not match across processes: "
|
|
171
|
+
f"{p1.operand_dtypes[i]} != {p2.operand_dtypes[i]}"
|
|
172
|
+
)
|
|
173
|
+
check_dtype(p1.operand_dtypes[i], operand_name[i])
|
|
174
|
+
|
|
175
|
+
def _check_extents(shape: list[int], name: str):
|
|
176
|
+
if len(shape) > 2:
|
|
177
|
+
raise ValueError("Batched matmul is not supported")
|
|
178
|
+
if name == "C" and len(shape) != 2:
|
|
179
|
+
raise ValueError(
|
|
180
|
+
"In order to avoid broadcasting behavior ambiguity, `c` must be 2-D. "
|
|
181
|
+
"Use a singleton dimension to convert your input array to 2-D."
|
|
182
|
+
)
|
|
183
|
+
# TODO: allow broadcasting A and B if 1D.
|
|
184
|
+
if len(shape) != 2:
|
|
185
|
+
raise ValueError("Operands must be two-dimensional")
|
|
186
|
+
if any(e <= 0 for e in shape):
|
|
187
|
+
message = (
|
|
188
|
+
f"The specified extents {shape} for operand {name} are not valid. The extents must be strictly positive. "
|
|
189
|
+
)
|
|
190
|
+
raise ValueError(message)
|
|
191
|
+
|
|
192
|
+
for p in (p1, p2):
|
|
193
|
+
if p.is_leaf:
|
|
194
|
+
for i in range(num_operands):
|
|
195
|
+
_check_extents(p.shapes[i], operand_name[i])
|
|
196
|
+
|
|
197
|
+
if len(set(p.packages)) != 1:
|
|
198
|
+
return ValueError(
|
|
199
|
+
f"The operands on process {p.rank} don't belong to the same package: got operand packages {p.packages}"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if len(set(p.memory_spaces)) != 1:
|
|
203
|
+
return ValueError(
|
|
204
|
+
f"The operands on process {p.rank} are not in the same memory space: got "
|
|
205
|
+
f"operand memory spaces {p.memory_spaces}"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
if len(set(p.device_ids)) != 1:
|
|
209
|
+
return ValueError(
|
|
210
|
+
f"The operands on process {p.rank} are not on the same device: got operand device IDs {p.device_ids}"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
input_type_width = typemaps.NAME_TO_DATA_WIDTH[p.operand_dtypes[0]]
|
|
214
|
+
if input_type_width <= 8 and p.options.block_scaling and p.compute_capability < (10, 0):
|
|
215
|
+
microscaling_format = "MXFP8" if input_type_width == 8 else "NVFP4"
|
|
216
|
+
return RuntimeError(
|
|
217
|
+
f"{microscaling_format} requires a device with compute capability 10.0 or higher "
|
|
218
|
+
"(Blackwell or newer architecture). The compute capability of "
|
|
219
|
+
f"process {p.rank}'s device is {p.compute_capability}"
|
|
220
|
+
)
|
|
221
|
+
if input_type_width == 8 and p.compute_capability < (8, 9):
|
|
222
|
+
return RuntimeError(
|
|
223
|
+
"FP8 requires a device with compute capability 8.9 or higher "
|
|
224
|
+
"(Ada, Hopper, Blackwell or newer architecture). The compute "
|
|
225
|
+
f"capability of process {p.rank}'s device is {p.compute_capability}"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
p.qualifiers = p.qualifiers if p.qualifiers is not None else np.zeros((3,), dtype=matrix_qualifiers_dtype)
|
|
229
|
+
if p.qualifiers.dtype != matrix_qualifiers_dtype:
|
|
230
|
+
return ValueError(
|
|
231
|
+
"The qualifiers must be specified as a NumPy array of length 3 "
|
|
232
|
+
"corresponding to the operands A, B, and C of type "
|
|
233
|
+
"'matrix_qualifiers_dtype'."
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
if p1.options != p2.options:
|
|
237
|
+
return ValueError(f"options are inconsistent across processes: {p1.options} != {p2.options}")
|
|
238
|
+
|
|
239
|
+
a_is_fp4 = p1.operand_dtypes[0] == "float4_e2m1fn_x2"
|
|
240
|
+
b_is_fp4 = p1.operand_dtypes[1] == "float4_e2m1fn_x2"
|
|
241
|
+
if a_is_fp4 != b_is_fp4:
|
|
242
|
+
return ValueError(
|
|
243
|
+
f"Mixed FP4/non-FP4 A/B operands are not supported. "
|
|
244
|
+
f"Got A dtype={p1.operand_dtypes[0]}, B dtype={p1.operand_dtypes[1]}. "
|
|
245
|
+
f"Both A and B operands must be float4_e2m1fn_x2 or neither."
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
if a_is_fp4 and not p1.options.block_scaling:
|
|
249
|
+
return ValueError(
|
|
250
|
+
f"When using FP4 (float4_e2m1fn_x2) A, B operands, block_scaling=True is required. "
|
|
251
|
+
f"Got block_scaling={p1.options.block_scaling}."
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
for i in range(num_operands):
|
|
255
|
+
if len(p1.shapes[i]) != len(p2.shapes[i]):
|
|
256
|
+
return ValueError(f"The number of dimensions of the operand {operand_name[i]} is inconsistent across processes")
|
|
257
|
+
|
|
258
|
+
if p1.packages[0] != p2.packages[0]:
|
|
259
|
+
return ValueError("operands don't belong to the same package on all processes")
|
|
260
|
+
|
|
261
|
+
if p1.memory_spaces[0] != p2.memory_spaces[0]:
|
|
262
|
+
return ValueError('operands are not in the same memory space ("cpu", "cuda") on all processes')
|
|
263
|
+
|
|
264
|
+
if p1.alpha != p2.alpha:
|
|
265
|
+
return ValueError(f"alpha does not match across processes: {p1.alpha} != {p2.alpha}")
|
|
266
|
+
|
|
267
|
+
if p1.beta != p2.beta:
|
|
268
|
+
return ValueError(f"beta does not match across processes: {p1.beta} != {p2.beta}")
|
|
269
|
+
|
|
270
|
+
if not np.array_equal(p1.qualifiers, p2.qualifiers):
|
|
271
|
+
return ValueError("The qualifiers don't match across processes")
|
|
272
|
+
|
|
273
|
+
if len(p1.distributions) != 3 or len(p2.distributions) != 3:
|
|
274
|
+
return ValueError("Must provide distributions for A, B and C/D")
|
|
275
|
+
|
|
276
|
+
p1.b_min_size = min(p1.b_min_size, p2.b_min_size)
|
|
277
|
+
p1.b_max_size = max(p1.b_max_size, p2.b_max_size)
|
|
278
|
+
|
|
279
|
+
for p in (p1, p2):
|
|
280
|
+
if p.is_leaf:
|
|
281
|
+
for i in range(num_operands):
|
|
282
|
+
p.distributions[i]._global_shape_reduction_prologue(p.rank, p.shapes[i])
|
|
283
|
+
|
|
284
|
+
# Determine the memory layout shared by all processes.
|
|
285
|
+
for i in range(num_operands):
|
|
286
|
+
p1.is_F[i] &= p2.is_F[i]
|
|
287
|
+
if not p1.is_F[i]:
|
|
288
|
+
return ValueError(f"Operand {operand_name[i]} doesn't have column-major (Fortran) memory layout")
|
|
289
|
+
|
|
290
|
+
# Calculate global shape based on process grid.
|
|
291
|
+
for i in range(num_operands):
|
|
292
|
+
p1.distributions[i]._global_shape_reduction(p2.distributions[i])
|
|
293
|
+
|
|
294
|
+
except Exception as e:
|
|
295
|
+
return e
|
|
296
|
+
p1.is_leaf = False
|
|
297
|
+
return p1
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
SHARED_MM_DOCUMENTATION = utils.COMMON_SHARED_DOC_MAP.copy()
|
|
301
|
+
SHARED_MM_DOCUMENTATION.update(
|
|
302
|
+
{
|
|
303
|
+
"a": """\
|
|
304
|
+
A distributed tensor representing the first operand to the matrix multiplication (see `Semantics`_).
|
|
305
|
+
The currently supported types are :class:`numpy.ndarray`, :class:`cupy.ndarray`, and
|
|
306
|
+
:class:`torch.Tensor`.""".replace("\n", " "),
|
|
307
|
+
#
|
|
308
|
+
"b": """\
|
|
309
|
+
A distributed tensor representing the second operand to the matrix multiplication (see `Semantics`_).
|
|
310
|
+
The currently supported types are :class:`numpy.ndarray`, :class:`cupy.ndarray`, and
|
|
311
|
+
:class:`torch.Tensor`.""".replace("\n", " "),
|
|
312
|
+
#
|
|
313
|
+
"c": """\
|
|
314
|
+
(Optional) A distributed tensor representing the operand to add to the matrix multiplication
|
|
315
|
+
result (see `Semantics`_). The currently supported types are :class:`numpy.ndarray`,
|
|
316
|
+
:class:`cupy.ndarray`, and :class:`torch.Tensor`.""".replace("\n", " "),
|
|
317
|
+
#
|
|
318
|
+
"distributions": """\
|
|
319
|
+
Sequence specifying the distribution across processes of matrices `a`, `b` and `c`/`d`. The distribution needs to
|
|
320
|
+
be BlockCyclic or compatible.""".replace("\n", " "),
|
|
321
|
+
#
|
|
322
|
+
"alpha": """\
|
|
323
|
+
The scale factor for the matrix multiplication term as a real or complex number. The default is
|
|
324
|
+
:math:`1.0`.""".replace("\n", " "),
|
|
325
|
+
#
|
|
326
|
+
"beta": """\
|
|
327
|
+
The scale factor for the matrix addition term as a real or complex number. A value for `beta` must be provided if
|
|
328
|
+
operand `c` is specified.""".replace("\n", " "),
|
|
329
|
+
#
|
|
330
|
+
"quantization_scales": """\
|
|
331
|
+
Specify scale factors for the matrix multiplication as a :class:`~nvmath.distributed.linalg.advanced.MatmulQuantizationScales`
|
|
332
|
+
object. Alternatively, a `dict` containing the parameters for the
|
|
333
|
+
:class:`~nvmath.distributed.linalg.advanced.MatmulQuantizationScales`
|
|
334
|
+
constructor can also be provided. The scale factors can be provided as scalars or tensors.
|
|
335
|
+
If a scale factor is provided as a tensor, it must be from the same package and on the same
|
|
336
|
+
memory space (CPU or GPU device) as the operands of the matmul.
|
|
337
|
+
If a scale factor is provided as a scalar, and the execution space is GPU,
|
|
338
|
+
a CPU->GPU copy is inevitable. To avoid this copy, provide
|
|
339
|
+
the quantization scale as one-element array on the GPU.
|
|
340
|
+
Allowed and required only for narrow-precision (FP8 and lower) operations.""".replace("\n", " "),
|
|
341
|
+
#
|
|
342
|
+
"epilog": """\
|
|
343
|
+
Specify an epilog :math:`\\mathcal{F}` as an object of type :class:`MatmulEpilog` to apply to the result of the matrix
|
|
344
|
+
multiplication: :math:`\\mathcal{F}(\\alpha a @ b + \\beta c)`. The default is no epilog. See `cuBLASMp documentation
|
|
345
|
+
<https://docs.nvidia.com/cuda/cublasmp/usage/types.html#cublasmpmatmulepilogue-t>`_ for the list of
|
|
346
|
+
available epilogs.""".replace("\n", " "),
|
|
347
|
+
#
|
|
348
|
+
"epilog_inputs": """\
|
|
349
|
+
Specify the additional inputs needed for the selected epilog as a dictionary, where the key is the epilog input name and
|
|
350
|
+
the value is the epilog input. The epilog input must be a tensor with the same package and in the same memory space as
|
|
351
|
+
the operands (see the constructor for more information on the operands). If the required epilog inputs are not provided,
|
|
352
|
+
an exception is raised that lists the required epilog inputs. Some epilog inputs are generated by other epilogs. For
|
|
353
|
+
example, the epilog input for :class:`MatmulEpilog.DRELU` is generated by matrix multiplication with the same operands
|
|
354
|
+
using :class:`MatmulEpilog.RELU_AUX`. """.replace("\n", " "),
|
|
355
|
+
#
|
|
356
|
+
"qualifiers": """\
|
|
357
|
+
Specify the matrix qualifiers as a :class:`numpy.ndarray` of
|
|
358
|
+
:class:`~nvmath.distributed.linalg.advanced.matrix_qualifiers_dtype` objects of length 3
|
|
359
|
+
corresponding to the operands `a`, `b`, and `c`. See
|
|
360
|
+
:ref:`matrix-tensor-qualifiers` for the motivation behind qualifiers.""".replace("\n", " "),
|
|
361
|
+
#
|
|
362
|
+
"options": """\
|
|
363
|
+
Specify options for the matrix multiplication as a
|
|
364
|
+
:class:`~nvmath.distributed.linalg.advanced.MatmulOptions` object. Alternatively, a `dict` containing
|
|
365
|
+
the parameters for the ``MatmulOptions`` constructor can also be provided. If not specified, the
|
|
366
|
+
value will be set to the default-constructed ``MatmulOptions`` object.""".replace("\n", " "),
|
|
367
|
+
#
|
|
368
|
+
"preferences": """\
|
|
369
|
+
This parameter specifies the preferences for planning as a :class:`MatmulPlanPreferences` object. Alternatively, a
|
|
370
|
+
dictionary containing the parameters for the :class:`MatmulPlanPreferences` constructor can also be provided. If not
|
|
371
|
+
specified, the value will be set to the default-constructed :class:`MatmulPlanPreferences` object.
|
|
372
|
+
""".replace("\n", " "),
|
|
373
|
+
#
|
|
374
|
+
"result": """\
|
|
375
|
+
The result of the specified matrix multiplication (epilog applied), which remains on the same device and belongs to the
|
|
376
|
+
same package as the input operands. If an epilog (like :attr:`nvmath.distributed.linalg.advanced.MatmulEpilog.RELU_AUX`) that
|
|
377
|
+
results in extra output is used, or an extra output is requested (for example by setting
|
|
378
|
+
:attr:`~nvmath.distributed.linalg.advanced.MatmulOptions.result_amax` option in ``options`` argument),
|
|
379
|
+
a tuple is returned with the first element being the matrix multiplication result (epilog applied) and the second element
|
|
380
|
+
being the auxiliary output provided as a `dict`. """.replace("\n", " "),
|
|
381
|
+
#
|
|
382
|
+
"narrow_precision": """\
|
|
383
|
+
|
|
384
|
+
.. _narrow_precision:
|
|
385
|
+
|
|
386
|
+
Matrix multiplication with narrow-precision operands is supported, in FP8, MXFP8, and NVFP4 formats.
|
|
387
|
+
|
|
388
|
+
**FP8 and MXFP8**
|
|
389
|
+
|
|
390
|
+
FP8 and MXFP8 use ``float8_e4m3fn`` or ``float8_e5m2`` data types. The difference is the scaling mode:
|
|
391
|
+
FP8 (``block_scaling=False``) uses per-tensor scaling where a single scalar scale is applied to each operand;
|
|
392
|
+
MXFP8 (``block_scaling=True``) uses microscaling with 32-element blocks arranged in 128x128 tiles.
|
|
393
|
+
|
|
394
|
+
.. note::
|
|
395
|
+
|
|
396
|
+
**FP8 requires a device with compute capability 8.9 or higher** (Ada, Hopper, Blackwell or newer architecture).
|
|
397
|
+
**MXFP8 requires a device with compute capability 10.0 or higher** (Blackwell or newer architecture).
|
|
398
|
+
Please refer to the `compute capability table <https://developer.nvidia.com/cuda-gpus>`_
|
|
399
|
+
to check the compute capability of your device.
|
|
400
|
+
|
|
401
|
+
For FP8 operations:
|
|
402
|
+
|
|
403
|
+
* For each operand a scaling factor needs to be specified via ``quantization_scales`` argument.
|
|
404
|
+
* Maximum absolute value of the result (amax) can be requested via
|
|
405
|
+
:attr:`~nvmath.distributed.linalg.advanced.MatmulOptions.result_amax` option in ``options`` argument.
|
|
406
|
+
* Custom result type (both FP8 and non-FP8) can be requested via
|
|
407
|
+
:attr:`~nvmath.distributed.linalg.advanced.MatmulOptions.result_type` option in ``options`` argument.
|
|
408
|
+
|
|
409
|
+
For MXFP8 operations:
|
|
410
|
+
|
|
411
|
+
* :attr:`~nvmath.distributed.linalg.advanced.MatmulOptions.block_scaling` option must be set to ``True`` and
|
|
412
|
+
block scaling factors need to be specified via ``quantization_scales`` argument.
|
|
413
|
+
Utilities in :mod:`nvmath.linalg.advanced.helpers.matmul` can be used to create and modify
|
|
414
|
+
block scaling factors, see e.g. :func:`~nvmath.linalg.advanced.helpers.matmul.create_mxfp8_scale`.
|
|
415
|
+
* When the result type is a narrow-precision data type, the auxiliary output ``"d_out_scale"``
|
|
416
|
+
will be returned containing the scales used for result quantization.
|
|
417
|
+
|
|
418
|
+
*Layout Requirements*
|
|
419
|
+
|
|
420
|
+
Due to the requirements of narrow-precision GEMM kernels, the contracting dimension
|
|
421
|
+
K must be contiguous (stride-1) for both operands. The following layout constraints
|
|
422
|
+
apply to both FP8 and MXFP8:
|
|
423
|
+
|
|
424
|
+
* Operand ``a`` must be ``(K, M)`` with column-major layout.
|
|
425
|
+
|
|
426
|
+
* Operand ``b`` must be ``(K, N)`` with column-major layout.
|
|
427
|
+
|
|
428
|
+
.. attention::
|
|
429
|
+
|
|
430
|
+
Epilog support for MXFP8 is still evolving in the underlying cuBLASLt library,
|
|
431
|
+
so not every combination of epilog, data type, and layout is guaranteed to work.
|
|
432
|
+
If running into unsupported combinations, a cuBLASLt error will be raised
|
|
433
|
+
either at planning time or at execution time that will reveal the
|
|
434
|
+
root cause. These gaps are expected to be filled in future cuBLASLt releases.
|
|
435
|
+
|
|
436
|
+
For more details on the FP8 and MXFP8 formats in cuBLAS,
|
|
437
|
+
see the `cublasLtMatmul documentation <https://docs.nvidia.com/cuda/cublas/#cublasltmatmul>`_.
|
|
438
|
+
|
|
439
|
+
**NVFP4**
|
|
440
|
+
|
|
441
|
+
.. versionadded:: 1.0
|
|
442
|
+
NVFP4 support.
|
|
443
|
+
|
|
444
|
+
NVFP4 uses ``float4_e2m1fn_x2`` data type with block scaling (16-element blocks arranged in 128x64 tiles).
|
|
445
|
+
|
|
446
|
+
.. note::
|
|
447
|
+
|
|
448
|
+
NVFP4 matrix multiplication requires **a device with compute capability 10.0 or higher**
|
|
449
|
+
(Blackwell or newer architecture), and **PyTorch 2.9 or newer** for ``float4_e2m1fn_x2``
|
|
450
|
+
dtype support.
|
|
451
|
+
Please refer to the `compute capability table <https://developer.nvidia.com/cuda-gpus>`_
|
|
452
|
+
to check the compute capability of your device.
|
|
453
|
+
|
|
454
|
+
For NVFP4 operations:
|
|
455
|
+
|
|
456
|
+
* :attr:`~nvmath.distributed.linalg.advanced.MatmulOptions.block_scaling` option must be set to ``True`` and
|
|
457
|
+
block scaling factors need to be specified via ``quantization_scales`` argument.
|
|
458
|
+
* When the result type is a narrow-precision data type, the auxiliary output ``"d_out_scale"``
|
|
459
|
+
will be returned containing the scales used for result quantization.
|
|
460
|
+
|
|
461
|
+
*Layout and Packing Requirements*
|
|
462
|
+
|
|
463
|
+
FP4 data is per-byte packed: ``float4_e2m1fn_x2`` stores 2 FP4 values per byte.
|
|
464
|
+
The block scaling (VEC16_UE4M3) assigns one scale factor per 16 consecutive elements
|
|
465
|
+
along the innermost (stride-1) dimension of each operand. The layout requirements
|
|
466
|
+
below ensure that this innermost dimension corresponds to the contracting dimension K
|
|
467
|
+
for both operands.
|
|
468
|
+
|
|
469
|
+
* Operand ``a`` must be ``(K//2, M)`` with ``stride[-2] == 1`` and
|
|
470
|
+
``stride[-1] >= K//2``, i.e., column-wise packed along K. Note that ``stride[-1]``
|
|
471
|
+
can be larger than ``K//2`` to support sliced views, as long as the stride remains
|
|
472
|
+
16-byte aligned.
|
|
473
|
+
|
|
474
|
+
* Operand ``b`` must be ``(K//2, N)`` with ``stride[-2] == 1`` and
|
|
475
|
+
``stride[-1] >= K//2``, i.e., column-wise packed along K. Note that ``stride[-1]``
|
|
476
|
+
can be larger than ``K//2`` to support sliced views, as long as the stride remains
|
|
477
|
+
16-byte aligned.
|
|
478
|
+
|
|
479
|
+
If your data has the stride-1 axis along a dimension other than K,
|
|
480
|
+
you must repack it before calling :func:`matmul`.
|
|
481
|
+
|
|
482
|
+
When the result type is also FP4, the output is column-major packed along the M dimension:
|
|
483
|
+
``(M//2, N)``, strides ``(1, M//2)``.
|
|
484
|
+
|
|
485
|
+
.. attention::
|
|
486
|
+
|
|
487
|
+
Epilog support for NVFP4 is still evolving in the underlying cuBLASLt library,
|
|
488
|
+
so not every combination of epilog, data type, and layout is guaranteed to work.
|
|
489
|
+
If running into unsupported combinations, a cuBLASLt error will be raised
|
|
490
|
+
either at planning time or at execution time that will reveal the
|
|
491
|
+
root cause. These gaps are expected to be filled in future cuBLASLt releases.
|
|
492
|
+
|
|
493
|
+
*Helper Functions*
|
|
494
|
+
|
|
495
|
+
The :mod:`nvmath.linalg.advanced.helpers.matmul` module provides helpers for working with
|
|
496
|
+
FP4 encoding/decoding and NVFP4 block scales, see e.g.
|
|
497
|
+
:func:`~nvmath.linalg.advanced.helpers.matmul.quantize_to_fp4`,
|
|
498
|
+
:func:`~nvmath.linalg.advanced.helpers.matmul.unpack_fp4`,
|
|
499
|
+
:func:`~nvmath.linalg.advanced.helpers.matmul.get_block_scale_offset`,
|
|
500
|
+
:func:`~nvmath.linalg.advanced.helpers.matmul.to_block_scale`,
|
|
501
|
+
:func:`~nvmath.linalg.advanced.helpers.matmul.expand_block_scale`.
|
|
502
|
+
|
|
503
|
+
.. note::
|
|
504
|
+
These helpers operate on the local data (i.e. local tensor and local block scales)
|
|
505
|
+
on each process.
|
|
506
|
+
|
|
507
|
+
For more details on the NVFP4 format in cuBLAS,
|
|
508
|
+
see the `cublasLtMatmul documentation <https://docs.nvidia.com/cuda/cublas/#cublasltmatmul>`_.
|
|
509
|
+
For usage examples, see the relevant files in the
|
|
510
|
+
`examples/distributed/linalg/advanced/matmul
|
|
511
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/linalg/advanced/matmul>`_
|
|
512
|
+
directory.
|
|
513
|
+
""".strip(),
|
|
514
|
+
#
|
|
515
|
+
"semantics": """\
|
|
516
|
+
.. _semantics:
|
|
517
|
+
|
|
518
|
+
The semantics of the matrix multiplication follows :external:py:data:`numpy.matmul` semantics, with some restrictions on
|
|
519
|
+
broadcasting. In addition, the semantics for the fused matrix addition are described below:
|
|
520
|
+
|
|
521
|
+
* For in-place matrix multiplication (where the result is written into `c`) the result has the same shape as `c`.
|
|
522
|
+
""".strip(),
|
|
523
|
+
#
|
|
524
|
+
"reset_operands_unchecked": utils._reset_operand_unchecked_docstring(True),
|
|
525
|
+
}
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
class InvalidMatmulState(Exception):
|
|
530
|
+
pass
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _create_d_out_scale(result_class, num_output_elements, d_dtype_width, device_id, stream_holder):
|
|
534
|
+
"""
|
|
535
|
+
Create the d_out_scale tensor for block-scaled output.
|
|
536
|
+
Block-scaled narrow-precision output (FP4 or FP8) requires a scale tensor where each
|
|
537
|
+
scale covers a group of output elements:
|
|
538
|
+
|
|
539
|
+
- FP4 (VEC16_UE4M3): each scale covers 16 elements, stored as ``float8_e4m3fn``
|
|
540
|
+
- FP8 (VEC32_UE8M0): each scale covers 32 elements, stored as ``uint8``.
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
result_class: The tensor class used to create the output tensor.
|
|
544
|
+
num_output_elements: Total number of output elements.
|
|
545
|
+
d_dtype_width: Result dtype width.
|
|
546
|
+
device_id: The CUDA device id.
|
|
547
|
+
stream_holder: The stream holder for tensor allocation.
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
d_out_scale_tensor — the allocated scale tensor.
|
|
551
|
+
"""
|
|
552
|
+
if d_dtype_width == 4:
|
|
553
|
+
scale_group_size = 16
|
|
554
|
+
scale_dtype = "float8_e4m3fn" # UE4M3 format, consistent with input A/B scales
|
|
555
|
+
else:
|
|
556
|
+
assert d_dtype_width == 8, "Internal error."
|
|
557
|
+
scale_group_size = 32
|
|
558
|
+
scale_dtype = "uint8" # UE8M0 format
|
|
559
|
+
|
|
560
|
+
num_scales = num_output_elements // scale_group_size
|
|
561
|
+
d_out_scale = utils.create_empty_tensor(
|
|
562
|
+
result_class,
|
|
563
|
+
(num_scales,),
|
|
564
|
+
scale_dtype,
|
|
565
|
+
device_id,
|
|
566
|
+
stream_holder,
|
|
567
|
+
verify_strides=False,
|
|
568
|
+
)
|
|
569
|
+
return d_out_scale
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _realloc_if_needed_and_copy_to_mirror(src_holder, dest_holder, destination_device_id, stream_holder, symmetric_memory=None):
|
|
573
|
+
"""
|
|
574
|
+
Copy a CPU source operand into its device-side mirror (``dest_holder``).
|
|
575
|
+
|
|
576
|
+
Two cases:
|
|
577
|
+
|
|
578
|
+
* **Buffer exists** (``dest_holder.tensor is not None``): copy the
|
|
579
|
+
source data into the existing device buffer in-place via ``copy_()``.
|
|
580
|
+
* **Buffer released** (``dest_holder.tensor is None``, e.g. after
|
|
581
|
+
``release_operands()``): call ``src_holder.to()`` to allocate a new
|
|
582
|
+
device buffer and copy the data into it, then assign the underlying
|
|
583
|
+
raw tensor to ``dest_holder.tensor``. The ``dest_holder`` wrapper
|
|
584
|
+
object itself is never replaced, so its concrete type is preserved.
|
|
585
|
+
|
|
586
|
+
Args:
|
|
587
|
+
src_holder: Wrapped CPU source operand.
|
|
588
|
+
dest_holder: Device-side tensor wrapper whose ``.tensor`` may be None.
|
|
589
|
+
Note: ``dest_holder.device_id`` is not usable when ``.tensor`` is
|
|
590
|
+
None, hence the explicit ``destination_device_id`` parameter.
|
|
591
|
+
destination_device_id: CUDA device ordinal (int, never ``"cpu"``)
|
|
592
|
+
since this function is only used for CPU-to-GPU mirroring.
|
|
593
|
+
stream_holder: Stream wrapper for the copy / allocation.
|
|
594
|
+
symmetric_memory: Whether to copy to buffer intended for symmetric
|
|
595
|
+
memory usage ("nvshmem" or "nccl"), or None if not.
|
|
596
|
+
|
|
597
|
+
Returns:
|
|
598
|
+
True if a new buffer was allocated (data pointer changed),
|
|
599
|
+
False if the existing buffer was reused via in-place copy.
|
|
600
|
+
"""
|
|
601
|
+
if dest_holder.tensor is not None:
|
|
602
|
+
dest_holder.copy_(src_holder, stream_holder=stream_holder)
|
|
603
|
+
return False
|
|
604
|
+
dest_holder.tensor = src_holder.to(destination_device_id, stream_holder, symmetric_memory=symmetric_memory).tensor
|
|
605
|
+
return True
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
@utils.docstring_decorator(SHARED_MM_DOCUMENTATION, skip_missing=False)
|
|
609
|
+
class Matmul:
|
|
610
|
+
"""
|
|
611
|
+
Create a stateful object encapsulating the specified distributed matrix multiplication
|
|
612
|
+
computation :math:`\\alpha a @ b + \\beta c` and the required resources to perform the
|
|
613
|
+
operation. A stateful object can be used to amortize the cost of preparation (planning
|
|
614
|
+
in the case of matrix multiplication) across multiple executions (also see the
|
|
615
|
+
:ref:`Stateful APIs <host api types>` section).
|
|
616
|
+
|
|
617
|
+
The function-form API :func:`matmul` is a convenient alternative to using stateful
|
|
618
|
+
objects for *single* use (the user needs to perform just one matrix multiplication, for
|
|
619
|
+
example), in which case there is no possibility of amortizing preparatory costs. The
|
|
620
|
+
function-form APIs are just convenience wrappers around the stateful object APIs.
|
|
621
|
+
|
|
622
|
+
Using the stateful object typically involves the following steps:
|
|
623
|
+
|
|
624
|
+
1. **Problem Specification**: Initialize the object with a defined operation and
|
|
625
|
+
options.
|
|
626
|
+
2. **Preparation**: Use :meth:`plan` to determine the best algorithmic implementation
|
|
627
|
+
for this specific matrix multiplication operation.
|
|
628
|
+
3. **Execution**: Perform the matrix multiplication computation with :meth:`execute`.
|
|
629
|
+
4. **Resource Management**: Ensure all resources are released either by explicitly
|
|
630
|
+
calling :meth:`free` or by managing the stateful object within a context manager.
|
|
631
|
+
|
|
632
|
+
Detailed information on what's happening in the various phases described above can be
|
|
633
|
+
obtained by passing in a :class:`logging.Logger` object to :class:`MatmulOptions` or by
|
|
634
|
+
setting the appropriate options in the root logger object, which is used by default:
|
|
635
|
+
|
|
636
|
+
>>> import logging
|
|
637
|
+
>>> logging.basicConfig(
|
|
638
|
+
... level=logging.INFO,
|
|
639
|
+
... format="%(asctime)s %(levelname)-8s %(message)s",
|
|
640
|
+
... datefmt="%m-%d %H:%M:%S",
|
|
641
|
+
... )
|
|
642
|
+
|
|
643
|
+
A user can select the desired logging level and, in general, take advantage of all of
|
|
644
|
+
the functionality offered by the Python `logging` module.
|
|
645
|
+
|
|
646
|
+
Args:
|
|
647
|
+
a: {a}
|
|
648
|
+
|
|
649
|
+
b: {b}
|
|
650
|
+
|
|
651
|
+
c: {c}
|
|
652
|
+
|
|
653
|
+
distributions: {distributions}
|
|
654
|
+
|
|
655
|
+
alpha: {alpha}
|
|
656
|
+
|
|
657
|
+
beta: {beta}
|
|
658
|
+
|
|
659
|
+
qualifiers: {qualifiers}
|
|
660
|
+
|
|
661
|
+
quantization_scales: {quantization_scales}
|
|
662
|
+
|
|
663
|
+
options: {options}
|
|
664
|
+
|
|
665
|
+
stream: {stream}
|
|
666
|
+
|
|
667
|
+
Semantics:
|
|
668
|
+
{semantics}
|
|
669
|
+
|
|
670
|
+
Narrow-precision support:
|
|
671
|
+
{narrow_precision}
|
|
672
|
+
|
|
673
|
+
.. seealso::
|
|
674
|
+
:meth:`plan`, :meth:`reset_operands`,
|
|
675
|
+
:meth:`reset_operands_unchecked`, :meth:`execute`
|
|
676
|
+
|
|
677
|
+
Examples:
|
|
678
|
+
|
|
679
|
+
>>> import numpy as np
|
|
680
|
+
>>> import nvmath.distributed
|
|
681
|
+
>>> from nvmath.distributed.distribution import Slab
|
|
682
|
+
>>> from nvmath.distributed.linalg.advanced import matrix_qualifiers_dtype
|
|
683
|
+
|
|
684
|
+
Get process group used to initialize nvmath.distributed (for information on
|
|
685
|
+
initializing ``nvmath.distributed``, you can refer to the documentation or to the
|
|
686
|
+
Matmul examples in `nvmath/examples/distributed/linalg/advanced
|
|
687
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/linalg/advanced/matmul>`_):
|
|
688
|
+
|
|
689
|
+
>>> process_group = nvmath.distributed.get_context().process_group
|
|
690
|
+
|
|
691
|
+
Get my process rank:
|
|
692
|
+
|
|
693
|
+
>>> rank = process_group.rank
|
|
694
|
+
|
|
695
|
+
Create two 2-D float64 ndarrays on the CPU (using Slab distributions to distribute
|
|
696
|
+
the matrices across processes):
|
|
697
|
+
|
|
698
|
+
>>> M, N, K = 1024, 1024, 1024
|
|
699
|
+
>>> a_shape = Slab.X.shape(rank, (K, M))
|
|
700
|
+
>>> b_shape = Slab.X.shape(rank, (K, N))
|
|
701
|
+
>>> a = np.asfortranarray(np.random.rand(*a_shape))
|
|
702
|
+
>>> b = np.asfortranarray(np.random.rand(*b_shape))
|
|
703
|
+
|
|
704
|
+
We will define a matrix multiplication operation followed by an AllReduce epilog
|
|
705
|
+
using the specialized matrix multiplication interface.
|
|
706
|
+
|
|
707
|
+
Create a Matmul object encapsulating the problem specification above:
|
|
708
|
+
|
|
709
|
+
>>> qualifiers = np.zeros((3,), dtype=matrix_qualifiers_dtype)
|
|
710
|
+
>>> qualifiers[0]["is_transpose"] = True # a is transposed
|
|
711
|
+
>>> distributions = [Slab.X, Slab.X, Slab.Y]
|
|
712
|
+
>>> mm = nvmath.distributed.linalg.advanced.Matmul(
|
|
713
|
+
... a, b, distributions=distributions, qualifiers=qualifiers
|
|
714
|
+
... )
|
|
715
|
+
|
|
716
|
+
Options can be provided above to control the behavior of the operation using the
|
|
717
|
+
`options` argument (see :class:`MatmulOptions`).
|
|
718
|
+
|
|
719
|
+
Next, plan the operation. The epilog is specified, and optionally, preferences can
|
|
720
|
+
be specified for planning:
|
|
721
|
+
|
|
722
|
+
>>> epilog = nvmath.distributed.linalg.advanced.MatmulEpilog.ALLREDUCE
|
|
723
|
+
>>> mm.plan(epilog=epilog)
|
|
724
|
+
|
|
725
|
+
Now execute the matrix multiplication, and obtain the result `r1` as a NumPy
|
|
726
|
+
ndarray.
|
|
727
|
+
|
|
728
|
+
>>> r1 = mm.execute()
|
|
729
|
+
|
|
730
|
+
Finally, free the object's resources. To avoid having to explicitly make this
|
|
731
|
+
call, it's recommended to use the Matmul object as a context manager as shown below,
|
|
732
|
+
if possible.
|
|
733
|
+
|
|
734
|
+
>>> mm.free()
|
|
735
|
+
|
|
736
|
+
Note that all :class:`Matmul` methods execute on the current stream by default.
|
|
737
|
+
Alternatively, the `stream` argument can be used to run a method on a specified
|
|
738
|
+
stream.
|
|
739
|
+
|
|
740
|
+
Let's now look at the same problem with CuPy ndarrays on the GPU.
|
|
741
|
+
|
|
742
|
+
>>> device_id = nvmath.distributed.get_context().device_id
|
|
743
|
+
>>> import cupy as cp
|
|
744
|
+
>>> with cp.cuda.Device(device_id):
|
|
745
|
+
... a = cp.asfortranarray(cp.random.rand(*a_shape))
|
|
746
|
+
... b = cp.asfortranarray(cp.random.rand(*b_shape))
|
|
747
|
+
|
|
748
|
+
Create a Matmul object encapsulating the problem specification described earlier
|
|
749
|
+
and use it as a context manager.
|
|
750
|
+
|
|
751
|
+
>>> with nvmath.distributed.linalg.advanced.Matmul(
|
|
752
|
+
... a, b, distributions=distributions, qualifiers=qualifiers
|
|
753
|
+
... ) as mm:
|
|
754
|
+
... mm.plan(epilog=epilog)
|
|
755
|
+
...
|
|
756
|
+
... # Execute the operation to get the first result.
|
|
757
|
+
... r1 = mm.execute()
|
|
758
|
+
...
|
|
759
|
+
... # Update operands 'a' and 'b' in-place (see reset_operands() for an
|
|
760
|
+
... # alternative).
|
|
761
|
+
... with cp.cuda.Device(device_id):
|
|
762
|
+
... a[:] = cp.random.rand(*a_shape)
|
|
763
|
+
... b[:] = cp.random.rand(*b_shape)
|
|
764
|
+
...
|
|
765
|
+
... # Execute the operation to get the new result.
|
|
766
|
+
... r2 = mm.execute()
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
All the resources used by the object are released at the end of the block.
|
|
770
|
+
|
|
771
|
+
Further examples can be found in the
|
|
772
|
+
`nvmath/examples/distributed/linalg/advanced/matmul
|
|
773
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/linalg/advanced/matmul>`_
|
|
774
|
+
directory.
|
|
775
|
+
"""
|
|
776
|
+
|
|
777
|
+
def __init__(
|
|
778
|
+
self,
|
|
779
|
+
a,
|
|
780
|
+
b,
|
|
781
|
+
/,
|
|
782
|
+
c=None,
|
|
783
|
+
*,
|
|
784
|
+
distributions: Sequence[Distribution],
|
|
785
|
+
alpha=None,
|
|
786
|
+
beta=None,
|
|
787
|
+
qualifiers=None,
|
|
788
|
+
quantization_scales=None,
|
|
789
|
+
options: MatmulOptions | dict[str, typing.Any] | None = None,
|
|
790
|
+
stream: utils.AnyStream | int | None = None,
|
|
791
|
+
):
|
|
792
|
+
distributed_ctx = nvmath.distributed.get_context()
|
|
793
|
+
if distributed_ctx is None:
|
|
794
|
+
raise RuntimeError(
|
|
795
|
+
"nvmath.distributed has not been initialized. Refer to "
|
|
796
|
+
"https://docs.nvidia.com/cuda/nvmath-python/latest/distributed-apis/runtime.html"
|
|
797
|
+
" for more information."
|
|
798
|
+
)
|
|
799
|
+
if distributed_ctx.nccl_comm is None:
|
|
800
|
+
raise RuntimeError("nvmath.distributed wasn't initialized with NCCL backend")
|
|
801
|
+
self.process_group = process_group = distributed_ctx.process_group
|
|
802
|
+
self.rank = rank = process_group.rank
|
|
803
|
+
self.nranks = nranks = process_group.nranks
|
|
804
|
+
|
|
805
|
+
self.options = options = cast(
|
|
806
|
+
MatmulOptions, utils.check_or_create_options(MatmulOptions, options, "Distributed matrix multiplication options")
|
|
807
|
+
)
|
|
808
|
+
self.logger = options.logger if options.logger is not None else logging.getLogger()
|
|
809
|
+
|
|
810
|
+
# The matrix multiplication has two required operands 'a' and 'b', and one optional
|
|
811
|
+
# operand 'c'.
|
|
812
|
+
a = tensor_wrapper.wrap_operand(a)
|
|
813
|
+
b = tensor_wrapper.wrap_operand(b)
|
|
814
|
+
if c is not None:
|
|
815
|
+
c = tensor_wrapper.wrap_operand(c)
|
|
816
|
+
|
|
817
|
+
operands = [a, b, c] if c is not None else [a, b]
|
|
818
|
+
|
|
819
|
+
version = cublasMp.get_version()
|
|
820
|
+
|
|
821
|
+
problem_spec = _ProblemSpec(
|
|
822
|
+
distributions=[d.copy() for d in distributions],
|
|
823
|
+
shapes=[list(o.shape) for o in operands], # local shapes
|
|
824
|
+
operand_dtypes=[o.dtype for o in operands],
|
|
825
|
+
options=_ProblemSpec.Options(options),
|
|
826
|
+
packages=[o.name for o in operands],
|
|
827
|
+
memory_spaces=[o.device for o in operands],
|
|
828
|
+
device_ids=[o.device_id for o in operands],
|
|
829
|
+
compute_capability=tuple(Device(distributed_ctx.device_id).compute_capability),
|
|
830
|
+
alpha=alpha,
|
|
831
|
+
beta=beta,
|
|
832
|
+
qualifiers=qualifiers,
|
|
833
|
+
is_F=[sorted(o.strides) == list(o.strides) and is_contiguous_and_dense(o.shape, o.strides) for o in operands],
|
|
834
|
+
lib_version=version,
|
|
835
|
+
nranks=nranks,
|
|
836
|
+
rank=rank,
|
|
837
|
+
b_min_size=operands[1].size,
|
|
838
|
+
b_max_size=operands[1].size,
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
if nranks > 1:
|
|
842
|
+
problem_spec = process_group.allreduce_object(problem_spec, op=_problem_spec_reducer)
|
|
843
|
+
else:
|
|
844
|
+
# Ensure we error-check with one rank.
|
|
845
|
+
problem_spec = _problem_spec_reducer(problem_spec, problem_spec)
|
|
846
|
+
if isinstance(problem_spec, Exception):
|
|
847
|
+
# There is an error or inconsistency in the problem spec across processes.
|
|
848
|
+
# Note that since this comes from an allreduce, all processes will have
|
|
849
|
+
# received the same exception.
|
|
850
|
+
raise problem_spec
|
|
851
|
+
|
|
852
|
+
# Note that the problem spec reducer above already checks that
|
|
853
|
+
# for FP4 both A and B are using float4_e2m1fn_x2.
|
|
854
|
+
self.using_fp4_ab = operands[0].dtype == "float4_e2m1fn_x2"
|
|
855
|
+
|
|
856
|
+
self.num_operands = len(operands)
|
|
857
|
+
|
|
858
|
+
if (
|
|
859
|
+
options.inplace
|
|
860
|
+
and options.result_type is not None
|
|
861
|
+
and self.num_operands == 3
|
|
862
|
+
and options.result_type != typemaps.NAME_TO_DATA_TYPE[c.dtype]
|
|
863
|
+
):
|
|
864
|
+
raise ValueError(
|
|
865
|
+
f"When specifying a result type ({typemaps.DATA_TYPE_TO_NAME[options.result_type]}) "
|
|
866
|
+
f"different from the c data type ({c.dtype}), inplace must be set to False."
|
|
867
|
+
)
|
|
868
|
+
self.inplace = False if self.num_operands == 2 else options.inplace
|
|
869
|
+
|
|
870
|
+
self.logger.info("= SPECIFICATION PHASE =")
|
|
871
|
+
if self.inplace:
|
|
872
|
+
self.logger.info("The MM operation will be performed in-place (the result will be written into operand C).")
|
|
873
|
+
self.logger.info("For performance and debugging hints, use CUBLASMP_LOG_LEVEL=5 and CUBLASLT_LOG_LEVEL=5")
|
|
874
|
+
self.logger.info(f"The data type of operand A is '{a.dtype}', and that of operand B is '{b.dtype}'.")
|
|
875
|
+
|
|
876
|
+
if c is not None:
|
|
877
|
+
self.logger.info(f"The data type of operand C is {c.dtype}.")
|
|
878
|
+
if beta is None:
|
|
879
|
+
raise ValueError("A value for beta must be provided if operand C is provided.")
|
|
880
|
+
|
|
881
|
+
if (a.dtype, b.dtype) not in NAMES_TO_DEFAULT_SCALE_TYPE:
|
|
882
|
+
raise ValueError(f"Unsupported combination of dtypes for operands A {a.dtype} and B {b.dtype}.")
|
|
883
|
+
|
|
884
|
+
operand_name = "ABC"
|
|
885
|
+
for i in range(self.num_operands):
|
|
886
|
+
global_shape = problem_spec.distributions[i]._global_shape_reduction_epilogue()
|
|
887
|
+
problem_spec.shapes[i] = list(global_shape)
|
|
888
|
+
self.logger.info(f"The global shape of operand {operand_name[i]} is {global_shape}.")
|
|
889
|
+
|
|
890
|
+
for i in range(3):
|
|
891
|
+
try:
|
|
892
|
+
problem_spec.distributions[i] = problem_spec.distributions[i].to(BlockCyclic, ndim=2)
|
|
893
|
+
except ConvertDistributionError as e:
|
|
894
|
+
raise TypeError("Distributed matrix multiplicaton requires distributions compatible with BlockCyclic") from e
|
|
895
|
+
|
|
896
|
+
self.distributions = distributions = cast(Sequence[BlockCyclic], problem_spec.distributions)
|
|
897
|
+
|
|
898
|
+
self.logger.info(f"The distribution of operand A is {self.distributions[0]}")
|
|
899
|
+
self.logger.info(f"The distribution of operand B is {self.distributions[1]}")
|
|
900
|
+
self.logger.info(f"The distribution of operand C/D is {self.distributions[2]}")
|
|
901
|
+
|
|
902
|
+
# Currently, a.dtype != b.dtype is only supported for FP8 (different FP8 kinds are
|
|
903
|
+
# allowed), so we assume that A and B have equal width.
|
|
904
|
+
self.input_type_width = typemaps.NAME_TO_DATA_WIDTH[a.dtype]
|
|
905
|
+
|
|
906
|
+
assert self.num_operands == 2 or self.num_operands == 3, "Internal Error."
|
|
907
|
+
|
|
908
|
+
# Infer the library package & device ID the operands belong to.
|
|
909
|
+
# Note that wrappers are kept even after release_operands();
|
|
910
|
+
# see that method's docstring for details.
|
|
911
|
+
self.operands: list[DistributedTensor] = operands
|
|
912
|
+
|
|
913
|
+
self.package = utils.get_operands_package(operands)
|
|
914
|
+
# Package used for internal stream operations. NumPy uses cuda.core.
|
|
915
|
+
self.internal_op_package = "cuda" if self.package == "numpy" else self.package
|
|
916
|
+
self.memory_space = "cuda"
|
|
917
|
+
self.device_id = utils.get_operands_device_id(operands)
|
|
918
|
+
if self.device_id == "cpu":
|
|
919
|
+
self.memory_space = "cpu"
|
|
920
|
+
self.device_id = distributed_ctx.device_id
|
|
921
|
+
elif self.device_id != distributed_ctx.device_id:
|
|
922
|
+
raise RuntimeError(
|
|
923
|
+
"The operands are not on the same device as the one assigned to the distributed "
|
|
924
|
+
f"runtime on this process: operands' device ID is {self.device_id} and the runtime "
|
|
925
|
+
f"device ID is {distributed_ctx.device_id}"
|
|
926
|
+
)
|
|
927
|
+
self.logger.info(
|
|
928
|
+
f"The input operands' memory space is {self.memory_space}, and the execution space is on device {self.device_id}."
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
nccl_comm = distributed_ctx.nccl_comm
|
|
932
|
+
|
|
933
|
+
# Allocate device memory (in stream context) if needed.
|
|
934
|
+
stream_holder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
|
|
935
|
+
self.logger.info(f"The specified stream for the Matmul ctor is {stream_holder.obj}.")
|
|
936
|
+
|
|
937
|
+
# Copy operands to device (and store reference to CPU operand), if needed.
|
|
938
|
+
self.cpu_c_ref = None
|
|
939
|
+
if self.memory_space == "cpu":
|
|
940
|
+
if self.inplace:
|
|
941
|
+
self.cpu_c_ref = self.operands[2] # Hold reference, needed for inplace operations.
|
|
942
|
+
# Currently, AG+GEMM in cuBLASMp performs better when B is on symmetric
|
|
943
|
+
# memory, so the buffer for B is allocated in preparation for NCCL symmetric
|
|
944
|
+
# memory registration.
|
|
945
|
+
self.operands = [
|
|
946
|
+
o.to(self.device_id, stream_holder, symmetric_memory="nccl" if i == 1 else None)
|
|
947
|
+
for i, o in enumerate(self.operands)
|
|
948
|
+
]
|
|
949
|
+
|
|
950
|
+
self._set_result_sheap_flag()
|
|
951
|
+
|
|
952
|
+
# Set qualifiers.
|
|
953
|
+
self.qualifiers = problem_spec.qualifiers
|
|
954
|
+
# Set qualifiers based on torch lazy conjugation flag if not provided.
|
|
955
|
+
self.qualifiers[0]["is_conjugate"] = self.qualifiers[0]["is_conjugate"] ^ self.operands[0].is_conjugate
|
|
956
|
+
self.qualifiers[1]["is_conjugate"] = self.qualifiers[1]["is_conjugate"] ^ self.operands[1].is_conjugate
|
|
957
|
+
self.lazy_conjugation = (self.operands[0].is_conjugate, self.operands[1].is_conjugate, False)
|
|
958
|
+
if self.num_operands == 3:
|
|
959
|
+
self.qualifiers[2]["is_conjugate"] = self.qualifiers[2]["is_conjugate"] ^ self.operands[2].is_conjugate
|
|
960
|
+
if self.qualifiers[2]["is_conjugate"]:
|
|
961
|
+
raise ValueError("The conjugate flag is currently not supported for operand C.")
|
|
962
|
+
if self.qualifiers[2]["is_transpose"]:
|
|
963
|
+
raise ValueError("The transpose flag is currently not supported for operand C.")
|
|
964
|
+
for i in range(2):
|
|
965
|
+
if self.qualifiers[i]["is_conjugate"] and not self.qualifiers[i]["is_transpose"]:
|
|
966
|
+
raise ValueError("Conjugate is not supported without transpose")
|
|
967
|
+
|
|
968
|
+
# Set blocking or non-blocking behavior.
|
|
969
|
+
self.blocking = self.options.blocking is True or self.memory_space == "cpu"
|
|
970
|
+
if self.blocking:
|
|
971
|
+
self.call_prologue = "This call is blocking and will return only after the operation is complete."
|
|
972
|
+
else:
|
|
973
|
+
self.call_prologue = (
|
|
974
|
+
"This call is non-blocking and will return immediately after the operation is launched on the device."
|
|
975
|
+
)
|
|
976
|
+
|
|
977
|
+
# The result class is that of the first wrapped device operand.
|
|
978
|
+
self.result_class = self.operands[0].__class__
|
|
979
|
+
|
|
980
|
+
# Create cuBLASMp handle.
|
|
981
|
+
with utils.device_ctx(self.device_id):
|
|
982
|
+
self.handle: int = cublasMp.create(stream_holder.ptr)
|
|
983
|
+
|
|
984
|
+
# Determine the data types for a and b.
|
|
985
|
+
self.a_dtype = typemaps.NAME_TO_DATA_TYPE[a.dtype]
|
|
986
|
+
self.b_dtype = typemaps.NAME_TO_DATA_TYPE[b.dtype]
|
|
987
|
+
self.a_dtype_name = a.dtype
|
|
988
|
+
self.b_dtype_name = b.dtype
|
|
989
|
+
|
|
990
|
+
self.is_complex = "complex" in self.a_dtype_name or "complex" in self.b_dtype_name
|
|
991
|
+
|
|
992
|
+
for i, dtype_name in enumerate((a.dtype, b.dtype)):
|
|
993
|
+
if self.qualifiers[i]["is_conjugate"] and "complex" not in dtype_name:
|
|
994
|
+
raise ValueError("The conjugate flag only applies to complex operands")
|
|
995
|
+
|
|
996
|
+
# Determine the data types for c and d.
|
|
997
|
+
self.d_dtype = None if self.inplace else options.result_type
|
|
998
|
+
if self.num_operands == 3:
|
|
999
|
+
self.c_dtype = typemaps.NAME_TO_DATA_TYPE[c.dtype]
|
|
1000
|
+
if self.d_dtype is None:
|
|
1001
|
+
self.d_dtype = self.c_dtype
|
|
1002
|
+
|
|
1003
|
+
elif self.num_operands == 2:
|
|
1004
|
+
if self.d_dtype is None:
|
|
1005
|
+
self.d_dtype = self.a_dtype
|
|
1006
|
+
|
|
1007
|
+
# c_dtype matches d_dtype, except if output is FP4/FP8, then C uses float16
|
|
1008
|
+
if self.d_dtype in (CudaDataType.CUDA_R_4F_E2M1, CudaDataType.CUDA_R_8F_E5M2, CudaDataType.CUDA_R_8F_E4M3):
|
|
1009
|
+
self.c_dtype = CudaDataType.CUDA_R_16F
|
|
1010
|
+
else:
|
|
1011
|
+
self.c_dtype = self.d_dtype
|
|
1012
|
+
|
|
1013
|
+
self.c_dtype_name = typemaps.DATA_TYPE_TO_NAME[self.c_dtype]
|
|
1014
|
+
self.d_dtype_name = typemaps.DATA_TYPE_TO_NAME[self.d_dtype]
|
|
1015
|
+
self.c_dtype_width = typemaps.NAME_TO_DATA_WIDTH[self.c_dtype_name]
|
|
1016
|
+
self.d_dtype_width = typemaps.NAME_TO_DATA_WIDTH[self.d_dtype_name]
|
|
1017
|
+
|
|
1018
|
+
self.logger.info(f"The data type for the result D is '{self.d_dtype_name}'.")
|
|
1019
|
+
|
|
1020
|
+
def assert_valid_compute_type(compute_type):
|
|
1021
|
+
if compute_type not in COMPUTE_TYPE_TO_DEFAULT_SCALE_TYPE["real"]:
|
|
1022
|
+
message = f"Unsupported compute type. The compute type '{repr(compute_type)}' is currently not supported."
|
|
1023
|
+
raise ValueError(message)
|
|
1024
|
+
|
|
1025
|
+
# Determine the scale type.
|
|
1026
|
+
if options.scale_type is None:
|
|
1027
|
+
if options.compute_type is not None:
|
|
1028
|
+
assert_valid_compute_type(options.compute_type)
|
|
1029
|
+
if self.is_complex:
|
|
1030
|
+
scale_type_map = COMPUTE_TYPE_TO_DEFAULT_SCALE_TYPE["complex"]
|
|
1031
|
+
else:
|
|
1032
|
+
scale_type_map = COMPUTE_TYPE_TO_DEFAULT_SCALE_TYPE["real"]
|
|
1033
|
+
self.scale_type = scale_type_map[options.compute_type]
|
|
1034
|
+
else:
|
|
1035
|
+
self.scale_type = NAMES_TO_DEFAULT_SCALE_TYPE[(self.a_dtype_name, self.b_dtype_name)]
|
|
1036
|
+
self.scale_type_name = typemaps.DATA_TYPE_TO_NAME[self.scale_type]
|
|
1037
|
+
else:
|
|
1038
|
+
self.scale_type = options.scale_type
|
|
1039
|
+
if self.scale_type not in SCALE_TYPE_TO_DEFAULT_COMPUTE_TYPE:
|
|
1040
|
+
message = f"Unsupported scale type. The data type '{repr(self.scale_type)}' is currently not supported."
|
|
1041
|
+
raise ValueError(message)
|
|
1042
|
+
self.scale_type_name = typemaps.DATA_TYPE_TO_NAME[self.scale_type]
|
|
1043
|
+
self.logger.info(f"The scale type is '{self.scale_type_name}'.")
|
|
1044
|
+
|
|
1045
|
+
# Determine the compute type.
|
|
1046
|
+
if options.compute_type is None:
|
|
1047
|
+
if options.scale_type is not None:
|
|
1048
|
+
self.compute_type = SCALE_TYPE_TO_DEFAULT_COMPUTE_TYPE[options.scale_type]
|
|
1049
|
+
else:
|
|
1050
|
+
self.compute_type = NAMES_TO_DEFAULT_COMPUTE_TYPE[(self.a_dtype_name, self.b_dtype_name)]
|
|
1051
|
+
else:
|
|
1052
|
+
self.compute_type = options.compute_type
|
|
1053
|
+
assert_valid_compute_type(self.compute_type)
|
|
1054
|
+
self.logger.info(f"The compute type is {self.compute_type.name}.")
|
|
1055
|
+
|
|
1056
|
+
def is_supported(atype, btype, compute_type, scale_type):
|
|
1057
|
+
ct = cublas.ComputeType
|
|
1058
|
+
st = CudaDataType
|
|
1059
|
+
abtype = atype if atype == btype else (atype, btype)
|
|
1060
|
+
if compute_type in (ct.COMPUTE_16F, ct.COMPUTE_16F_PEDANTIC):
|
|
1061
|
+
return scale_type == st.CUDA_R_16F and abtype == "float16"
|
|
1062
|
+
elif compute_type == ct.COMPUTE_32F_PEDANTIC:
|
|
1063
|
+
if scale_type == st.CUDA_R_32F:
|
|
1064
|
+
return abtype in ("float32", "bfloat16", "float16", "float8_e4m3fn", "float8_e5m2")
|
|
1065
|
+
elif scale_type == st.CUDA_C_32F:
|
|
1066
|
+
return abtype == "complex64"
|
|
1067
|
+
elif compute_type == ct.COMPUTE_32F:
|
|
1068
|
+
if scale_type == st.CUDA_R_32F:
|
|
1069
|
+
return abtype in (
|
|
1070
|
+
"float32",
|
|
1071
|
+
"bfloat16",
|
|
1072
|
+
"float16",
|
|
1073
|
+
"float8_e4m3fn",
|
|
1074
|
+
"float8_e5m2",
|
|
1075
|
+
("float8_e4m3fn", "float8_e5m2"),
|
|
1076
|
+
("float8_e5m2", "float8_e4m3fn"),
|
|
1077
|
+
)
|
|
1078
|
+
elif scale_type == st.CUDA_C_32F:
|
|
1079
|
+
return abtype == "complex64"
|
|
1080
|
+
elif compute_type in (
|
|
1081
|
+
ct.COMPUTE_32F_FAST_16F,
|
|
1082
|
+
ct.COMPUTE_32F_FAST_16BF,
|
|
1083
|
+
ct.COMPUTE_32F_FAST_TF32,
|
|
1084
|
+
ct.COMPUTE_32F_EMULATED_16BFX9,
|
|
1085
|
+
):
|
|
1086
|
+
if scale_type == st.CUDA_R_32F:
|
|
1087
|
+
return abtype == "float32"
|
|
1088
|
+
if scale_type == st.CUDA_C_32F:
|
|
1089
|
+
return abtype == "complex64"
|
|
1090
|
+
elif compute_type in (ct.COMPUTE_64F, ct.COMPUTE_64F_PEDANTIC, ct.COMPUTE_64F_EMULATED_FIXEDPOINT):
|
|
1091
|
+
if scale_type == st.CUDA_R_64F:
|
|
1092
|
+
return abtype == "float64"
|
|
1093
|
+
if scale_type == st.CUDA_C_64F:
|
|
1094
|
+
return abtype == "complex128"
|
|
1095
|
+
return False
|
|
1096
|
+
|
|
1097
|
+
def is_supported_nvfp4(ctype, dtype, compute_type, scale_type):
|
|
1098
|
+
"""
|
|
1099
|
+
Validate type combinations based on cuBLAS documentation.
|
|
1100
|
+
https://docs.nvidia.com/cuda/cublas/index.html#id105,
|
|
1101
|
+
see Table 4 and related text.
|
|
1102
|
+
"""
|
|
1103
|
+
ct = cublas.ComputeType
|
|
1104
|
+
st = CudaDataType
|
|
1105
|
+
|
|
1106
|
+
# Check compute/scale types
|
|
1107
|
+
if not (compute_type == ct.COMPUTE_32F and scale_type == st.CUDA_R_32F):
|
|
1108
|
+
raise ValueError(
|
|
1109
|
+
f"Selected scale_type={repr(self.scale_type)} compute_type={repr(self.compute_type)} "
|
|
1110
|
+
f"are not supported. Only the following combination is supported: "
|
|
1111
|
+
f"A and B must both be float4_e2m1fn_x2, compute_type=COMPUTE_32F, scale_type=CUDA_R_32F."
|
|
1112
|
+
)
|
|
1113
|
+
|
|
1114
|
+
# Check valid C/D type combinations (Table 4)
|
|
1115
|
+
valid_cd_combos = {
|
|
1116
|
+
("bfloat16", "bfloat16"),
|
|
1117
|
+
("bfloat16", "float4_e2m1fn_x2"),
|
|
1118
|
+
("float16", "float16"),
|
|
1119
|
+
("float16", "float4_e2m1fn_x2"),
|
|
1120
|
+
("float32", "float32"),
|
|
1121
|
+
}
|
|
1122
|
+
if (ctype, dtype) not in valid_cd_combos:
|
|
1123
|
+
raise ValueError(
|
|
1124
|
+
f"Invalid C/D type combination for FP4: ctype={self.c_dtype_name}, dtype={self.d_dtype_name}. "
|
|
1125
|
+
f"Valid combinations are: (bfloat16, bfloat16), (bfloat16, float4_e2m1fn_x2), "
|
|
1126
|
+
f"(float16, float16), (float16, float4_e2m1fn_x2), (float32, float32). "
|
|
1127
|
+
f"See cuBLAS documentation Table 4."
|
|
1128
|
+
)
|
|
1129
|
+
|
|
1130
|
+
if self.using_fp4_ab:
|
|
1131
|
+
# Validate remaining type combinations (A and B are already
|
|
1132
|
+
# known to be float4_e2m1fn_x2)
|
|
1133
|
+
is_supported_nvfp4(self.c_dtype_name, self.d_dtype_name, self.compute_type, self.scale_type)
|
|
1134
|
+
elif not is_supported(self.a_dtype_name, self.b_dtype_name, self.compute_type, self.scale_type):
|
|
1135
|
+
raise ValueError(
|
|
1136
|
+
f"Selected scale_type={repr(self.scale_type)} compute_type={repr(self.compute_type)} "
|
|
1137
|
+
+ f"are not supported for data types {self.a_dtype_name} (A) and {self.b_dtype_name} (B)."
|
|
1138
|
+
)
|
|
1139
|
+
|
|
1140
|
+
# Set alpha and beta.
|
|
1141
|
+
self.alpha = np.zeros((1,), dtype=self.scale_type_name)
|
|
1142
|
+
try:
|
|
1143
|
+
self.alpha[0] = alpha if alpha is not None else 1
|
|
1144
|
+
except (ValueError, TypeError) as e:
|
|
1145
|
+
raise ValueError(f"The value provided for alpha {alpha} is not convertible to dtype '{self.alpha.dtype}'.") from e
|
|
1146
|
+
|
|
1147
|
+
self.beta = np.zeros((1,), dtype=self.scale_type_name)
|
|
1148
|
+
if beta is not None and self.num_operands == 2:
|
|
1149
|
+
self.logger.warning(f"Matmul: The provided beta value {beta} is ignored since operand C is not specified.")
|
|
1150
|
+
try:
|
|
1151
|
+
self.beta[0] = beta if beta is not None and self.num_operands == 3 else 0
|
|
1152
|
+
except (ValueError, TypeError) as e:
|
|
1153
|
+
raise ValueError(f"The value provided for beta {beta} is not convertible to dtype '{self.beta.dtype}'.") from e
|
|
1154
|
+
|
|
1155
|
+
# Set narrow-precision (FP8 and lower) quantization_scales.
|
|
1156
|
+
if self.input_type_width <= 8:
|
|
1157
|
+
self.quantization_scales = self._validate_operand_scales(quantization_scales, all_required=True)
|
|
1158
|
+
elif quantization_scales is not None:
|
|
1159
|
+
self.logger.warning(
|
|
1160
|
+
"Matmul: The provided scales are ignored, since they are only applicable to narrow-precision (FP8 and lower) "
|
|
1161
|
+
"operations."
|
|
1162
|
+
)
|
|
1163
|
+
|
|
1164
|
+
if self.options.result_amax and self.d_dtype_width > 8:
|
|
1165
|
+
raise ValueError("result_amax=True is allowed only for narrow-precision (FP8 and lower) results")
|
|
1166
|
+
|
|
1167
|
+
# Check operands alignment if needed
|
|
1168
|
+
if self.input_type_width <= 8:
|
|
1169
|
+
for operand, operand_name in zip(self.operands, "ABC", strict=False):
|
|
1170
|
+
if operand.data_ptr % 16 != 0:
|
|
1171
|
+
raise ValueError(
|
|
1172
|
+
f"For narrow-precision (FP8 and lower) multiplication, operand {operand_name} should be aligned to 16 "
|
|
1173
|
+
"bytes."
|
|
1174
|
+
)
|
|
1175
|
+
|
|
1176
|
+
# Capture physical operand extents and strides for consistency check when resetting
|
|
1177
|
+
# operands. In the case of FP4, these must be the packed (physical) values,
|
|
1178
|
+
# not the logical ones , because reset() receives packed FP4 tensors
|
|
1179
|
+
# and must compare against the same physical format.
|
|
1180
|
+
# Hence, these two do not need special handling for FP4.
|
|
1181
|
+
self.operand_extents = tuple(o.shape for o in self.operands)
|
|
1182
|
+
self.operand_strides = tuple(o.strides for o in self.operands)
|
|
1183
|
+
|
|
1184
|
+
# Create local operand layouts.
|
|
1185
|
+
a_layout = MatrixLayout(
|
|
1186
|
+
shape=self.operands[0].shape,
|
|
1187
|
+
strides=self.operands[0].strides,
|
|
1188
|
+
is_transpose=bool(self.qualifiers[0]["is_transpose"]),
|
|
1189
|
+
is_conjugate=bool(self.qualifiers[0]["is_conjugate"]),
|
|
1190
|
+
)
|
|
1191
|
+
b_layout = MatrixLayout(
|
|
1192
|
+
shape=self.operands[1].shape,
|
|
1193
|
+
strides=self.operands[1].strides,
|
|
1194
|
+
is_transpose=bool(self.qualifiers[1]["is_transpose"]),
|
|
1195
|
+
is_conjugate=bool(self.qualifiers[1]["is_conjugate"]),
|
|
1196
|
+
)
|
|
1197
|
+
c_layout = (
|
|
1198
|
+
MatrixLayout(shape=self.operands[2].shape, strides=self.operands[2].strides) if self.num_operands == 3 else None
|
|
1199
|
+
)
|
|
1200
|
+
|
|
1201
|
+
input_layout = ("T" if a_layout.is_transpose else "N") + ("T" if b_layout.is_transpose else "N")
|
|
1202
|
+
if self.input_type_width <= 8 and input_layout != "TN":
|
|
1203
|
+
raise ValueError(f"FP4/FP8 matrix multiplications support only TN input layout. Got {input_layout}")
|
|
1204
|
+
|
|
1205
|
+
# Get the operation traits.
|
|
1206
|
+
A_shape = problem_spec.shapes[0] # this is global
|
|
1207
|
+
B_shape = problem_spec.shapes[1] # this is global
|
|
1208
|
+
M0, K0 = (A_shape[0], A_shape[1]) if not a_layout.is_transpose else (A_shape[1], A_shape[0])
|
|
1209
|
+
K1, N0 = (B_shape[0], B_shape[1]) if not b_layout.is_transpose else (B_shape[1], B_shape[0])
|
|
1210
|
+
if K0 != K1:
|
|
1211
|
+
raise ValueError(
|
|
1212
|
+
f"The 'K' extent must match for the operands: K={K0} in operand A is not equal to K={K1} in operand B."
|
|
1213
|
+
)
|
|
1214
|
+
|
|
1215
|
+
if self.using_fp4_ab:
|
|
1216
|
+
# Adjust K, and A and B layouts for FP4.
|
|
1217
|
+
# C is never FP4, so no packed-to-logical conversion is needed.
|
|
1218
|
+
|
|
1219
|
+
K0 *= 2 # Unpack: K//2 -> K
|
|
1220
|
+
|
|
1221
|
+
# Note that narrow precision requires TN (checked above) and both
|
|
1222
|
+
# operands are column-major, so K is the first dimension.
|
|
1223
|
+
a_layout.shape = (a_layout.shape[0] * 2, a_layout.shape[1])
|
|
1224
|
+
a_layout.strides = (a_layout.strides[0], a_layout.strides[1] * 2)
|
|
1225
|
+
b_layout.shape = (b_layout.shape[0] * 2, b_layout.shape[1])
|
|
1226
|
+
b_layout.strides = (b_layout.strides[0], b_layout.strides[1] * 2)
|
|
1227
|
+
|
|
1228
|
+
self.logger.info(f"A: packed shape {self.operands[0].shape} -> logical shape {a_layout.shape}")
|
|
1229
|
+
self.logger.info(f"B: packed shape {self.operands[1].shape} -> logical shape {b_layout.shape}")
|
|
1230
|
+
else:
|
|
1231
|
+
self.logger.info(f"A: shape {self.operands[0].shape}, strides {self.operands[0].strides}")
|
|
1232
|
+
self.logger.info(f"B: shape {self.operands[1].shape}, strides {self.operands[1].strides}")
|
|
1233
|
+
|
|
1234
|
+
self.mm_traits = MMTraits(
|
|
1235
|
+
M=M0,
|
|
1236
|
+
N=N0,
|
|
1237
|
+
K=K0,
|
|
1238
|
+
a_layout=a_layout,
|
|
1239
|
+
b_layout=b_layout,
|
|
1240
|
+
c_layout=c_layout,
|
|
1241
|
+
d_layout=None, # this is determined in plan
|
|
1242
|
+
)
|
|
1243
|
+
self.result_layout: None | MatrixLayout = None # Wait till planning to determine this based on the epilog.
|
|
1244
|
+
self.logger.info(
|
|
1245
|
+
f"The matrix multiplication attributes are M={self.mm_traits.M}, N={self.mm_traits.N}, "
|
|
1246
|
+
f"K={self.mm_traits.K}, transA={a_layout.is_transpose} and transB={b_layout.is_transpose}."
|
|
1247
|
+
)
|
|
1248
|
+
|
|
1249
|
+
def use_alt_cache():
|
|
1250
|
+
if distributions[0].process_grid == distributions[1].process_grid == distributions[2].process_grid:
|
|
1251
|
+
# cuBLASMp uses SUMMA if all the process grids are equal, but there are
|
|
1252
|
+
# cases that are problematic with SUMMA, so avoid SUMMA for now for those
|
|
1253
|
+
# cases by using alternate cache for one of the process grids.
|
|
1254
|
+
for d in distributions:
|
|
1255
|
+
if isinstance(d, BlockNonCyclic) or d._is_1d_distribution():
|
|
1256
|
+
return True
|
|
1257
|
+
nprow, npcol = distributions[0].process_grid.shape
|
|
1258
|
+
if (distributions[2].block_sizes[0] == M0 // nprow) or (distributions[2].block_sizes[1] == N0 // npcol):
|
|
1259
|
+
return True
|
|
1260
|
+
return False
|
|
1261
|
+
|
|
1262
|
+
# Create process grids.
|
|
1263
|
+
alt_cache = use_alt_cache()
|
|
1264
|
+
self.lib_process_grids = []
|
|
1265
|
+
with utils.device_ctx(self.device_id):
|
|
1266
|
+
for i, d in enumerate(distributions):
|
|
1267
|
+
grid = d.process_grid
|
|
1268
|
+
assert grid.layout is not None
|
|
1269
|
+
from_alt_cache = alt_cache and i == 2
|
|
1270
|
+
lib_grid = _grid_cache.get_library_process_grid(grid, self.device_id, nccl_comm, from_alt=from_alt_cache)
|
|
1271
|
+
self.lib_process_grids.append(lib_grid)
|
|
1272
|
+
self.logger.info("Created cuBLASMp process grids")
|
|
1273
|
+
|
|
1274
|
+
# Set memory allocator.
|
|
1275
|
+
# cuBLASMp requires the workspace on NCCL symmetric heap.
|
|
1276
|
+
self.allocator = CublasMpMemoryManager(self.device_id, list(set(self.lib_process_grids)), self.logger)
|
|
1277
|
+
|
|
1278
|
+
# Create and set the operation descriptor.
|
|
1279
|
+
self.mm_desc = cublasMp.matmul_descriptor_create(self.compute_type)
|
|
1280
|
+
self.mm_desc_ifc = matmul_desc_ifc.MatmulDescInterface(self.mm_desc)
|
|
1281
|
+
self.mm_desc_ifc.transa = (
|
|
1282
|
+
cublas.Operation.C
|
|
1283
|
+
if (a_layout.is_conjugate and a_layout.is_transpose)
|
|
1284
|
+
else cublas.Operation.T
|
|
1285
|
+
if a_layout.is_transpose
|
|
1286
|
+
else cublas.Operation.N
|
|
1287
|
+
)
|
|
1288
|
+
self.mm_desc_ifc.transb = (
|
|
1289
|
+
cublas.Operation.C
|
|
1290
|
+
if (b_layout.is_conjugate and b_layout.is_transpose)
|
|
1291
|
+
else cublas.Operation.T
|
|
1292
|
+
if b_layout.is_transpose
|
|
1293
|
+
else cublas.Operation.N
|
|
1294
|
+
)
|
|
1295
|
+
if self.options.sm_count_communication:
|
|
1296
|
+
self.mm_desc_ifc.communication_sm_count = self.options.sm_count_communication
|
|
1297
|
+
if self.options.algo_type:
|
|
1298
|
+
self.mm_desc_ifc.algo_type = self.options.algo_type
|
|
1299
|
+
|
|
1300
|
+
if version == 800 and self.d_dtype_width <= 16 and self.compute_type.name.startswith("COMPUTE_32F"):
|
|
1301
|
+
# This is only needed for cuBLASMp 0.8 (versions >= 0.8.1 do this by default).
|
|
1302
|
+
self.mm_desc_ifc.communication_type = CudaDataType.CUDA_R_32F
|
|
1303
|
+
|
|
1304
|
+
# This ensures cuBLASMp always returns full (reduced) bias gradient
|
|
1305
|
+
# output. This is independent of whether the output is partitioned
|
|
1306
|
+
# or replicated.
|
|
1307
|
+
self.mm_desc_ifc.bias_result_scheme = cublasMp.ResultScheme.FULL
|
|
1308
|
+
|
|
1309
|
+
self.problem_spec = problem_spec
|
|
1310
|
+
|
|
1311
|
+
# Whether to register operand buffers (currently only B) with NCCL symmetric
|
|
1312
|
+
# memory or not. This is a hidden option and the environment variable is
|
|
1313
|
+
# expected to hold "0" or "1".
|
|
1314
|
+
self._register_buffers_option = bool(int(os.getenv("NVMATH_DIST_MATMUL_REGISTER_BUFFERS", default="1")))
|
|
1315
|
+
|
|
1316
|
+
# Register operand buffers with NCCL symmetric memory for performance.
|
|
1317
|
+
self._register_buffers()
|
|
1318
|
+
|
|
1319
|
+
# Planning preferences
|
|
1320
|
+
self.preferences = None
|
|
1321
|
+
|
|
1322
|
+
# Epilog attributes.
|
|
1323
|
+
self.epilog = None
|
|
1324
|
+
|
|
1325
|
+
# Epilog attributes: name-to-operand.
|
|
1326
|
+
self.epilog_operands: dict[str, typing.Any] = {}
|
|
1327
|
+
|
|
1328
|
+
# Epilog attributes: epilog input name-to-handler.
|
|
1329
|
+
self.epilog_input_name_to_handler: dict[str, typing.Any] = {}
|
|
1330
|
+
|
|
1331
|
+
# Epilog attributes: name-to-output tensor.
|
|
1332
|
+
self.epilog_outputs: dict[str, typing.Any] = {}
|
|
1333
|
+
|
|
1334
|
+
# Keep track of epilog input traits for resetting operands.
|
|
1335
|
+
self.epilog_inputs_traits: dict[str, typing.Any] = {}
|
|
1336
|
+
|
|
1337
|
+
# Keep track of epilog output handlers to allocate output in execute().
|
|
1338
|
+
self.epilog_output_handlers: list[EpilogOutputHandler] = []
|
|
1339
|
+
|
|
1340
|
+
# Non-epilog aux outputs. Currently, only used for quantization outputs (amax etc.)
|
|
1341
|
+
self.aux_outputs: dict[str, typing.Any] | None = None
|
|
1342
|
+
|
|
1343
|
+
# Plan attributes.
|
|
1344
|
+
self.matrix_descriptors: list[int] = []
|
|
1345
|
+
self.mm_planned = False
|
|
1346
|
+
|
|
1347
|
+
# Workspace attributes.
|
|
1348
|
+
self.workspace_device: None | memory.MemoryPointer = None
|
|
1349
|
+
self.workspace_size_device = 0
|
|
1350
|
+
self.workspace_host: None | np.ndarray = None
|
|
1351
|
+
self.workspace_size_host = 0
|
|
1352
|
+
self.workspace_allocated_size = 0
|
|
1353
|
+
self.workspace_allocated_here = False
|
|
1354
|
+
|
|
1355
|
+
# Attributes to establish stream ordering.
|
|
1356
|
+
self.workspace_stream = None
|
|
1357
|
+
self.last_compute_event = None
|
|
1358
|
+
|
|
1359
|
+
# Track whether the user has called release_operands().
|
|
1360
|
+
self._operands_released = False
|
|
1361
|
+
# Device-side array with the quantization_scales
|
|
1362
|
+
self.quantization_scales_device: dict[str, utils.TensorHolder] = {}
|
|
1363
|
+
|
|
1364
|
+
self.valid_state = True
|
|
1365
|
+
self.logger.info("The distributed Matmul operation has been created.")
|
|
1366
|
+
|
|
1367
|
+
def __enter__(self):
|
|
1368
|
+
return self
|
|
1369
|
+
|
|
1370
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
1371
|
+
self.free()
|
|
1372
|
+
|
|
1373
|
+
def _register_buffers(self):
|
|
1374
|
+
"""Register input buffers with NCCL symmetric memory for best performance."""
|
|
1375
|
+
# Currently only registration of B is relevant.
|
|
1376
|
+
self._register_operand_buffer_perhaps(operand_index=1)
|
|
1377
|
+
|
|
1378
|
+
def _deregister_buffers(self):
|
|
1379
|
+
if self._operands_released:
|
|
1380
|
+
return
|
|
1381
|
+
# Currently only registration of B is relevant.
|
|
1382
|
+
self._deregister_operand_buffer_perhaps(operand_index=1)
|
|
1383
|
+
|
|
1384
|
+
def _register_operand_buffer_perhaps(self, operand_index):
|
|
1385
|
+
"""Attempt to register the operand buffer with cuBLASMp (for NCCL symmetric
|
|
1386
|
+
memory). Note that cuBLASMp does nothing if the buffer is already registered."""
|
|
1387
|
+
# With CPU operands, the GPU execution buffers are internal and we can
|
|
1388
|
+
# always register them.
|
|
1389
|
+
if self._register_buffers_option is False and self.memory_space == "cuda":
|
|
1390
|
+
return
|
|
1391
|
+
|
|
1392
|
+
# Currently only registration of B is relevant.
|
|
1393
|
+
assert operand_index == 1, "Internal error."
|
|
1394
|
+
|
|
1395
|
+
# Buffer size must be the same on every process for symmetric buffer registration.
|
|
1396
|
+
# For CPU operands, the GPU execution buffers are managed internally and always
|
|
1397
|
+
# allocated with the same buffer size on every process (copy from CPU to GPU
|
|
1398
|
+
# with symmetric_memory="nccl" option guarantees this).
|
|
1399
|
+
if self.memory_space == "cuda" and self.problem_spec.b_min_size != self.problem_spec.b_max_size:
|
|
1400
|
+
self.logger.info(
|
|
1401
|
+
"Skipping registration of B buffer with NCCL symmetric memory because it is not the same size on every process."
|
|
1402
|
+
)
|
|
1403
|
+
return
|
|
1404
|
+
|
|
1405
|
+
grid_ptrs = set(self.lib_process_grids)
|
|
1406
|
+
_grid_cache.register_buffer_with_nccl(
|
|
1407
|
+
self.operands[operand_index],
|
|
1408
|
+
"B",
|
|
1409
|
+
grid_ptrs,
|
|
1410
|
+
self.logger,
|
|
1411
|
+
# For CPU operands, GPU mirrors are allocated with nccl4py so we expect
|
|
1412
|
+
# registration to succeed.
|
|
1413
|
+
ignore_errors=self.memory_space != "cpu",
|
|
1414
|
+
)
|
|
1415
|
+
|
|
1416
|
+
def _deregister_operand_buffer_perhaps(self, operand_index):
|
|
1417
|
+
"""Signal that this stateful object no longer requires this buffer to be
|
|
1418
|
+
registered with cuBLASMp (for NCCL symmetric memory). When the number of objects
|
|
1419
|
+
that have it registered becomes zero, then the buffer will actually be deregister
|
|
1420
|
+
with cuBLASMp."""
|
|
1421
|
+
if self._register_buffers_option is False and self.memory_space == "cuda":
|
|
1422
|
+
return
|
|
1423
|
+
|
|
1424
|
+
assert not self._operands_released, "Internal error."
|
|
1425
|
+
|
|
1426
|
+
# Currently only registration of B is relevant.
|
|
1427
|
+
assert operand_index == 1, "Internal error."
|
|
1428
|
+
|
|
1429
|
+
grid_ptrs = set(self.lib_process_grids)
|
|
1430
|
+
_grid_cache.deregister_buffer_with_nccl(self.operands[operand_index], grid_ptrs)
|
|
1431
|
+
|
|
1432
|
+
def _check_valid_matmul(self, *args, **kwargs):
|
|
1433
|
+
"""
|
|
1434
|
+
Check if the Matmul object is alive and well.
|
|
1435
|
+
"""
|
|
1436
|
+
if not self.valid_state:
|
|
1437
|
+
raise InvalidMatmulState("The Matmul object cannot be used after resources are free'd")
|
|
1438
|
+
|
|
1439
|
+
def _check_valid_operands(self, *args, **kwargs):
|
|
1440
|
+
"""
|
|
1441
|
+
Check if the operands are available for the operation.
|
|
1442
|
+
"""
|
|
1443
|
+
what = kwargs["what"]
|
|
1444
|
+
if self._operands_released:
|
|
1445
|
+
raise RuntimeError(
|
|
1446
|
+
f"{what} cannot be performed after the operands have been released. "
|
|
1447
|
+
f"Use reset_operands() to provide new operands before performing the {what.lower()}."
|
|
1448
|
+
)
|
|
1449
|
+
|
|
1450
|
+
def _free_plan_resources(self, exception: Exception | None = None) -> bool:
|
|
1451
|
+
"""
|
|
1452
|
+
Free resources allocated in planning.
|
|
1453
|
+
"""
|
|
1454
|
+
|
|
1455
|
+
# Destroy matrix descriptors.
|
|
1456
|
+
for descriptor in self.matrix_descriptors:
|
|
1457
|
+
if descriptor is not None:
|
|
1458
|
+
cublasMp.matrix_descriptor_destroy(descriptor)
|
|
1459
|
+
self.matrix_descriptors = []
|
|
1460
|
+
|
|
1461
|
+
self.mm_planned = False
|
|
1462
|
+
return True
|
|
1463
|
+
|
|
1464
|
+
def _check_planned(self, *args, **kwargs):
|
|
1465
|
+
what = kwargs["what"]
|
|
1466
|
+
if not self.mm_planned:
|
|
1467
|
+
raise RuntimeError(f"{what} cannot be performed before plan() has been called.")
|
|
1468
|
+
|
|
1469
|
+
def _free_workspace_memory(self, exception: Exception | None = None) -> bool:
|
|
1470
|
+
"""
|
|
1471
|
+
Free workspace by releasing the MemoryPointer object.
|
|
1472
|
+
"""
|
|
1473
|
+
if self.workspace_device is None:
|
|
1474
|
+
assert self.workspace_host is None, "Internal error."
|
|
1475
|
+
return True
|
|
1476
|
+
|
|
1477
|
+
with utils.device_ctx(self.device_id):
|
|
1478
|
+
if self.workspace_stream is not None:
|
|
1479
|
+
# The workspace is on NCCL symmetric memory (allocated by cuBLASMp
|
|
1480
|
+
# with ncclMemAlloc).
|
|
1481
|
+
# Wait for the computation to finish before calling ncclMemFree.
|
|
1482
|
+
self.workspace_stream.sync()
|
|
1483
|
+
self.workspace_device.free()
|
|
1484
|
+
self.workspace_device = self.workspace_host = None
|
|
1485
|
+
self.workspace_allocated_size = 0
|
|
1486
|
+
self.logger.debug("[_free_workspace_memory] The workspace has been released.")
|
|
1487
|
+
|
|
1488
|
+
return True
|
|
1489
|
+
|
|
1490
|
+
def _reset_workspace_allocation_tracking(self):
|
|
1491
|
+
"""
|
|
1492
|
+
Reset workspace allocation tracking attributes to False at the end of the methods
|
|
1493
|
+
where workspace memory is potentially allocated. This is necessary to prevent any
|
|
1494
|
+
exceptions raised before method entry from using stale tracking values.
|
|
1495
|
+
"""
|
|
1496
|
+
self.workspace_allocated_here = False
|
|
1497
|
+
|
|
1498
|
+
@utils.precondition(_check_valid_matmul)
|
|
1499
|
+
def _release_workspace_memory_perhaps(self, release_workspace):
|
|
1500
|
+
"""
|
|
1501
|
+
Free workspace memory if it's larger than the specified limit.
|
|
1502
|
+
"""
|
|
1503
|
+
if not release_workspace:
|
|
1504
|
+
return True
|
|
1505
|
+
|
|
1506
|
+
# Establish ordering wrt the computation and free workspace if requested.
|
|
1507
|
+
if self.last_compute_event is not None:
|
|
1508
|
+
self.workspace_stream.wait(self.last_compute_event)
|
|
1509
|
+
self.logger.debug("Established ordering with respect to the computation before releasing the workspace.")
|
|
1510
|
+
self.last_compute_event = None
|
|
1511
|
+
|
|
1512
|
+
self.logger.debug("[_release_workspace_memory_perhaps] The workspace memory will be released.")
|
|
1513
|
+
return self._free_workspace_memory()
|
|
1514
|
+
|
|
1515
|
+
def _release_workspace_memory_perhaps_wrapper(self, exception: Exception | None = None) -> bool:
|
|
1516
|
+
"""
|
|
1517
|
+
This is used in @atomic.
|
|
1518
|
+
"""
|
|
1519
|
+
if isinstance(exception, cublasMp.cuBLASMpError) and (
|
|
1520
|
+
"NOT_SUPPORTED" in str(exception) or "INVALID_VALUE" in str(exception)
|
|
1521
|
+
):
|
|
1522
|
+
addendum = (
|
|
1523
|
+
" It is also recommended to check the dtype support table at "
|
|
1524
|
+
"https://docs.nvidia.com/cuda/cublasmp/usage/functions.html#cublasmpmatmul"
|
|
1525
|
+
)
|
|
1526
|
+
# For cuBLASMpError we know that args attribute is (str,)
|
|
1527
|
+
exception.args = (exception.args[0] + addendum,)
|
|
1528
|
+
self._release_workspace_memory_perhaps(release_workspace=self.workspace_allocated_here)
|
|
1529
|
+
self._reset_workspace_allocation_tracking()
|
|
1530
|
+
return True
|
|
1531
|
+
|
|
1532
|
+
@utils.precondition(_check_valid_matmul)
|
|
1533
|
+
@utils.precondition(_check_planned, "Workspace memory allocation")
|
|
1534
|
+
@utils.atomic(_free_workspace_memory, method=True)
|
|
1535
|
+
def _allocate_workspace_memory(self, stream_holder: utils.StreamHolder):
|
|
1536
|
+
"""
|
|
1537
|
+
Allocate workspace memory using the specified allocator.
|
|
1538
|
+
"""
|
|
1539
|
+
|
|
1540
|
+
assert self.workspace_size_device is not None, "Internal Error."
|
|
1541
|
+
assert self.workspace_allocated_here is False, "Internal Error."
|
|
1542
|
+
|
|
1543
|
+
if self.workspace_size_device == 0: # For performance, bypass allocator for workspace size == 0.
|
|
1544
|
+
self.workspace_device = memory.MemoryPointer(0, 0, finalizer=None)
|
|
1545
|
+
else:
|
|
1546
|
+
self.logger.debug("Allocating device workspace for performing the matrix multiplication...")
|
|
1547
|
+
with utils.device_ctx(self.device_id), stream_holder.ctx:
|
|
1548
|
+
try:
|
|
1549
|
+
if isinstance(self.allocator, memory.BaseCUDAMemoryManagerAsync):
|
|
1550
|
+
self.workspace_device = self.allocator.memalloc_async(self.workspace_size_device, stream_holder.obj)
|
|
1551
|
+
else:
|
|
1552
|
+
self.workspace_device = self.allocator.memalloc(self.workspace_size_device)
|
|
1553
|
+
self.workspace_allocated_here = True
|
|
1554
|
+
except TypeError as e:
|
|
1555
|
+
message = (
|
|
1556
|
+
"The method 'memalloc' in the allocator object must conform to the interface in the "
|
|
1557
|
+
"'BaseCUDAMemoryManager' protocol."
|
|
1558
|
+
)
|
|
1559
|
+
raise TypeError(message) from e
|
|
1560
|
+
|
|
1561
|
+
if self.workspace_size_host > 0:
|
|
1562
|
+
self.logger.debug("Allocating host workspace for performing the matrix multiplication...")
|
|
1563
|
+
self.workspace_host = np.empty(self.workspace_size_host, dtype=np.int8)
|
|
1564
|
+
|
|
1565
|
+
self.workspace_allocated_size = self.workspace_size_device
|
|
1566
|
+
self.workspace_stream = stream_holder.obj
|
|
1567
|
+
self.logger.debug(
|
|
1568
|
+
f"Finished allocating device workspace of size {formatters.MemoryStr(self.workspace_size_device)} in the context "
|
|
1569
|
+
f"of stream {self.workspace_stream}."
|
|
1570
|
+
)
|
|
1571
|
+
self.logger.debug(f"Finished allocating host workspace of size {formatters.MemoryStr(self.workspace_size_host)}.")
|
|
1572
|
+
|
|
1573
|
+
def _allocate_workspace_memory_perhaps(self, stream_holder: utils.StreamHolder):
|
|
1574
|
+
"""
|
|
1575
|
+
Allocate workspace memory using the specified allocator, if it hasn't already been
|
|
1576
|
+
done.
|
|
1577
|
+
"""
|
|
1578
|
+
|
|
1579
|
+
if self.workspace_device is not None and self.workspace_allocated_size >= self.workspace_size_device:
|
|
1580
|
+
return
|
|
1581
|
+
|
|
1582
|
+
return self._allocate_workspace_memory(stream_holder)
|
|
1583
|
+
|
|
1584
|
+
@utils.precondition(_check_valid_matmul)
|
|
1585
|
+
def _infer_blocking_sizes(self, problem_spec, m, k, n, epilog_AR):
|
|
1586
|
+
# Infer block sizes for the case of BlockNonCyclic 1D distributions with uniform
|
|
1587
|
+
# partition sizes. Even though the block sizes were set individually for each
|
|
1588
|
+
# distribution when BlockNonCyclic._bind() was called, the block sizes might need
|
|
1589
|
+
# to be tweaked because they have to match across matrices A, B and C/D for m, n, k
|
|
1590
|
+
# and so must be inferred jointly.
|
|
1591
|
+
if not all(isinstance(d, BlockNonCyclic) and d._is_1d_distribution() for d in self.distributions):
|
|
1592
|
+
return
|
|
1593
|
+
|
|
1594
|
+
assert all(d._bound for d in self.distributions), "Internal error"
|
|
1595
|
+
|
|
1596
|
+
nranks = self.nranks
|
|
1597
|
+
|
|
1598
|
+
transA = self.mm_traits.a_layout.is_transpose
|
|
1599
|
+
transB = self.mm_traits.b_layout.is_transpose
|
|
1600
|
+
|
|
1601
|
+
# This function only infers for uniform partition sizes.
|
|
1602
|
+
if self.distributions[0]._is_row_wise():
|
|
1603
|
+
if transA and k % nranks != 0:
|
|
1604
|
+
return
|
|
1605
|
+
if not transA and m % nranks != 0:
|
|
1606
|
+
return
|
|
1607
|
+
else:
|
|
1608
|
+
if transA and m % nranks != 0:
|
|
1609
|
+
return
|
|
1610
|
+
if not transA and k % nranks != 0:
|
|
1611
|
+
return
|
|
1612
|
+
|
|
1613
|
+
if self.distributions[1]._is_row_wise():
|
|
1614
|
+
if transB and n % nranks != 0:
|
|
1615
|
+
return
|
|
1616
|
+
if not transB and k % nranks != 0:
|
|
1617
|
+
return
|
|
1618
|
+
else:
|
|
1619
|
+
if transB and k % nranks != 0:
|
|
1620
|
+
return
|
|
1621
|
+
if not transB and n % nranks != 0:
|
|
1622
|
+
return
|
|
1623
|
+
|
|
1624
|
+
A = self.operands[0]
|
|
1625
|
+
B = self.operands[1]
|
|
1626
|
+
mbA, nbA = A.shape # local
|
|
1627
|
+
mbB, nbB = B.shape # local
|
|
1628
|
+
|
|
1629
|
+
if self.using_fp4_ab:
|
|
1630
|
+
# Unpack the packed dimension to get the logical size.
|
|
1631
|
+
if transA:
|
|
1632
|
+
mbA *= 2
|
|
1633
|
+
else:
|
|
1634
|
+
nbA *= 2
|
|
1635
|
+
|
|
1636
|
+
if transB:
|
|
1637
|
+
nbB *= 2
|
|
1638
|
+
else:
|
|
1639
|
+
mbB *= 2
|
|
1640
|
+
|
|
1641
|
+
if epilog_AR:
|
|
1642
|
+
mbD, nbD = m, n
|
|
1643
|
+
else:
|
|
1644
|
+
mbD, nbD = (m // nranks, n) if self.distributions[2]._is_row_wise() else (m, n // nranks)
|
|
1645
|
+
|
|
1646
|
+
# Note that for a dimension of length L that isn't partitioned, L//N is also
|
|
1647
|
+
# a valid block size (a single block in that dimension is equivalent to N
|
|
1648
|
+
# contiguous blocks in that dimension).
|
|
1649
|
+
if not transA:
|
|
1650
|
+
# A is (m, k)
|
|
1651
|
+
mbA = mbD = min(mbA, mbD)
|
|
1652
|
+
if not transB:
|
|
1653
|
+
# B is (k, n)
|
|
1654
|
+
nbA = mbB = min(nbA, mbB)
|
|
1655
|
+
else:
|
|
1656
|
+
# B is (n, k)
|
|
1657
|
+
nbA = nbB = min(nbA, nbB)
|
|
1658
|
+
else:
|
|
1659
|
+
# A is (k, m)
|
|
1660
|
+
nbA = mbD = min(nbA, mbD)
|
|
1661
|
+
if not transB:
|
|
1662
|
+
# B is (k, n)
|
|
1663
|
+
mbA = mbB = min(mbA, mbB)
|
|
1664
|
+
else:
|
|
1665
|
+
# B is (n, k)
|
|
1666
|
+
mbA = nbB = min(mbA, nbB)
|
|
1667
|
+
|
|
1668
|
+
if not transB:
|
|
1669
|
+
# B is (k, n)
|
|
1670
|
+
nbB = nbD = min(nbB, nbD)
|
|
1671
|
+
else:
|
|
1672
|
+
# B is (n, k)
|
|
1673
|
+
mbB = nbD = min(mbB, nbD)
|
|
1674
|
+
|
|
1675
|
+
self.distributions[0]._block_sizes = (mbA, nbA)
|
|
1676
|
+
self.distributions[1]._block_sizes = (mbB, nbB)
|
|
1677
|
+
self.distributions[2]._block_sizes = (mbD, nbD)
|
|
1678
|
+
|
|
1679
|
+
@utils.precondition(_check_valid_matmul)
|
|
1680
|
+
def _infer_algo(self, m, k, n, epilog_AR: bool) -> int:
|
|
1681
|
+
"""Return distributed matrix multiplication algorithm that is expected to run.
|
|
1682
|
+
Currently only tries to infer the tensor parallelism comm-overlap algorithms.
|
|
1683
|
+
|
|
1684
|
+
0 if naive
|
|
1685
|
+
2 if GEMM+RS
|
|
1686
|
+
3 if AG+GEMM
|
|
1687
|
+
4 if GEMM+AR
|
|
1688
|
+
5 if local GEMM
|
|
1689
|
+
-1 otherwise (unknown, probably naive or SUMMA)
|
|
1690
|
+
"""
|
|
1691
|
+
nranks = self.nranks
|
|
1692
|
+
if nranks == 1:
|
|
1693
|
+
return 5
|
|
1694
|
+
|
|
1695
|
+
if any(not d._is_1d_distribution() for d in self.distributions):
|
|
1696
|
+
return -1 # unknown
|
|
1697
|
+
|
|
1698
|
+
is_transpose = [
|
|
1699
|
+
self.mm_traits.a_layout.is_transpose,
|
|
1700
|
+
self.mm_traits.b_layout.is_transpose,
|
|
1701
|
+
False,
|
|
1702
|
+
]
|
|
1703
|
+
|
|
1704
|
+
global_shape = [
|
|
1705
|
+
(m, k) if not is_transpose[0] else (k, m),
|
|
1706
|
+
(k, n) if not is_transpose[1] else (n, k),
|
|
1707
|
+
(m, n) if not epilog_AR else (m * nranks, n) if self.distributions[2]._is_row_wise() else (m, n * nranks),
|
|
1708
|
+
]
|
|
1709
|
+
|
|
1710
|
+
# Check that data divides evenly and distribution is non-cyclic
|
|
1711
|
+
for i in range(3):
|
|
1712
|
+
partitioned_dim = 0 if self.distributions[i]._is_row_wise() else 1
|
|
1713
|
+
|
|
1714
|
+
if global_shape[i][partitioned_dim] % nranks != 0:
|
|
1715
|
+
# Data has to divide evenly
|
|
1716
|
+
return -1
|
|
1717
|
+
|
|
1718
|
+
block_sizes = self.distributions[i].block_sizes
|
|
1719
|
+
if global_shape[i][partitioned_dim] // nranks != block_sizes[partitioned_dim]:
|
|
1720
|
+
# has to be non-cyclic
|
|
1721
|
+
return -1
|
|
1722
|
+
|
|
1723
|
+
transA = is_transpose[0]
|
|
1724
|
+
A_distribution = "R" if self.distributions[0]._is_row_wise() else "C"
|
|
1725
|
+
|
|
1726
|
+
transB = is_transpose[1]
|
|
1727
|
+
B_distribution = "R" if self.distributions[1]._is_row_wise() else "C"
|
|
1728
|
+
|
|
1729
|
+
C_distribution = "R" if self.distributions[2]._is_row_wise() else "C"
|
|
1730
|
+
|
|
1731
|
+
expected_algo = 0 # naive
|
|
1732
|
+
if epilog_AR:
|
|
1733
|
+
expected_algo = 4 # GEMM+AR
|
|
1734
|
+
elif (
|
|
1735
|
+
C_distribution == "R" and A_distribution == ("C" if transA else "R") and B_distribution == ("R" if transB else "C")
|
|
1736
|
+
):
|
|
1737
|
+
expected_algo = 3 # AG+GEMM
|
|
1738
|
+
elif (
|
|
1739
|
+
C_distribution == "C" and A_distribution == ("R" if transA else "C") and B_distribution == ("C" if transB else "R")
|
|
1740
|
+
):
|
|
1741
|
+
expected_algo = 2 # GEMM+RS
|
|
1742
|
+
|
|
1743
|
+
return expected_algo
|
|
1744
|
+
|
|
1745
|
+
def _check_local_gemm_sizes_fp4_fp8(self):
|
|
1746
|
+
"""Check that the local GEMM size is supported by cuBLASLt"""
|
|
1747
|
+
# The cuBLASMp algorithms considered here currently require uniform partitioning,
|
|
1748
|
+
# so the logic below assumes local GEMM size is the same on every rank.
|
|
1749
|
+
|
|
1750
|
+
# Get m, n, k for local GEMM
|
|
1751
|
+
if self._expected_algo == 3: # AG+GEMM
|
|
1752
|
+
# For local gemm:
|
|
1753
|
+
# - m is partitioned
|
|
1754
|
+
# - k and n are the global size (B is gathered)
|
|
1755
|
+
assert self.mm_traits.M % self.nranks == 0, "Internal error."
|
|
1756
|
+
m, n, k = self.mm_traits.M // self.nranks, self.mm_traits.N, self.mm_traits.K
|
|
1757
|
+
elif self._expected_algo in (2, 4): # GEMM+RS, GEMM+AR
|
|
1758
|
+
# A and B are partitioned only on k
|
|
1759
|
+
assert self.mm_traits.K % self.nranks == 0, "Internal error."
|
|
1760
|
+
m, n, k = self.mm_traits.M, self.mm_traits.N, self.mm_traits.K // self.nranks
|
|
1761
|
+
elif self._expected_algo == 5: # non-distributed GEMM
|
|
1762
|
+
m, n, k = self.mm_traits.M, self.mm_traits.N, self.mm_traits.K
|
|
1763
|
+
else:
|
|
1764
|
+
return
|
|
1765
|
+
|
|
1766
|
+
if self.input_type_width == 4:
|
|
1767
|
+
errors = []
|
|
1768
|
+
if m % 128 != 0:
|
|
1769
|
+
errors.append(f"M={m} must be divisible by 128")
|
|
1770
|
+
if n % 128 != 0:
|
|
1771
|
+
errors.append(f"N={n} must be divisible by 128")
|
|
1772
|
+
if k % 64 != 0:
|
|
1773
|
+
errors.append(f"K={k} must be divisible by 64")
|
|
1774
|
+
if errors:
|
|
1775
|
+
raise ValueError(
|
|
1776
|
+
f"FP4 block scaling dimension requirements not met: {', '.join(errors)}. "
|
|
1777
|
+
f"NVFP4 requires M and N divisible by 128, K divisible by 64."
|
|
1778
|
+
)
|
|
1779
|
+
elif self.input_type_width == 8:
|
|
1780
|
+
if self.options.block_scaling and (m % 128 != 0 or n % 128 != 0 or k % 128 != 0):
|
|
1781
|
+
raise ValueError("Matrix sizes for local GEMM must be divisible by 128 when block_scaling=True.")
|
|
1782
|
+
|
|
1783
|
+
if m % 16 != 0 or n % 16 != 0 or k % 16 != 0:
|
|
1784
|
+
raise ValueError("Matrix sizes for local GEMM must be divisible by 16 for FP8 operations")
|
|
1785
|
+
|
|
1786
|
+
def _validate_scalar_scale(self, operand: str):
|
|
1787
|
+
"""
|
|
1788
|
+
Validates a scalar scale.
|
|
1789
|
+
"""
|
|
1790
|
+
if self.options.block_scaling:
|
|
1791
|
+
raise ValueError(f"A scalar tensor-wide scale factor is not allowed for {operand.upper()} when block_scaling=True.")
|
|
1792
|
+
|
|
1793
|
+
def _validate_tensor_scale(self, scale, operand: str, operand_size=None):
|
|
1794
|
+
"""
|
|
1795
|
+
Validates a tensor scale.
|
|
1796
|
+
|
|
1797
|
+
Args:
|
|
1798
|
+
scale: The tensor scale to validate
|
|
1799
|
+
operand: The operand name (a, b, c, d)
|
|
1800
|
+
operand_size: Size of the operand (needed for block scaling shape validation)
|
|
1801
|
+
"""
|
|
1802
|
+
scale_package = utils.infer_object_package(scale)
|
|
1803
|
+
if scale_package != self.package:
|
|
1804
|
+
raise TypeError(
|
|
1805
|
+
f"The quantization scaling tensor for {operand.upper()} must belong to the same package as the operands."
|
|
1806
|
+
)
|
|
1807
|
+
|
|
1808
|
+
# Wrap temporarily since this is needed for validation
|
|
1809
|
+
scale_wrapped = tensor_wrapper.wrap_operand(scale)
|
|
1810
|
+
|
|
1811
|
+
# Device/memory space validation
|
|
1812
|
+
expected_device_id = "cpu" if self.memory_space == "cpu" else self.device_id
|
|
1813
|
+
if expected_device_id != scale_wrapped.device_id:
|
|
1814
|
+
raise ValueError(
|
|
1815
|
+
f'The scale for {operand.upper()} is on device "{scale_wrapped.device_id}", '
|
|
1816
|
+
f'but it should be on device "{expected_device_id}" to match the operands memory space.'
|
|
1817
|
+
)
|
|
1818
|
+
|
|
1819
|
+
# Shape and dtype validation for non-block-scaling
|
|
1820
|
+
if not self.options.block_scaling:
|
|
1821
|
+
if scale_wrapped.shape not in ((1,), ()):
|
|
1822
|
+
raise ValueError(
|
|
1823
|
+
f"The scale for {operand.upper()} must be of shape (1,) or (). Got {scale_wrapped.shape} instead."
|
|
1824
|
+
)
|
|
1825
|
+
if scale_wrapped.dtype != "float32":
|
|
1826
|
+
raise ValueError(f"The scale for {operand.upper()} must be float32 type. Got {scale_wrapped.dtype} instead.")
|
|
1827
|
+
|
|
1828
|
+
# Shape and dtype validation for block-scaling
|
|
1829
|
+
elif self.input_type_width == 8:
|
|
1830
|
+
# FP8: 32-element blocks with 128x128 tiled layout (VEC32_UE8M0)
|
|
1831
|
+
# Dtype validation (always possible)
|
|
1832
|
+
if scale_wrapped.dtype != "uint8":
|
|
1833
|
+
raise ValueError(f"Block scales for {operand.upper()} must be uint8 tensor.")
|
|
1834
|
+
|
|
1835
|
+
# Shape validation (only if operand_size is available)
|
|
1836
|
+
if operand_size is not None:
|
|
1837
|
+
expected_shape = (operand_size // 32,)
|
|
1838
|
+
if scale_wrapped.shape != expected_shape:
|
|
1839
|
+
raise ValueError(
|
|
1840
|
+
f"Scales for {operand.upper()} should have shape {expected_shape}. Got {scale_wrapped.shape}."
|
|
1841
|
+
)
|
|
1842
|
+
elif self.using_fp4_ab:
|
|
1843
|
+
# FP4: 16-element blocks with 128x64 tiled layout (VEC16_UE4M3)
|
|
1844
|
+
# Dtype validation (always possible)
|
|
1845
|
+
if scale_wrapped.dtype != "float8_e4m3fn":
|
|
1846
|
+
raise ValueError(
|
|
1847
|
+
f"Block scales for {operand.upper()} must be float8_e4m3fn tensor (UE4M3 format). "
|
|
1848
|
+
f"Got {scale_wrapped.dtype}."
|
|
1849
|
+
)
|
|
1850
|
+
|
|
1851
|
+
# Shape validation (only if operand_size is available)
|
|
1852
|
+
if operand_size is not None:
|
|
1853
|
+
expected_shape = (operand_size // 16,)
|
|
1854
|
+
if scale_wrapped.shape != expected_shape:
|
|
1855
|
+
raise ValueError(
|
|
1856
|
+
f"Scales for {operand.upper()} should have shape {expected_shape}. Got {scale_wrapped.shape}."
|
|
1857
|
+
)
|
|
1858
|
+
else:
|
|
1859
|
+
raise ValueError("block_scaling == True is not supported for non-FP8/FP4 types.")
|
|
1860
|
+
|
|
1861
|
+
def _validate_operand_scales(self, quantization_scales, all_required):
|
|
1862
|
+
"""
|
|
1863
|
+
Validates quantization scales, wrapping them into a MatmulQuantizationScales
|
|
1864
|
+
object if needed.
|
|
1865
|
+
|
|
1866
|
+
Args:
|
|
1867
|
+
quantization_scales: The quantization scales to validate.
|
|
1868
|
+
all_required: Whether all scales are required.
|
|
1869
|
+
|
|
1870
|
+
Returns:
|
|
1871
|
+
A MatmulQuantizationScales object with the validated quantization scales.
|
|
1872
|
+
"""
|
|
1873
|
+
if quantization_scales is None:
|
|
1874
|
+
raise ValueError(
|
|
1875
|
+
"Scales are required for narrow-precision (FP8 and lower) operations. "
|
|
1876
|
+
"Please set `quantization_scales` argument."
|
|
1877
|
+
)
|
|
1878
|
+
|
|
1879
|
+
# wrap the quantization scales into a MatmulQuantizationScales object if needed
|
|
1880
|
+
# otherwise, return the quantization scales as is
|
|
1881
|
+
quantization_scales = utils.check_or_create_options(
|
|
1882
|
+
_configuration.MatmulQuantizationScales, quantization_scales, "Scale factors"
|
|
1883
|
+
)
|
|
1884
|
+
|
|
1885
|
+
# Validate which scales are required/allowed
|
|
1886
|
+
expected_scales = "AB"
|
|
1887
|
+
if self.d_dtype_width <= 8 and not self.options.block_scaling:
|
|
1888
|
+
expected_scales += "D"
|
|
1889
|
+
elif quantization_scales.d is not None:
|
|
1890
|
+
if self.options.block_scaling:
|
|
1891
|
+
raise ValueError("Quantization scaling is not supported for D when `block_scaling` option is enabled.")
|
|
1892
|
+
if self.d_dtype_width > 8:
|
|
1893
|
+
raise ValueError(
|
|
1894
|
+
"Quantization scaling is not supported for D when it is not a narrow-precision (FP8 and lower) type."
|
|
1895
|
+
)
|
|
1896
|
+
|
|
1897
|
+
if self.num_operands == 3 and self.c_dtype_width <= 8:
|
|
1898
|
+
expected_scales += "C"
|
|
1899
|
+
elif quantization_scales.c is not None:
|
|
1900
|
+
raise ValueError(
|
|
1901
|
+
"Quantization scaling is not supported for C when it is not a narrow-precision (FP8 and lower) type."
|
|
1902
|
+
)
|
|
1903
|
+
|
|
1904
|
+
if all_required:
|
|
1905
|
+
for operand in expected_scales:
|
|
1906
|
+
if getattr(quantization_scales, operand.lower()) is None:
|
|
1907
|
+
raise ValueError(f"Scale for {operand.upper()} is not specified")
|
|
1908
|
+
|
|
1909
|
+
# Validate each scale by delegating to scalar/tensor specific validators
|
|
1910
|
+
for operand in "abcd":
|
|
1911
|
+
scale = getattr(quantization_scales, operand)
|
|
1912
|
+
if scale is None:
|
|
1913
|
+
continue
|
|
1914
|
+
|
|
1915
|
+
if isinstance(scale, (int, float)):
|
|
1916
|
+
self._validate_scalar_scale(operand)
|
|
1917
|
+
else:
|
|
1918
|
+
# For block scaling, pass operand size for shape validation
|
|
1919
|
+
if self.options.block_scaling and operand in ("a", "b"):
|
|
1920
|
+
operand_idx = 0 if operand == "a" else 1
|
|
1921
|
+
operand_size = self.operands[operand_idx].size # type: ignore[union-attr,index]
|
|
1922
|
+
|
|
1923
|
+
# For FP4, data is always packed: double the size to get logical size
|
|
1924
|
+
if self.using_fp4_ab:
|
|
1925
|
+
operand_size *= 2 # Packed tensor has half the elements
|
|
1926
|
+
self.logger.debug(f"FP4 scale validation for {operand}: adjusted operand_size={operand_size}")
|
|
1927
|
+
else:
|
|
1928
|
+
operand_size = None
|
|
1929
|
+
self._validate_tensor_scale(scale, operand, operand_size)
|
|
1930
|
+
|
|
1931
|
+
return quantization_scales
|
|
1932
|
+
|
|
1933
|
+
def _validate_epilog_aux_scale(self, aux_quantization_scale, *, required):
|
|
1934
|
+
is_narrow_aux = (
|
|
1935
|
+
self.preferences.epilog.aux_type is not None
|
|
1936
|
+
and typemaps.NAME_TO_DATA_WIDTH[typemaps.DATA_TYPE_TO_NAME[self.preferences.epilog.aux_type]] <= 8
|
|
1937
|
+
)
|
|
1938
|
+
if aux_quantization_scale is not None and not is_narrow_aux:
|
|
1939
|
+
raise ValueError(
|
|
1940
|
+
"Scales for epilog auxiliary output are not supported when `preferences.epilog.aux_type` is not set to a "
|
|
1941
|
+
"narrow-precision type."
|
|
1942
|
+
)
|
|
1943
|
+
elif aux_quantization_scale is None and is_narrow_aux and required:
|
|
1944
|
+
raise ValueError(
|
|
1945
|
+
'"aux_quantization_scale" epilog input is required when `preferences.epilog.aux_type` is set to a '
|
|
1946
|
+
"narrow-precision type."
|
|
1947
|
+
)
|
|
1948
|
+
|
|
1949
|
+
# Validate scalar vs tensor scale (same as for operand scales)
|
|
1950
|
+
if aux_quantization_scale is not None:
|
|
1951
|
+
if isinstance(aux_quantization_scale, (int, float)):
|
|
1952
|
+
self._validate_scalar_scale("epilog_aux")
|
|
1953
|
+
else:
|
|
1954
|
+
# No operand_size for epilog_aux scales
|
|
1955
|
+
self._validate_tensor_scale(aux_quantization_scale, "epilog_aux", operand_size=None)
|
|
1956
|
+
|
|
1957
|
+
def _prepare_validated_scalar_scale(self, scale: int | float, operand: str, stream_holder: utils.StreamHolder):
|
|
1958
|
+
"""
|
|
1959
|
+
Converts validated scalar to float32 tensor and copies to GPU.
|
|
1960
|
+
Assumes validation already done in _validate_scalar_scale.
|
|
1961
|
+
"""
|
|
1962
|
+
# If it's a scalar, copy to GPU. Float32 is the only type allowed by
|
|
1963
|
+
# cublasLtMatmulScale_t for tensor-wide scaling.
|
|
1964
|
+
self.logger.debug(f"Scale for {operand.upper()} will be copied to device {self.device_id}.")
|
|
1965
|
+
scale_op = tensor_wrapper.wrap_operand(np.asarray([scale], dtype="float32"))
|
|
1966
|
+
self.quantization_scales_device[operand] = scale_op.to(self.device_id, stream_holder)
|
|
1967
|
+
|
|
1968
|
+
def _prepare_validated_tensor_scale(self, scale, operand: str, stream_holder: utils.StreamHolder):
|
|
1969
|
+
"""
|
|
1970
|
+
Wraps validated tensor and copies to GPU.
|
|
1971
|
+
Assumes all validation already done in _validate_tensor_scale (called in __init__).
|
|
1972
|
+
This is pure preparation - no validation here.
|
|
1973
|
+
|
|
1974
|
+
Note: We wrap the tensor a second time here (first wrap was for validation).
|
|
1975
|
+
This is acceptable because wrapping is cheap and we get early error detection.
|
|
1976
|
+
"""
|
|
1977
|
+
# Wrap the scale (second time - first was for validation in __init__)
|
|
1978
|
+
self.quantization_scales_device[operand] = tensor_wrapper.wrap_operand(scale)
|
|
1979
|
+
|
|
1980
|
+
# Copy to GPU if on CPU (no validation, just preparation)
|
|
1981
|
+
if self.quantization_scales_device[operand].device in (None, "cpu"):
|
|
1982
|
+
self.logger.debug(f"Scale for {operand.upper()} will be copied to device {self.device_id}.")
|
|
1983
|
+
self.quantization_scales_device[operand] = self.quantization_scales_device[operand].to(
|
|
1984
|
+
self.device_id, stream_holder
|
|
1985
|
+
)
|
|
1986
|
+
|
|
1987
|
+
def _prepare_single_validated_scale(self, scale, operand: str, cublas_operand: str, stream_holder: utils.StreamHolder):
|
|
1988
|
+
"""
|
|
1989
|
+
Prepares a single validated scale and sets its pointer/mode in mm_desc_ifc.
|
|
1990
|
+
Used for both operand scales (a,b,c,d) and epilog scales (epilog_aux).
|
|
1991
|
+
Assumes validation already done.
|
|
1992
|
+
"""
|
|
1993
|
+
if scale is None:
|
|
1994
|
+
return
|
|
1995
|
+
|
|
1996
|
+
# Delegate to specific preparer (validation already done)
|
|
1997
|
+
if isinstance(scale, (int, float)):
|
|
1998
|
+
self._prepare_validated_scalar_scale(scale, operand, stream_holder)
|
|
1999
|
+
else:
|
|
2000
|
+
self._prepare_validated_tensor_scale(scale, operand, stream_holder)
|
|
2001
|
+
|
|
2002
|
+
# Set pointer and mode in descriptor
|
|
2003
|
+
setattr(self.mm_desc_ifc, f"{cublas_operand}_scale_pointer", self.quantization_scales_device[operand].data_ptr)
|
|
2004
|
+
|
|
2005
|
+
if self.options.block_scaling:
|
|
2006
|
+
# MXFP8 uses 32-element blocks with UE8M0, FP4 uses 16-element blocks with UE4M3
|
|
2007
|
+
if self.using_fp4_ab:
|
|
2008
|
+
# FP4: 16-element block scaling with UE4M3 format
|
|
2009
|
+
self.logger.debug(f"Using VEC16_UE4M3 scale mode for operand {operand.upper()}.")
|
|
2010
|
+
setattr(self.mm_desc_ifc, f"{cublas_operand}_scale_mode", cublasMp.MatmulMatrixScale.VEC16_UE4M3)
|
|
2011
|
+
else:
|
|
2012
|
+
# FP8: 32-element block scaling with UE8M0 format
|
|
2013
|
+
self.logger.debug(f"Using VEC32_UE8M0 scale mode for operand {operand.upper()}.")
|
|
2014
|
+
setattr(self.mm_desc_ifc, f"{cublas_operand}_scale_mode", cublasMp.MatmulMatrixScale.VEC32_UE8M0)
|
|
2015
|
+
else:
|
|
2016
|
+
self.logger.debug(f"Using SCALAR_32F scale mode for operand {operand.upper()}.")
|
|
2017
|
+
setattr(self.mm_desc_ifc, f"{cublas_operand}_scale_mode", cublasMp.MatmulMatrixScale.SCALAR_FP32)
|
|
2018
|
+
|
|
2019
|
+
def _prepare_operand_quantization_scales(self, scales, stream_holder: utils.StreamHolder):
|
|
2020
|
+
"""
|
|
2021
|
+
Prepares validated operand scales (a,b,c,d).
|
|
2022
|
+
Assumes scales are validated and wrapped into a MatmulQuantizationScales object.
|
|
2023
|
+
"""
|
|
2024
|
+
for operand in "abcd":
|
|
2025
|
+
scale = getattr(scales, operand)
|
|
2026
|
+
self._prepare_single_validated_scale(scale, operand, cublas_operand=operand, stream_holder=stream_holder)
|
|
2027
|
+
|
|
2028
|
+
@utils.precondition(_check_valid_matmul)
|
|
2029
|
+
@utils.atomic(_free_plan_resources, method=True)
|
|
2030
|
+
def plan(
|
|
2031
|
+
self, *, preferences=None, epilog=None, epilog_inputs=None, stream: utils.AnyStream | int | None = None
|
|
2032
|
+
): # Epilog inputs require as many inputs (with specific shapes etc) as required by the epilogue. It's a dict.
|
|
2033
|
+
"""
|
|
2034
|
+
Plan the matrix multiplication operation, considering the epilog (if provided).
|
|
2035
|
+
|
|
2036
|
+
Args:
|
|
2037
|
+
preferences: {preferences}
|
|
2038
|
+
|
|
2039
|
+
epilog: {epilog}
|
|
2040
|
+
|
|
2041
|
+
epilog_inputs: {epilog_inputs}
|
|
2042
|
+
|
|
2043
|
+
stream: {stream}
|
|
2044
|
+
|
|
2045
|
+
Notes:
|
|
2046
|
+
Epilogs that have ``BIAS`` in their name need an epilog input with the key
|
|
2047
|
+
``'bias'``. Epilogs that have ``DRELU`` need an epilog input with the key
|
|
2048
|
+
``'relu_aux'``, which is produced in a "forward pass" epilog like ``RELU_AUX``
|
|
2049
|
+
or ``RELU_AUX_BIAS``. Similarly, epilogs with ``DGELU`` in their name require an
|
|
2050
|
+
epilog input with the key ``'gelu_aux'``, produced in the corresponding forward
|
|
2051
|
+
pass operation.
|
|
2052
|
+
|
|
2053
|
+
See :class:`Matmul` for an example, and further examples can be found in the
|
|
2054
|
+
`nvmath/examples/distributed/linalg/advanced/matmul
|
|
2055
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/linalg/advanced/matmul>`_
|
|
2056
|
+
directory.
|
|
2057
|
+
"""
|
|
2058
|
+
self.logger.info("= PLANNING PHASE =")
|
|
2059
|
+
|
|
2060
|
+
# Clear epilog operands, since different epilogs can be provided in different calls.
|
|
2061
|
+
# We don't need to worry about ordering, since it's the user's responsibility to
|
|
2062
|
+
# order calls that accept a stream argument. This applies to CPU operands as well,
|
|
2063
|
+
# even though we move them to the GPU, since the execution is blocking.
|
|
2064
|
+
self.epilog_operands = {} # Clear operands in case of repeated planning.
|
|
2065
|
+
self.epilog_input_name_to_handler = {} # Clear input name to handler map as well,
|
|
2066
|
+
self.epilog_inputs_traits = {} # ... and the input traits as well.
|
|
2067
|
+
|
|
2068
|
+
preferences = utils.check_or_create_options(
|
|
2069
|
+
_configuration.MatmulPlanPreferences, preferences, "Distributed matrix multiplication plan preferences"
|
|
2070
|
+
)
|
|
2071
|
+
self.preferences = preferences
|
|
2072
|
+
|
|
2073
|
+
if self._operands_released:
|
|
2074
|
+
raise RuntimeError("The Matmul has no operands. Please call reset_operands")
|
|
2075
|
+
|
|
2076
|
+
mm_traits = self.mm_traits
|
|
2077
|
+
|
|
2078
|
+
stream_holder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
|
|
2079
|
+
self.logger.info(f"The specified stream for the matrix multiplication plan is {stream_holder.obj}.")
|
|
2080
|
+
|
|
2081
|
+
if epilog is None and epilog_inputs is not None:
|
|
2082
|
+
self.logger.warning(
|
|
2083
|
+
f"Matmul: The provided epilog inputs {epilog_inputs.keys()} are ignored since an epilog is not specified."
|
|
2084
|
+
)
|
|
2085
|
+
|
|
2086
|
+
m, n, k = mm_traits.M, mm_traits.N, mm_traits.K
|
|
2087
|
+
|
|
2088
|
+
if self.num_operands == 3:
|
|
2089
|
+
if epilog == MatmulEpilog.ALLREDUCE:
|
|
2090
|
+
allreduce_c_global_shape = tuple(
|
|
2091
|
+
x * y for x, y in zip(self.distributions[2].process_grid.shape, (m, n), strict=False)
|
|
2092
|
+
)
|
|
2093
|
+
if tuple(self.problem_spec.shapes[2]) != allreduce_c_global_shape:
|
|
2094
|
+
raise ValueError(
|
|
2095
|
+
f"The global shape of C according to its distribution ({self.problem_spec.shapes[2]}) is "
|
|
2096
|
+
f"not the expected one when using AllReduce epilogue ({allreduce_c_global_shape})"
|
|
2097
|
+
)
|
|
2098
|
+
if self.operands[2].shape != (m, n):
|
|
2099
|
+
raise ValueError(f"The shape of C on every process when using AllReduce epilogue must be (m, n)={(m, n)}")
|
|
2100
|
+
elif tuple(self.problem_spec.shapes[2]) != (m, n):
|
|
2101
|
+
raise ValueError(
|
|
2102
|
+
f"The global shape of C according to its distribution ({self.problem_spec.shapes[2]}) is "
|
|
2103
|
+
f"not the expected shape ({(m, n)})"
|
|
2104
|
+
)
|
|
2105
|
+
|
|
2106
|
+
transA = self.mm_traits.a_layout.is_transpose
|
|
2107
|
+
transB = self.mm_traits.b_layout.is_transpose
|
|
2108
|
+
|
|
2109
|
+
operand_names = "abc"
|
|
2110
|
+
if not self.distributions[0]._bound:
|
|
2111
|
+
for i, d in enumerate(self.distributions):
|
|
2112
|
+
assert not d._bound, "Internal error"
|
|
2113
|
+
if i < self.num_operands:
|
|
2114
|
+
if i == 0:
|
|
2115
|
+
global_shape = (m, k) if not transA else (k, m)
|
|
2116
|
+
elif i == 1:
|
|
2117
|
+
global_shape = (k, n) if not transB else (n, k)
|
|
2118
|
+
else:
|
|
2119
|
+
global_shape = (m, n) if epilog != MatmulEpilog.ALLREDUCE else allreduce_c_global_shape # type: ignore
|
|
2120
|
+
shape = getattr(self.mm_traits, f"{operand_names[i]}_layout").shape
|
|
2121
|
+
else:
|
|
2122
|
+
if epilog == MatmulEpilog.ALLREDUCE:
|
|
2123
|
+
global_shape = tuple(
|
|
2124
|
+
x * y
|
|
2125
|
+
for x, y in zip(self.distributions[2].process_grid.shape, (m, n), strict=False) # type: ignore
|
|
2126
|
+
)
|
|
2127
|
+
else:
|
|
2128
|
+
global_shape = (m, n)
|
|
2129
|
+
shape = None
|
|
2130
|
+
d._bind(global_shape, shape=shape)
|
|
2131
|
+
self._infer_blocking_sizes(self.problem_spec, m, k, n, epilog == MatmulEpilog.ALLREDUCE)
|
|
2132
|
+
|
|
2133
|
+
# Check block size on m dimension.
|
|
2134
|
+
m_block_size_A = self.distributions[0].block_sizes[1] if transA else self.distributions[0].block_sizes[0]
|
|
2135
|
+
m_block_size_D = self.distributions[2].block_sizes[0]
|
|
2136
|
+
if m_block_size_A != m_block_size_D:
|
|
2137
|
+
raise ValueError("Block size of m dimension must be the same for A and C/D")
|
|
2138
|
+
|
|
2139
|
+
# Check block size on n dimension.
|
|
2140
|
+
n_block_size_B = self.distributions[1].block_sizes[0] if transB else self.distributions[1].block_sizes[1]
|
|
2141
|
+
n_block_size_D = self.distributions[2].block_sizes[1]
|
|
2142
|
+
if n_block_size_B != n_block_size_D:
|
|
2143
|
+
raise ValueError("Block size of n dimension must be the same for B and C/D")
|
|
2144
|
+
|
|
2145
|
+
# Check block size on k dimension.
|
|
2146
|
+
k_block_size_A = self.distributions[0].block_sizes[0] if transA else self.distributions[0].block_sizes[1]
|
|
2147
|
+
k_block_size_B = self.distributions[1].block_sizes[1] if transB else self.distributions[1].block_sizes[0]
|
|
2148
|
+
if k_block_size_A != k_block_size_B:
|
|
2149
|
+
raise ValueError("Block size of k dimension must be the same for A and B")
|
|
2150
|
+
|
|
2151
|
+
self._expected_algo = self._infer_algo(m, k, n, epilog == MatmulEpilog.ALLREDUCE)
|
|
2152
|
+
|
|
2153
|
+
# Fill the result traits, now that we know the epilog.
|
|
2154
|
+
result_shape = self.distributions[2]._data_shape
|
|
2155
|
+
self.result_layout = MatrixLayout(
|
|
2156
|
+
shape=result_shape,
|
|
2157
|
+
strides=calculate_strides(result_shape, (0, 1)) if not self.inplace else self.operands[2].strides,
|
|
2158
|
+
)
|
|
2159
|
+
mm_traits.d_layout = self.result_layout
|
|
2160
|
+
|
|
2161
|
+
self.epilog = epilog
|
|
2162
|
+
if epilog is not None:
|
|
2163
|
+
assert epilog in EPILOG_INPUT_HANDLERS_MAP, "Not supported."
|
|
2164
|
+
self.logger.info(f"The specified epilog is {epilog.name}.")
|
|
2165
|
+
|
|
2166
|
+
epilog_minimum_versions = EPILOG_MINIMUM_VERSIONS_MAP[epilog]
|
|
2167
|
+
version = cublasMp.get_version()
|
|
2168
|
+
if version < epilog_minimum_versions["cublasMp"]:
|
|
2169
|
+
message = (
|
|
2170
|
+
f"The epilog {epilog.name} requires cublasMp >= {epilog_minimum_versions['cublasMp']}; "
|
|
2171
|
+
f"you have version {version}."
|
|
2172
|
+
)
|
|
2173
|
+
raise ValueError(message)
|
|
2174
|
+
|
|
2175
|
+
# Take a copy of the user-provided inputs.
|
|
2176
|
+
if epilog_inputs is not None:
|
|
2177
|
+
epilog_inputs = epilog_inputs.copy()
|
|
2178
|
+
else:
|
|
2179
|
+
epilog_inputs = {}
|
|
2180
|
+
|
|
2181
|
+
# Get the dtype of auxiliary buffer
|
|
2182
|
+
aux_dtype_name = (
|
|
2183
|
+
typemaps.DATA_TYPE_TO_NAME[self.preferences.epilog.aux_type] # type: ignore[attr-defined]
|
|
2184
|
+
if self.preferences.epilog.aux_type is not None # type: ignore[attr-defined]
|
|
2185
|
+
else None
|
|
2186
|
+
)
|
|
2187
|
+
|
|
2188
|
+
# Extract aux quantization scale from the inputs.
|
|
2189
|
+
aux_quantization_scale = (
|
|
2190
|
+
epilog_inputs.pop("aux_quantization_scale") if "aux_quantization_scale" in epilog_inputs else None
|
|
2191
|
+
)
|
|
2192
|
+
self._validate_epilog_aux_scale(aux_quantization_scale, required=True)
|
|
2193
|
+
self._prepare_single_validated_scale(
|
|
2194
|
+
aux_quantization_scale, "epilog_aux", cublas_operand="epilogue_aux", stream_holder=stream_holder
|
|
2195
|
+
)
|
|
2196
|
+
|
|
2197
|
+
epilog_input_handler_types = EPILOG_INPUT_HANDLERS_MAP[epilog]
|
|
2198
|
+
if epilog_input_handler_types:
|
|
2199
|
+
epilog_input_handlers = [
|
|
2200
|
+
handler_type(self.logger, mm_traits, epilog, self.c_dtype_name, self.d_dtype_name, aux_dtype_name)
|
|
2201
|
+
for handler_type in epilog_input_handler_types
|
|
2202
|
+
]
|
|
2203
|
+
|
|
2204
|
+
required_epilog_input_names = {h.name for h in epilog_input_handlers}
|
|
2205
|
+
|
|
2206
|
+
self.logger.info(f"The epilog requires the following additional inputs: {required_epilog_input_names}.")
|
|
2207
|
+
if required_epilog_input_names != set(epilog_inputs.keys()):
|
|
2208
|
+
raise ValueError(
|
|
2209
|
+
f"The epilog {epilog.name} requires the following input tensors: "
|
|
2210
|
+
f"{required_epilog_input_names}. The provided tensor names are: {epilog_inputs.keys()}"
|
|
2211
|
+
)
|
|
2212
|
+
|
|
2213
|
+
# Wrap epilog inputs.
|
|
2214
|
+
for name in epilog_inputs:
|
|
2215
|
+
epilog_inputs[name] = nvmath.internal.tensor_wrapper.wrap_operand(epilog_inputs[name])
|
|
2216
|
+
|
|
2217
|
+
# Check if epilog inputs all belong to the same package, which is the same
|
|
2218
|
+
# as the package of the MM operands.
|
|
2219
|
+
epilog_package = utils.get_operands_package(list(epilog_inputs.values()))
|
|
2220
|
+
if self.package != epilog_package:
|
|
2221
|
+
message = f"Library package mismatch for epilog: '{self.package}' => '{epilog_package}'"
|
|
2222
|
+
raise TypeError(message)
|
|
2223
|
+
|
|
2224
|
+
# Check if all epilog inputs all are on the same device, which is the device
|
|
2225
|
+
# of the operands.
|
|
2226
|
+
device_id = utils.get_operands_device_id(list(epilog_inputs.values()))
|
|
2227
|
+
if device_id != "cpu" and self.device_id != device_id:
|
|
2228
|
+
raise ValueError(
|
|
2229
|
+
f"The epilog inputs must be on the same device ({device_id}) as the operands ({self.device_id})."
|
|
2230
|
+
)
|
|
2231
|
+
|
|
2232
|
+
# Move epilog inputs to the GPU, if needed.
|
|
2233
|
+
if device_id == "cpu":
|
|
2234
|
+
for e in required_epilog_input_names:
|
|
2235
|
+
self.logger.debug(f"The epilog input {e} will be copied to device{self.device_id}.")
|
|
2236
|
+
self.epilog_operands[e] = epilog_inputs[e].to(self.device_id, stream_holder)
|
|
2237
|
+
else:
|
|
2238
|
+
for e in required_epilog_input_names:
|
|
2239
|
+
self.epilog_operands[e] = epilog_inputs[e]
|
|
2240
|
+
|
|
2241
|
+
# First validate all epilog inputs. Use the GPU tensors in case metadata has
|
|
2242
|
+
# changed.
|
|
2243
|
+
for handler in epilog_input_handlers:
|
|
2244
|
+
handler.validate(epilog_inputs[handler.name])
|
|
2245
|
+
|
|
2246
|
+
# Finally, update the MM descriptor. Note that we pass in
|
|
2247
|
+
# self.epilog_operands (which are on the GPU).
|
|
2248
|
+
for handler in epilog_input_handlers:
|
|
2249
|
+
handler.update(self.mm_desc_ifc, self.epilog_operands[handler.name])
|
|
2250
|
+
self.epilog_input_name_to_handler[handler.name] = handler
|
|
2251
|
+
|
|
2252
|
+
# Capture the epilog operands traits for consistency checks when resetting
|
|
2253
|
+
# operands.
|
|
2254
|
+
self.epilog_inputs_traits = {
|
|
2255
|
+
name: EpilogInputTraits(
|
|
2256
|
+
dtype=self.epilog_operands[name].dtype,
|
|
2257
|
+
extents=self.epilog_operands[name].shape,
|
|
2258
|
+
strides=self.epilog_operands[name].strides,
|
|
2259
|
+
)
|
|
2260
|
+
for name in self.epilog_operands
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
epilog_output_handler_types = EPILOG_OUTPUT_HANDLERS_MAP[epilog]
|
|
2264
|
+
if epilog_output_handler_types:
|
|
2265
|
+
self.epilog_output_handlers = epilog_output_handlers = [
|
|
2266
|
+
handler_type(self.logger, mm_traits, epilog, self.c_dtype_name, self.d_dtype_name, aux_dtype_name)
|
|
2267
|
+
for handler_type in epilog_output_handler_types
|
|
2268
|
+
]
|
|
2269
|
+
|
|
2270
|
+
# Update the MM descriptor, except for the device pointer.
|
|
2271
|
+
for ohandler in epilog_output_handlers:
|
|
2272
|
+
ohandler.update(self.mm_desc_ifc)
|
|
2273
|
+
|
|
2274
|
+
# Set the epilog. At this point, we're sure that the epilog inputs, if any, are
|
|
2275
|
+
# valid and have been set.
|
|
2276
|
+
self.mm_desc_ifc.epilogue = epilog
|
|
2277
|
+
|
|
2278
|
+
# Create descriptors for matrices A, B, C and D.
|
|
2279
|
+
matrix_dtypes = (self.a_dtype, self.b_dtype, self.c_dtype, self.d_dtype)
|
|
2280
|
+
for i in range(4):
|
|
2281
|
+
distribution = self.distributions[min(i, 2)] # distribution for C/D is the same
|
|
2282
|
+
lld = (
|
|
2283
|
+
getattr(self.mm_traits, f"{operand_names[i]}_layout").strides[1]
|
|
2284
|
+
if i < self.num_operands
|
|
2285
|
+
else distribution._data_shape[0]
|
|
2286
|
+
)
|
|
2287
|
+
if i == 3 and self.inplace:
|
|
2288
|
+
assert self.mm_traits.c_layout is not None, "Internal error."
|
|
2289
|
+
lld = self.mm_traits.c_layout.strides[1]
|
|
2290
|
+
descriptor = cublasMp.matrix_descriptor_create(
|
|
2291
|
+
distribution._data_global_shape[0],
|
|
2292
|
+
distribution._data_global_shape[1],
|
|
2293
|
+
distribution.block_sizes[0],
|
|
2294
|
+
distribution.block_sizes[1],
|
|
2295
|
+
distribution.first_process[0],
|
|
2296
|
+
distribution.first_process[1],
|
|
2297
|
+
lld,
|
|
2298
|
+
matrix_dtypes[i],
|
|
2299
|
+
self.lib_process_grids[min(i, 2)],
|
|
2300
|
+
)
|
|
2301
|
+
self.matrix_descriptors.append(descriptor)
|
|
2302
|
+
|
|
2303
|
+
if self.options.block_scaling and self.d_dtype_width <= 8: # FP8 and FP4
|
|
2304
|
+
self.mm_desc_ifc.d_out_scale_mode = (
|
|
2305
|
+
cublasMp.MatmulMatrixScale.VEC16_UE4M3 if self.d_dtype_width == 4 else cublasMp.MatmulMatrixScale.VEC32_UE8M0
|
|
2306
|
+
)
|
|
2307
|
+
|
|
2308
|
+
if self.input_type_width <= 8:
|
|
2309
|
+
self._check_local_gemm_sizes_fp4_fp8()
|
|
2310
|
+
self._prepare_operand_quantization_scales(self.quantization_scales, stream_holder)
|
|
2311
|
+
|
|
2312
|
+
alpha_ptr, beta_ptr = self.alpha.ctypes.data, self.beta.ctypes.data
|
|
2313
|
+
self.workspace_size_device, self.workspace_size_host = cublasMp.matmul_buffer_size(
|
|
2314
|
+
self.handle,
|
|
2315
|
+
self.mm_desc,
|
|
2316
|
+
mm_traits.M,
|
|
2317
|
+
mm_traits.N,
|
|
2318
|
+
mm_traits.K,
|
|
2319
|
+
alpha_ptr,
|
|
2320
|
+
self.operands[0].data_ptr,
|
|
2321
|
+
1,
|
|
2322
|
+
1,
|
|
2323
|
+
self.matrix_descriptors[0],
|
|
2324
|
+
self.operands[1].data_ptr,
|
|
2325
|
+
1,
|
|
2326
|
+
1,
|
|
2327
|
+
self.matrix_descriptors[1],
|
|
2328
|
+
beta_ptr,
|
|
2329
|
+
0 if self.num_operands == 2 else self.operands[2].data_ptr,
|
|
2330
|
+
1,
|
|
2331
|
+
1,
|
|
2332
|
+
self.matrix_descriptors[2],
|
|
2333
|
+
0, # d pointer
|
|
2334
|
+
1,
|
|
2335
|
+
1,
|
|
2336
|
+
self.matrix_descriptors[3],
|
|
2337
|
+
)
|
|
2338
|
+
|
|
2339
|
+
self.mm_planned = True
|
|
2340
|
+
|
|
2341
|
+
def _set_result_sheap_flag(self):
|
|
2342
|
+
# This refers only to NVSHMEM symmetric memory.
|
|
2343
|
+
self.result_on_symmetric_memory = False
|
|
2344
|
+
on_symmetric_memory = {o.is_symmetric_memory for o in self.operands}
|
|
2345
|
+
if len(on_symmetric_memory) == 2:
|
|
2346
|
+
self.logger.warning(
|
|
2347
|
+
"Some operands are on symmetric memory and others are not. Result won't be allocated on symmetric memory"
|
|
2348
|
+
)
|
|
2349
|
+
elif on_symmetric_memory == {True}:
|
|
2350
|
+
if self.memory_space == "cuda":
|
|
2351
|
+
self.logger.info("Input operands are on symmetric memory. Result will be allocated on symmetric memory.")
|
|
2352
|
+
self.result_on_symmetric_memory = True
|
|
2353
|
+
|
|
2354
|
+
def _check_and_set_operand(
|
|
2355
|
+
self,
|
|
2356
|
+
operand,
|
|
2357
|
+
operand_name,
|
|
2358
|
+
mm_desc_ifc,
|
|
2359
|
+
stream_holder,
|
|
2360
|
+
*,
|
|
2361
|
+
operand_index=None,
|
|
2362
|
+
epilog_name=None,
|
|
2363
|
+
package=None,
|
|
2364
|
+
dtype=None,
|
|
2365
|
+
extents=None,
|
|
2366
|
+
strides=None,
|
|
2367
|
+
):
|
|
2368
|
+
"""
|
|
2369
|
+
Check to make sure that the provided operand is consistent with the one it's
|
|
2370
|
+
updating, and update it.
|
|
2371
|
+
"""
|
|
2372
|
+
assert (operand_index is None) ^ (epilog_name is None), "Internal Error."
|
|
2373
|
+
assert self.operands is not None, "Internal Error."
|
|
2374
|
+
|
|
2375
|
+
# Make sure that the data type and extents match.
|
|
2376
|
+
utils.check_attribute_match(dtype, operand.dtype, "data type")
|
|
2377
|
+
utils.check_attribute_match(extents, operand.shape, "extents")
|
|
2378
|
+
|
|
2379
|
+
package = utils.infer_object_package(operand.tensor)
|
|
2380
|
+
|
|
2381
|
+
# Conjugate flag of the provided operands must match the original qualifiers
|
|
2382
|
+
if operand_index is not None and self.lazy_conjugation[operand_index] != operand.is_conjugate:
|
|
2383
|
+
raise ValueError(f"The provided operand {operand_name} has different conjugate flag than the original operand")
|
|
2384
|
+
|
|
2385
|
+
device_id = operand.device_id
|
|
2386
|
+
# When memory_space is "cpu", operands should be on CPU even though
|
|
2387
|
+
# self.device_id is the execution device.
|
|
2388
|
+
expected_device_id = "cpu" if self.memory_space == "cpu" else self.device_id
|
|
2389
|
+
if expected_device_id != device_id:
|
|
2390
|
+
raise ValueError(
|
|
2391
|
+
f'The operand {operand_name} is on device "{device_id}", but it should be on device '
|
|
2392
|
+
f'"{expected_device_id}" to match the original operand.'
|
|
2393
|
+
)
|
|
2394
|
+
|
|
2395
|
+
if device_id == "cpu":
|
|
2396
|
+
if self.package != package:
|
|
2397
|
+
message = f"Library package mismatch: '{self.package}' => '{package}'"
|
|
2398
|
+
raise TypeError(message)
|
|
2399
|
+
|
|
2400
|
+
# Check if we have a GPU buffer to update into.
|
|
2401
|
+
if operand_index is not None:
|
|
2402
|
+
o = self.operands[operand_index]
|
|
2403
|
+
else:
|
|
2404
|
+
o = self.epilog_operands[epilog_name]
|
|
2405
|
+
if o is None: # No buffer, create one.
|
|
2406
|
+
# Copy operand across memory spaces (CPU to GPU).
|
|
2407
|
+
# Currently, AG+GEMM in cuBLASMp performs better when B is on
|
|
2408
|
+
# symmetric memory, so the buffer for B is allocated in preparation
|
|
2409
|
+
# for NCCL symmetric memory registration.
|
|
2410
|
+
o = operand.to(self.device_id, stream_holder, symmetric_memory="nccl" if operand_index == 1 else None)
|
|
2411
|
+
if operand_index is not None:
|
|
2412
|
+
self.operands[operand_index] = o
|
|
2413
|
+
else:
|
|
2414
|
+
self.epilog_operands[epilog_name] = o
|
|
2415
|
+
# Update the epilog pointer, since we're starting afresh.
|
|
2416
|
+
self.epilog_input_name_to_handler[epilog_name].update(mm_desc_ifc, o)
|
|
2417
|
+
else:
|
|
2418
|
+
# In-place copy to existing device pointer because the new operand is on the
|
|
2419
|
+
# CPU.
|
|
2420
|
+
o.copy_(operand, stream_holder=stream_holder)
|
|
2421
|
+
if self.memory_space == "cpu" and operand_index == 2:
|
|
2422
|
+
# Hold references, needed for inplace operations.
|
|
2423
|
+
self.cpu_c_ref = operand
|
|
2424
|
+
else:
|
|
2425
|
+
if self.package != package:
|
|
2426
|
+
message = f"Library package mismatch: '{self.package}' => '{package}'"
|
|
2427
|
+
raise TypeError(message)
|
|
2428
|
+
|
|
2429
|
+
utils.check_attribute_match(strides, operand.strides, "strides")
|
|
2430
|
+
|
|
2431
|
+
if self.device_id != device_id:
|
|
2432
|
+
raise ValueError(
|
|
2433
|
+
f"The operand {operand_name} must be on the same device ({device_id}) as the original operand "
|
|
2434
|
+
f"({self.device_id})."
|
|
2435
|
+
)
|
|
2436
|
+
|
|
2437
|
+
# Finally, replace the original operand by the new one.
|
|
2438
|
+
if operand_index is not None:
|
|
2439
|
+
self.operands[operand_index] = operand
|
|
2440
|
+
else:
|
|
2441
|
+
self.epilog_operands[epilog_name] = operand
|
|
2442
|
+
# Update the epilog pointer, since we're starting afresh.
|
|
2443
|
+
self.epilog_input_name_to_handler[epilog_name].update(mm_desc_ifc, operand)
|
|
2444
|
+
|
|
2445
|
+
self.logger.info(f"Operand '{operand_name}' has been reset to the new value.")
|
|
2446
|
+
|
|
2447
|
+
return
|
|
2448
|
+
|
|
2449
|
+
def _reset_quantization_scales_unchecked(self, quantization_scales_obj, stream, stream_holder):
|
|
2450
|
+
"""
|
|
2451
|
+
Unchecked reset of quantization scales for operands A, B, C, and D.
|
|
2452
|
+
|
|
2453
|
+
Works for both CPU and CUDA memory spaces. For each provided scale the
|
|
2454
|
+
function updates the host-side value in ``self.quantization_scales``, updates
|
|
2455
|
+
the existing device-side holder in ``self.quantization_scales_device``
|
|
2456
|
+
(copy in-place or rebind ``.tensor``), and refreshes the descriptor pointer.
|
|
2457
|
+
|
|
2458
|
+
A stream holder is created lazily only when a CPU-to-GPU copy is needed
|
|
2459
|
+
(scalar scales, or tensor scales in CPU memory space).
|
|
2460
|
+
|
|
2461
|
+
Returns the stream_holder (may be newly created).
|
|
2462
|
+
"""
|
|
2463
|
+
mm_desc_ifc = self.mm_desc_ifc
|
|
2464
|
+
qsd = self.quantization_scales_device
|
|
2465
|
+
|
|
2466
|
+
def _update(holder, scale):
|
|
2467
|
+
nonlocal stream_holder
|
|
2468
|
+
if isinstance(scale, (int, float)):
|
|
2469
|
+
if stream_holder is None:
|
|
2470
|
+
stream_holder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
|
|
2471
|
+
src = tensor_wrapper.wrap_operand(np.asarray(scale, dtype="float32"))
|
|
2472
|
+
_realloc_if_needed_and_copy_to_mirror(src, holder, self.device_id, stream_holder)
|
|
2473
|
+
elif self.memory_space == "cpu":
|
|
2474
|
+
if stream_holder is None:
|
|
2475
|
+
stream_holder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
|
|
2476
|
+
_realloc_if_needed_and_copy_to_mirror(tensor_wrapper.wrap_operand(scale), holder, self.device_id, stream_holder)
|
|
2477
|
+
else:
|
|
2478
|
+
holder.tensor = scale
|
|
2479
|
+
|
|
2480
|
+
if quantization_scales_obj.a is not None:
|
|
2481
|
+
self.quantization_scales.a = quantization_scales_obj.a
|
|
2482
|
+
holder = qsd["a"]
|
|
2483
|
+
_update(holder, quantization_scales_obj.a)
|
|
2484
|
+
mm_desc_ifc.set_a_scale_pointer_unchecked(holder.data_ptr)
|
|
2485
|
+
|
|
2486
|
+
if quantization_scales_obj.b is not None:
|
|
2487
|
+
self.quantization_scales.b = quantization_scales_obj.b
|
|
2488
|
+
holder = qsd["b"]
|
|
2489
|
+
_update(holder, quantization_scales_obj.b)
|
|
2490
|
+
mm_desc_ifc.set_b_scale_pointer_unchecked(holder.data_ptr)
|
|
2491
|
+
|
|
2492
|
+
if quantization_scales_obj.c is not None:
|
|
2493
|
+
self.quantization_scales.c = quantization_scales_obj.c
|
|
2494
|
+
holder = qsd["c"]
|
|
2495
|
+
_update(holder, quantization_scales_obj.c)
|
|
2496
|
+
mm_desc_ifc.set_c_scale_pointer_unchecked(holder.data_ptr)
|
|
2497
|
+
|
|
2498
|
+
if quantization_scales_obj.d is not None:
|
|
2499
|
+
self.quantization_scales.d = quantization_scales_obj.d
|
|
2500
|
+
holder = qsd["d"]
|
|
2501
|
+
_update(holder, quantization_scales_obj.d)
|
|
2502
|
+
mm_desc_ifc.set_d_scale_pointer_unchecked(holder.data_ptr)
|
|
2503
|
+
|
|
2504
|
+
return stream_holder
|
|
2505
|
+
|
|
2506
|
+
def _reset_aux_quantization_scale_unchecked(self, aux_quantization_scale, stream, stream_holder):
|
|
2507
|
+
"""
|
|
2508
|
+
Unchecked reset of aux_quantization_scale.
|
|
2509
|
+
|
|
2510
|
+
Works for both CPU and CUDA memory spaces. Updates the existing
|
|
2511
|
+
device-side holder (copy in-place or rebind ``.tensor``) and refreshes
|
|
2512
|
+
the descriptor pointer.
|
|
2513
|
+
A stream holder is created lazily only when a CPU-to-GPU copy is needed.
|
|
2514
|
+
|
|
2515
|
+
Returns the stream_holder (may be newly created).
|
|
2516
|
+
"""
|
|
2517
|
+
holder = self.quantization_scales_device["epilog_aux"]
|
|
2518
|
+
|
|
2519
|
+
if isinstance(aux_quantization_scale, (int, float)):
|
|
2520
|
+
if stream_holder is None:
|
|
2521
|
+
stream_holder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
|
|
2522
|
+
src = tensor_wrapper.wrap_operand(np.asarray(aux_quantization_scale, dtype="float32"))
|
|
2523
|
+
_realloc_if_needed_and_copy_to_mirror(src, holder, self.device_id, stream_holder)
|
|
2524
|
+
elif self.memory_space == "cpu":
|
|
2525
|
+
if stream_holder is None:
|
|
2526
|
+
stream_holder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
|
|
2527
|
+
_realloc_if_needed_and_copy_to_mirror(
|
|
2528
|
+
tensor_wrapper.wrap_operand(aux_quantization_scale), holder, self.device_id, stream_holder
|
|
2529
|
+
)
|
|
2530
|
+
else:
|
|
2531
|
+
holder.tensor = aux_quantization_scale
|
|
2532
|
+
|
|
2533
|
+
self.mm_desc_ifc.epilogue_aux_scale_pointer = holder.data_ptr
|
|
2534
|
+
return stream_holder
|
|
2535
|
+
|
|
2536
|
+
def reset_operands_unchecked(
|
|
2537
|
+
self,
|
|
2538
|
+
*,
|
|
2539
|
+
a=None,
|
|
2540
|
+
b=None,
|
|
2541
|
+
c=None,
|
|
2542
|
+
alpha=None,
|
|
2543
|
+
beta=None,
|
|
2544
|
+
quantization_scales=None,
|
|
2545
|
+
epilog_inputs=None,
|
|
2546
|
+
stream: utils.AnyStream | int | None = None,
|
|
2547
|
+
):
|
|
2548
|
+
"""
|
|
2549
|
+
{reset_operands_unchecked}
|
|
2550
|
+
"""
|
|
2551
|
+
if alpha is not None:
|
|
2552
|
+
self.alpha[0] = alpha
|
|
2553
|
+
|
|
2554
|
+
if beta is not None:
|
|
2555
|
+
self.beta[0] = beta
|
|
2556
|
+
|
|
2557
|
+
stream_holder = None
|
|
2558
|
+
|
|
2559
|
+
if epilog_inputs is not None and "aux_quantization_scale" in epilog_inputs:
|
|
2560
|
+
aux_quantization_scale = epilog_inputs.get("aux_quantization_scale")
|
|
2561
|
+
stream_holder = self._reset_aux_quantization_scale_unchecked(aux_quantization_scale, stream, stream_holder)
|
|
2562
|
+
|
|
2563
|
+
quantization_scales_obj = quantization_scales
|
|
2564
|
+
if quantization_scales is not None and isinstance(quantization_scales, dict):
|
|
2565
|
+
quantization_scales_obj = _configuration.MatmulQuantizationScales(**quantization_scales)
|
|
2566
|
+
|
|
2567
|
+
if self.memory_space == "cuda":
|
|
2568
|
+
if a is not None:
|
|
2569
|
+
self.operands[0].tensor = a # type: ignore[index, union-attr]
|
|
2570
|
+
if b is not None:
|
|
2571
|
+
if self.operands[1].tensor is not None:
|
|
2572
|
+
self._deregister_operand_buffer_perhaps(operand_index=1)
|
|
2573
|
+
self.operands[1].tensor = b # type: ignore[index, union-attr]
|
|
2574
|
+
self._register_operand_buffer_perhaps(operand_index=1)
|
|
2575
|
+
if c is not None:
|
|
2576
|
+
self.operands[2].tensor = c # type: ignore[index, union-attr]
|
|
2577
|
+
|
|
2578
|
+
if epilog_inputs is not None:
|
|
2579
|
+
for epilog_name, epilog_value in epilog_inputs.items():
|
|
2580
|
+
if epilog_name != "aux_quantization_scale":
|
|
2581
|
+
self.epilog_operands[epilog_name].tensor = epilog_value
|
|
2582
|
+
self.epilog_input_name_to_handler[epilog_name].update_pointer(
|
|
2583
|
+
self.mm_desc_ifc, self.epilog_operands[epilog_name].data_ptr
|
|
2584
|
+
)
|
|
2585
|
+
|
|
2586
|
+
else:
|
|
2587
|
+
if stream_holder is None:
|
|
2588
|
+
stream_holder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
|
|
2589
|
+
|
|
2590
|
+
if a is not None:
|
|
2591
|
+
a_wrapped = tensor_wrapper.wrap_operand(a)
|
|
2592
|
+
_realloc_if_needed_and_copy_to_mirror(a_wrapped, self.operands[0], self.device_id, stream_holder) # type: ignore[index, union-attr]
|
|
2593
|
+
if b is not None:
|
|
2594
|
+
b_wrapped = tensor_wrapper.wrap_operand(b)
|
|
2595
|
+
register_buffer = self.operands[1].tensor is None
|
|
2596
|
+
_realloc_if_needed_and_copy_to_mirror(
|
|
2597
|
+
b_wrapped, self.operands[1], self.device_id, stream_holder, symmetric_memory="nccl"
|
|
2598
|
+
) # type: ignore[index, union-attr]
|
|
2599
|
+
if register_buffer:
|
|
2600
|
+
self._register_operand_buffer_perhaps(operand_index=1)
|
|
2601
|
+
if c is not None:
|
|
2602
|
+
c_wrapped = tensor_wrapper.wrap_operand(c)
|
|
2603
|
+
_realloc_if_needed_and_copy_to_mirror(c_wrapped, self.operands[2], self.device_id, stream_holder) # type: ignore[index, union-attr]
|
|
2604
|
+
if self.inplace:
|
|
2605
|
+
self.cpu_c_ref = c_wrapped
|
|
2606
|
+
|
|
2607
|
+
if epilog_inputs is not None:
|
|
2608
|
+
for epilog_name, epilog_value in epilog_inputs.items():
|
|
2609
|
+
if epilog_name != "aux_quantization_scale":
|
|
2610
|
+
epilog_input_wrapped = tensor_wrapper.wrap_operand(epilog_value)
|
|
2611
|
+
reallocated = _realloc_if_needed_and_copy_to_mirror(
|
|
2612
|
+
epilog_input_wrapped, self.epilog_operands[epilog_name], self.device_id, stream_holder
|
|
2613
|
+
)
|
|
2614
|
+
if reallocated:
|
|
2615
|
+
self.epilog_input_name_to_handler[epilog_name].update_pointer(
|
|
2616
|
+
self.mm_desc_ifc, self.epilog_operands[epilog_name].data_ptr
|
|
2617
|
+
)
|
|
2618
|
+
|
|
2619
|
+
if quantization_scales_obj is not None:
|
|
2620
|
+
self._reset_quantization_scales_unchecked(quantization_scales_obj, stream, stream_holder)
|
|
2621
|
+
|
|
2622
|
+
self._operands_released = False
|
|
2623
|
+
self._set_result_sheap_flag()
|
|
2624
|
+
|
|
2625
|
+
@utils.precondition(_check_valid_matmul)
|
|
2626
|
+
def reset_operands(
|
|
2627
|
+
self,
|
|
2628
|
+
*,
|
|
2629
|
+
a=None,
|
|
2630
|
+
b=None,
|
|
2631
|
+
c=None,
|
|
2632
|
+
alpha=None,
|
|
2633
|
+
beta=None,
|
|
2634
|
+
quantization_scales=None,
|
|
2635
|
+
epilog_inputs=None,
|
|
2636
|
+
stream: utils.AnyStream | int | None = None,
|
|
2637
|
+
):
|
|
2638
|
+
"""
|
|
2639
|
+
Reset one or more operands held by this :class:`Matmul` instance.
|
|
2640
|
+
|
|
2641
|
+
.. versionchanged:: 0.9
|
|
2642
|
+
All parameters are now keyword-only.
|
|
2643
|
+
|
|
2644
|
+
Args:
|
|
2645
|
+
a: {a}
|
|
2646
|
+
|
|
2647
|
+
b: {b}
|
|
2648
|
+
|
|
2649
|
+
c: {c}
|
|
2650
|
+
|
|
2651
|
+
alpha: {alpha}
|
|
2652
|
+
|
|
2653
|
+
beta: {beta}
|
|
2654
|
+
|
|
2655
|
+
quantization_scales: {quantization_scales}
|
|
2656
|
+
|
|
2657
|
+
epilog_inputs: {epilog_inputs}
|
|
2658
|
+
|
|
2659
|
+
stream: {stream}
|
|
2660
|
+
|
|
2661
|
+
Semantics:
|
|
2662
|
+
- Only the operands explicitly passed are updated. At least one operand
|
|
2663
|
+
is required (all of them after :meth:`release_operands`), otherwise
|
|
2664
|
+
a :class:`ValueError` is raised.
|
|
2665
|
+
|
|
2666
|
+
- This method will perform various checks on the new operands to make sure:
|
|
2667
|
+
|
|
2668
|
+
- The distributions, shapes, strides, datatypes match those of the old ones.
|
|
2669
|
+
- The packages that the operands belong to match those of the old ones.
|
|
2670
|
+
- If input tensors are on GPU, the device must match.
|
|
2671
|
+
|
|
2672
|
+
Examples:
|
|
2673
|
+
|
|
2674
|
+
>>> import cupy as cp
|
|
2675
|
+
>>> import nvmath.distributed
|
|
2676
|
+
>>> from nvmath.distributed.distribution import Slab
|
|
2677
|
+
|
|
2678
|
+
Get process group used to initialize nvmath.distributed (for information on
|
|
2679
|
+
initializing ``nvmath.distributed``, you can refer to the documentation or to
|
|
2680
|
+
the Matmul examples in `nvmath/examples/distributed/linalg/advanced
|
|
2681
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/linalg/advanced/matmul>`_):
|
|
2682
|
+
|
|
2683
|
+
>>> process_group = nvmath.distributed.get_context().process_group
|
|
2684
|
+
|
|
2685
|
+
Get my process rank:
|
|
2686
|
+
|
|
2687
|
+
>>> rank = process_group.rank
|
|
2688
|
+
|
|
2689
|
+
Create two 3-D float64 ndarrays on the GPU (using Slab distributions to
|
|
2690
|
+
distribute the matrices across processes):
|
|
2691
|
+
|
|
2692
|
+
>>> M, N, K = 128, 128, 256
|
|
2693
|
+
>>> a_shape = Slab.X.shape(rank, (M, K))
|
|
2694
|
+
>>> b_shape = Slab.Y.shape(rank, (K, N))
|
|
2695
|
+
>>> device_id = nvmath.distributed.get_context().device_id
|
|
2696
|
+
>>> with cp.cuda.Device(device_id):
|
|
2697
|
+
... a = cp.asfortranarray(cp.random.rand(*a_shape))
|
|
2698
|
+
... b = cp.asfortranarray(cp.random.rand(*b_shape))
|
|
2699
|
+
|
|
2700
|
+
Create an matrix multiplication object as a context manager
|
|
2701
|
+
|
|
2702
|
+
>>> d = [Slab.X, Slab.Y, Slab.X]
|
|
2703
|
+
>>> with nvmath.distributed.linalg.advanced.Matmul(a, b, distributions=d) as mm:
|
|
2704
|
+
... # Plan the operation.
|
|
2705
|
+
... mm.plan()
|
|
2706
|
+
...
|
|
2707
|
+
... # Execute the MM to get the first result.
|
|
2708
|
+
... r1 = mm.execute()
|
|
2709
|
+
...
|
|
2710
|
+
... # Reset the operands to new CuPy ndarrays.
|
|
2711
|
+
... with cp.cuda.Device(device_id):
|
|
2712
|
+
... a_new = cp.asfortranarray(cp.random.rand(*a_shape))
|
|
2713
|
+
... b_new = cp.asfortranarray(cp.random.rand(*b_shape))
|
|
2714
|
+
... mm.reset_operands(a=a_new, b=b_new)
|
|
2715
|
+
...
|
|
2716
|
+
... # Execute to get the new result corresponding to the updated operands.
|
|
2717
|
+
... r2 = mm.execute()
|
|
2718
|
+
|
|
2719
|
+
Note that if only a subset of operands are reset, the operands that are not
|
|
2720
|
+
reset hold their original values.
|
|
2721
|
+
|
|
2722
|
+
With :meth:`reset_operands`, minimal overhead is achieved as problem
|
|
2723
|
+
specification and planning are only performed once.
|
|
2724
|
+
|
|
2725
|
+
For the particular example above, explicitly calling :meth:`reset_operands` is
|
|
2726
|
+
functionally equivalent to updating the operands in-place, i.e, replacing
|
|
2727
|
+
``mm.reset_operands(a=a_new, b=b_new)`` with ``a[:]=a_new`` and ``b[:]=b_new``.
|
|
2728
|
+
Note that updating the operand in-place should be adopted with caution as
|
|
2729
|
+
it can only yield the expected result when the operand memory space is
|
|
2730
|
+
accessible from the execution space.
|
|
2731
|
+
|
|
2732
|
+
For more details, please refer to `inplace update example
|
|
2733
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/linalg/advanced/matmul/example06_stateful_inplace.py>`_.
|
|
2734
|
+
|
|
2735
|
+
.. seealso::
|
|
2736
|
+
:meth:`release_operands`, :meth:`reset_operands_unchecked`
|
|
2737
|
+
"""
|
|
2738
|
+
|
|
2739
|
+
if c is not None and self.num_operands == 2:
|
|
2740
|
+
raise ValueError(
|
|
2741
|
+
"The matrix multiplication problem specification does not include operand C, so it cannot be reset."
|
|
2742
|
+
)
|
|
2743
|
+
|
|
2744
|
+
# If operands have been released, all required operands must be provided.
|
|
2745
|
+
if self._operands_released:
|
|
2746
|
+
abc_ok = a is not None and b is not None and (self.num_operands != 3 or c is not None)
|
|
2747
|
+
|
|
2748
|
+
epilog_names = self.epilog_inputs_traits.keys()
|
|
2749
|
+
epilog_ok = True
|
|
2750
|
+
if epilog_names:
|
|
2751
|
+
if epilog_inputs is None:
|
|
2752
|
+
epilog_ok = False
|
|
2753
|
+
elif epilog_names != epilog_inputs.keys():
|
|
2754
|
+
raise ValueError(
|
|
2755
|
+
f"The epilog inputs {epilog_names} are required. The provided epilog input names are "
|
|
2756
|
+
f"{epilog_inputs.keys()}."
|
|
2757
|
+
)
|
|
2758
|
+
|
|
2759
|
+
scales_ok = True
|
|
2760
|
+
needs_scales = self.input_type_width <= 8
|
|
2761
|
+
if needs_scales and quantization_scales is None:
|
|
2762
|
+
scales_ok = False
|
|
2763
|
+
|
|
2764
|
+
if not abc_ok:
|
|
2765
|
+
raise ValueError(
|
|
2766
|
+
"After release_operands(), all required operands must be provided to reset_operands(). "
|
|
2767
|
+
f"Required: a, b{', c' if self.num_operands == 3 else ''}."
|
|
2768
|
+
)
|
|
2769
|
+
if not scales_ok:
|
|
2770
|
+
raise ValueError("After release_operands(), quantization_scales must be provided to reset_operands().")
|
|
2771
|
+
if not epilog_ok:
|
|
2772
|
+
raise ValueError(
|
|
2773
|
+
"After release_operands(), epilog_inputs must be provided to reset_operands(). "
|
|
2774
|
+
f"Required epilog inputs: {epilog_names}."
|
|
2775
|
+
)
|
|
2776
|
+
|
|
2777
|
+
self.operands = [None] * self.num_operands # type: ignore
|
|
2778
|
+
self.epilog_operands = dict.fromkeys(epilog_names)
|
|
2779
|
+
if needs_scales:
|
|
2780
|
+
self.quantization_scales = _configuration.MatmulQuantizationScales()
|
|
2781
|
+
elif all(arg is None for arg in (a, b, c, alpha, beta, quantization_scales, epilog_inputs)):
|
|
2782
|
+
# All arguments are None: there is nothing to update, so reject the call.
|
|
2783
|
+
raise ValueError("reset_operands() requires at least one operand to be provided.")
|
|
2784
|
+
|
|
2785
|
+
# Update alpha.
|
|
2786
|
+
if alpha is not None:
|
|
2787
|
+
try:
|
|
2788
|
+
self.alpha[0] = alpha
|
|
2789
|
+
except (ValueError, TypeError) as e:
|
|
2790
|
+
raise ValueError(
|
|
2791
|
+
f"The value provided for alpha {alpha} is not convertible to dtype '{self.alpha.dtype}'."
|
|
2792
|
+
) from e
|
|
2793
|
+
|
|
2794
|
+
# Update beta.
|
|
2795
|
+
if beta is not None:
|
|
2796
|
+
if self.num_operands == 2:
|
|
2797
|
+
self.logger.warning(f"Matmul: The provided beta value {beta} is ignored since operand C is not specified.")
|
|
2798
|
+
else:
|
|
2799
|
+
try:
|
|
2800
|
+
self.beta[0] = beta
|
|
2801
|
+
except (ValueError, TypeError) as e:
|
|
2802
|
+
raise ValueError(
|
|
2803
|
+
f"The value provided for beta {beta} is not convertible to dtype '{self.beta.dtype}'."
|
|
2804
|
+
) from e
|
|
2805
|
+
|
|
2806
|
+
stream_holder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
|
|
2807
|
+
|
|
2808
|
+
# Update quantization_scales.
|
|
2809
|
+
if quantization_scales is not None:
|
|
2810
|
+
quantization_scales = self._validate_operand_scales(quantization_scales, all_required=False)
|
|
2811
|
+
if quantization_scales.a is not None:
|
|
2812
|
+
self.quantization_scales.a = quantization_scales.a
|
|
2813
|
+
if quantization_scales.b is not None:
|
|
2814
|
+
self.quantization_scales.b = quantization_scales.b
|
|
2815
|
+
if quantization_scales.c is not None:
|
|
2816
|
+
self.quantization_scales.c = quantization_scales.c
|
|
2817
|
+
if quantization_scales.d is not None:
|
|
2818
|
+
self.quantization_scales.d = quantization_scales.d
|
|
2819
|
+
self._prepare_operand_quantization_scales(self.quantization_scales, stream_holder)
|
|
2820
|
+
|
|
2821
|
+
if epilog_inputs is not None and "aux_quantization_scale" in epilog_inputs:
|
|
2822
|
+
epilog_inputs = epilog_inputs.copy()
|
|
2823
|
+
aux_quantization_scale = epilog_inputs.pop("aux_quantization_scale")
|
|
2824
|
+
self._validate_epilog_aux_scale(aux_quantization_scale, required=False)
|
|
2825
|
+
self._prepare_single_validated_scale(
|
|
2826
|
+
aux_quantization_scale, "epilog_aux", cublas_operand="epilogue_aux", stream_holder=stream_holder
|
|
2827
|
+
)
|
|
2828
|
+
|
|
2829
|
+
# Reset the provided operands.
|
|
2830
|
+
if a is not None:
|
|
2831
|
+
a = tensor_wrapper.wrap_operand(a)
|
|
2832
|
+
index = 0
|
|
2833
|
+
self._check_and_set_operand(
|
|
2834
|
+
a,
|
|
2835
|
+
"A",
|
|
2836
|
+
self.mm_desc_ifc,
|
|
2837
|
+
stream_holder,
|
|
2838
|
+
operand_index=index,
|
|
2839
|
+
dtype=self.a_dtype_name,
|
|
2840
|
+
extents=self.operand_extents[index],
|
|
2841
|
+
strides=self.operand_strides[index],
|
|
2842
|
+
)
|
|
2843
|
+
|
|
2844
|
+
if b is not None:
|
|
2845
|
+
b = tensor_wrapper.wrap_operand(b)
|
|
2846
|
+
index = 1
|
|
2847
|
+
if not self._operands_released:
|
|
2848
|
+
self._deregister_operand_buffer_perhaps(operand_index=index)
|
|
2849
|
+
self._check_and_set_operand(
|
|
2850
|
+
b,
|
|
2851
|
+
"B",
|
|
2852
|
+
self.mm_desc_ifc,
|
|
2853
|
+
stream_holder,
|
|
2854
|
+
operand_index=index,
|
|
2855
|
+
dtype=self.b_dtype_name,
|
|
2856
|
+
extents=self.operand_extents[index],
|
|
2857
|
+
strides=self.operand_strides[index],
|
|
2858
|
+
)
|
|
2859
|
+
self._register_operand_buffer_perhaps(operand_index=index)
|
|
2860
|
+
|
|
2861
|
+
if c is not None: # If we get here, we know that C is one of the operands in the problem specification.
|
|
2862
|
+
c = tensor_wrapper.wrap_operand(c)
|
|
2863
|
+
index = 2
|
|
2864
|
+
self._check_and_set_operand(
|
|
2865
|
+
c,
|
|
2866
|
+
"C",
|
|
2867
|
+
self.mm_desc_ifc,
|
|
2868
|
+
stream_holder,
|
|
2869
|
+
operand_index=index,
|
|
2870
|
+
dtype=self.c_dtype_name,
|
|
2871
|
+
extents=self.operand_extents[index],
|
|
2872
|
+
strides=self.operand_strides[index],
|
|
2873
|
+
)
|
|
2874
|
+
|
|
2875
|
+
# Reset the provided epilog inputs.
|
|
2876
|
+
if epilog_inputs is not None:
|
|
2877
|
+
for name in epilog_inputs:
|
|
2878
|
+
epilog_input = tensor_wrapper.wrap_operand(epilog_inputs[name])
|
|
2879
|
+
self._check_and_set_operand(
|
|
2880
|
+
epilog_input,
|
|
2881
|
+
name,
|
|
2882
|
+
self.mm_desc_ifc,
|
|
2883
|
+
stream_holder,
|
|
2884
|
+
epilog_name=name,
|
|
2885
|
+
dtype=self.epilog_inputs_traits[name].dtype,
|
|
2886
|
+
extents=self.epilog_inputs_traits[name].extents,
|
|
2887
|
+
strides=self.epilog_inputs_traits[name].strides,
|
|
2888
|
+
)
|
|
2889
|
+
|
|
2890
|
+
self._set_result_sheap_flag()
|
|
2891
|
+
self._operands_released = False
|
|
2892
|
+
|
|
2893
|
+
@utils.precondition(_check_valid_matmul)
|
|
2894
|
+
def release_operands(self):
|
|
2895
|
+
"""
|
|
2896
|
+
{release_operands}
|
|
2897
|
+
"""
|
|
2898
|
+
if self._operands_released:
|
|
2899
|
+
self.logger.info("Operands have already been released; nothing to do.")
|
|
2900
|
+
return
|
|
2901
|
+
|
|
2902
|
+
self._deregister_buffers()
|
|
2903
|
+
|
|
2904
|
+
# CUDA memory space:
|
|
2905
|
+
# self.operands, self.epilog_operands, and
|
|
2906
|
+
# self.quantization_scales_device hold direct user references;
|
|
2907
|
+
# self.quantization_scales holds the user-provided scales object.
|
|
2908
|
+
# CPU memory space:
|
|
2909
|
+
# self.operands, self.epilog_operands, and
|
|
2910
|
+
# self.quantization_scales_device hold internal device mirrors;
|
|
2911
|
+
# self.quantization_scales holds the user-provided scales object;
|
|
2912
|
+
# self.cpu_c_ref holds a direct reference to the user's CPU operand C.
|
|
2913
|
+
# In both cases, release all of them.
|
|
2914
|
+
# Note that if/when possible, we keep the TensorHolder wrappers and
|
|
2915
|
+
# container structures alive and only release the internal tensor reference.
|
|
2916
|
+
# This is useful when reset_operands_unchecked is called subsequently
|
|
2917
|
+
# because it can reuse the existing wrappers, saving overhead.
|
|
2918
|
+
for op in self.operands:
|
|
2919
|
+
op.tensor = None
|
|
2920
|
+
for op in self.epilog_operands.values():
|
|
2921
|
+
op.tensor = None
|
|
2922
|
+
for op in self.quantization_scales_device.values():
|
|
2923
|
+
op.tensor = None
|
|
2924
|
+
|
|
2925
|
+
# The attribute itself might not exist (non-narrow-precision paths), so check first.
|
|
2926
|
+
if hasattr(self, "quantization_scales"):
|
|
2927
|
+
self.quantization_scales.a = None
|
|
2928
|
+
self.quantization_scales.b = None
|
|
2929
|
+
self.quantization_scales.c = None
|
|
2930
|
+
self.quantization_scales.d = None
|
|
2931
|
+
|
|
2932
|
+
if self.memory_space == "cpu" and self.cpu_c_ref is not None:
|
|
2933
|
+
self.cpu_c_ref.tensor = None
|
|
2934
|
+
|
|
2935
|
+
self._operands_released = True
|
|
2936
|
+
self.logger.info("User-provided operands have been released.")
|
|
2937
|
+
|
|
2938
|
+
def _create_result_tensor(self, stream_holder: utils.StreamHolder, log_debug: bool):
|
|
2939
|
+
"""
|
|
2940
|
+
Create the output result tensor for the matmul operation.
|
|
2941
|
+
|
|
2942
|
+
We need special treatment for FP4 output (``float4_e2m1fn_x2``),
|
|
2943
|
+
because the logical shape ``(M, N)`` from ``result_traits`` is converted
|
|
2944
|
+
to a packed shape where one dimension is halved
|
|
2945
|
+
(since two FP4 values are packed per byte).
|
|
2946
|
+
|
|
2947
|
+
For all other dtypes, the logical shape is used directly.
|
|
2948
|
+
"""
|
|
2949
|
+
# result_layout is based on local properties.
|
|
2950
|
+
assert self.result_layout is not None, (
|
|
2951
|
+
"Internal error: result_layout must be set by plan() before creating the result tensor."
|
|
2952
|
+
)
|
|
2953
|
+
|
|
2954
|
+
if self.d_dtype != CudaDataType.CUDA_R_4F_E2M1:
|
|
2955
|
+
# Non-FP4 output: no packing needed, use logical shape as-is.
|
|
2956
|
+
result_shape = self.result_layout.shape
|
|
2957
|
+
result_strides = self.result_layout.strides
|
|
2958
|
+
|
|
2959
|
+
if log_debug:
|
|
2960
|
+
self.logger.debug(
|
|
2961
|
+
f"The output tensor shape = {result_shape} with strides = "
|
|
2962
|
+
f"{result_strides} and data type '{self.d_dtype_name}'."
|
|
2963
|
+
)
|
|
2964
|
+
|
|
2965
|
+
result = utils.create_empty_tensor(
|
|
2966
|
+
self.result_class,
|
|
2967
|
+
result_shape,
|
|
2968
|
+
self.d_dtype_name,
|
|
2969
|
+
self.device_id,
|
|
2970
|
+
stream_holder,
|
|
2971
|
+
verify_strides=False,
|
|
2972
|
+
strides=result_strides,
|
|
2973
|
+
symmetric_memory="nvshmem" if self.result_on_symmetric_memory else None,
|
|
2974
|
+
make_symmetric=self.result_on_symmetric_memory,
|
|
2975
|
+
)
|
|
2976
|
+
return result
|
|
2977
|
+
|
|
2978
|
+
# If we are here, we are dealing with FP4 output that must be packed.
|
|
2979
|
+
# Convert logical shape/strides to packed (physical) shape/strides.
|
|
2980
|
+
# Result is column-major and therefore packed on axis 0.
|
|
2981
|
+
result_packed_shape = (self.result_layout.shape[0] // 2, self.result_layout.shape[1])
|
|
2982
|
+
result_packed_strides = (self.result_layout.strides[0], self.result_layout.strides[1] // 2)
|
|
2983
|
+
|
|
2984
|
+
if log_debug:
|
|
2985
|
+
self.logger.debug(
|
|
2986
|
+
f"FP4 output: logical shape = {tuple(self.result_layout.shape)}, "
|
|
2987
|
+
f"logical strides = {tuple(self.result_layout.strides)} -> "
|
|
2988
|
+
f"packed shape = {result_packed_shape}, packed strides = {result_packed_strides}, "
|
|
2989
|
+
f"data type '{self.d_dtype_name}'."
|
|
2990
|
+
)
|
|
2991
|
+
|
|
2992
|
+
result = utils.create_empty_tensor(
|
|
2993
|
+
self.result_class,
|
|
2994
|
+
result_packed_shape,
|
|
2995
|
+
self.d_dtype_name,
|
|
2996
|
+
self.device_id,
|
|
2997
|
+
stream_holder,
|
|
2998
|
+
verify_strides=False,
|
|
2999
|
+
strides=result_packed_strides,
|
|
3000
|
+
symmetric_memory="nvshmem" if self.result_on_symmetric_memory else None,
|
|
3001
|
+
make_symmetric=self.result_on_symmetric_memory,
|
|
3002
|
+
)
|
|
3003
|
+
return result
|
|
3004
|
+
|
|
3005
|
+
@utils.precondition(_check_valid_matmul)
|
|
3006
|
+
@utils.precondition(_check_planned, "Execution")
|
|
3007
|
+
@utils.precondition(_check_valid_operands, "Execution")
|
|
3008
|
+
@utils.atomic(_release_workspace_memory_perhaps_wrapper, method=True)
|
|
3009
|
+
def execute(self, *, release_workspace=False, stream: utils.AnyStream | int | None = None):
|
|
3010
|
+
"""
|
|
3011
|
+
Execute a planned distributed matrix multiplication.
|
|
3012
|
+
|
|
3013
|
+
Args:
|
|
3014
|
+
release_workspace: {release_workspace}
|
|
3015
|
+
|
|
3016
|
+
stream: {stream}
|
|
3017
|
+
|
|
3018
|
+
Returns:
|
|
3019
|
+
{result}
|
|
3020
|
+
"""
|
|
3021
|
+
log_info = self.logger.isEnabledFor(logging.INFO)
|
|
3022
|
+
log_debug = self.logger.isEnabledFor(logging.DEBUG)
|
|
3023
|
+
|
|
3024
|
+
assert self.operands is not None, "Internal error."
|
|
3025
|
+
|
|
3026
|
+
if log_info:
|
|
3027
|
+
self.logger.info("= EXECUTION PHASE =")
|
|
3028
|
+
stream_holder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
|
|
3029
|
+
if log_info:
|
|
3030
|
+
self.logger.info(f"The specified stream for execute() is {stream_holder.obj}.")
|
|
3031
|
+
|
|
3032
|
+
# Allocate workspace if needed.
|
|
3033
|
+
# Note: workspace is allocated with cuBLASMp allocator, which calls ncclMemAlloc
|
|
3034
|
+
# (which is blocking).
|
|
3035
|
+
self._allocate_workspace_memory_perhaps(stream_holder)
|
|
3036
|
+
|
|
3037
|
+
# Create empty tensors for auxiliary output.
|
|
3038
|
+
for handler in self.epilog_output_handlers:
|
|
3039
|
+
name = handler.name
|
|
3040
|
+
shape, strides, dtype_name = handler.attributes()
|
|
3041
|
+
if log_debug:
|
|
3042
|
+
self.logger.debug(f"Beginning auxiliary output tensor '{name}' creation...")
|
|
3043
|
+
self.logger.debug(f"The '{name}' tensor shape = {shape} with strides = {strides} and data type '{dtype_name}'.")
|
|
3044
|
+
self.epilog_outputs[name] = aux_tensor = utils.create_empty_tensor(
|
|
3045
|
+
self.result_class,
|
|
3046
|
+
shape,
|
|
3047
|
+
dtype_name,
|
|
3048
|
+
self.device_id,
|
|
3049
|
+
stream_holder,
|
|
3050
|
+
verify_strides=False,
|
|
3051
|
+
strides=strides,
|
|
3052
|
+
)
|
|
3053
|
+
if name == "relu_aux" and self._expected_algo == 3:
|
|
3054
|
+
with utils.device_ctx(self.device_id):
|
|
3055
|
+
aux_tensor.tensor[:] = 0
|
|
3056
|
+
if log_debug:
|
|
3057
|
+
self.logger.debug(f"The auxiliary output tensor '{name}' has been created.")
|
|
3058
|
+
if self.preferences.epilog.aux_amax: # type: ignore[attr-defined]
|
|
3059
|
+
if "float8" not in dtype_name:
|
|
3060
|
+
raise ValueError("epilog.aux_amax=True is not supported when epilog output type is not FP8.")
|
|
3061
|
+
self.epilog_outputs[f"{name}_amax"] = utils.create_empty_tensor(
|
|
3062
|
+
self.result_class,
|
|
3063
|
+
(1,),
|
|
3064
|
+
"float32", # This is the only type allowed by cuBLAS for AMAX.
|
|
3065
|
+
self.device_id,
|
|
3066
|
+
stream_holder,
|
|
3067
|
+
verify_strides=False,
|
|
3068
|
+
)
|
|
3069
|
+
self.mm_desc_ifc.epilogue_aux_amax_pointer = self.epilog_outputs[f"{name}_amax"].data_ptr
|
|
3070
|
+
|
|
3071
|
+
# Update the data pointer in the MM descriptor.
|
|
3072
|
+
handler.update_pointer(self.mm_desc_ifc, aux_tensor.data_ptr)
|
|
3073
|
+
|
|
3074
|
+
# Create empty tensor for the result, if the operation is not in-place.
|
|
3075
|
+
if self.inplace:
|
|
3076
|
+
if log_debug:
|
|
3077
|
+
self.logger.debug("The operation is in-place (operand C will be overwritten).")
|
|
3078
|
+
result = self.operands[2]
|
|
3079
|
+
else:
|
|
3080
|
+
if log_debug:
|
|
3081
|
+
self.logger.debug("Beginning output (empty) tensor creation...")
|
|
3082
|
+
result = self._create_result_tensor(stream_holder, log_debug)
|
|
3083
|
+
if log_debug:
|
|
3084
|
+
self.logger.debug("The output (empty) tensor has been created.")
|
|
3085
|
+
|
|
3086
|
+
self.aux_outputs = {}
|
|
3087
|
+
|
|
3088
|
+
if self.options.result_amax:
|
|
3089
|
+
self.aux_outputs["result_amax"] = utils.create_empty_tensor(
|
|
3090
|
+
self.result_class,
|
|
3091
|
+
(1,),
|
|
3092
|
+
"float32", # This is the only type allowed by cuBLAS for AMAX.
|
|
3093
|
+
self.device_id,
|
|
3094
|
+
stream_holder,
|
|
3095
|
+
verify_strides=False,
|
|
3096
|
+
)
|
|
3097
|
+
self.mm_desc_ifc.amax_d_pointer = self.aux_outputs["result_amax"].data_ptr
|
|
3098
|
+
|
|
3099
|
+
if self.options.block_scaling and self.d_dtype_width <= 8: # FP8 and FP4
|
|
3100
|
+
num_output_elements = (
|
|
3101
|
+
self.mm_traits.d_layout.shape[0] * self.mm_traits.d_layout.shape[1] # type: ignore
|
|
3102
|
+
)
|
|
3103
|
+
d_out_scale = _create_d_out_scale(
|
|
3104
|
+
self.result_class, num_output_elements, self.d_dtype_width, self.device_id, stream_holder
|
|
3105
|
+
)
|
|
3106
|
+
self.aux_outputs["d_out_scale"] = d_out_scale
|
|
3107
|
+
self.mm_desc_ifc.d_out_scale_pointer = d_out_scale.data_ptr
|
|
3108
|
+
|
|
3109
|
+
a, b = self.operands[0], self.operands[1]
|
|
3110
|
+
raw_workspace_ptr_device = utils.get_ptr_from_memory_pointer(self.workspace_device)
|
|
3111
|
+
if log_info:
|
|
3112
|
+
self.logger.info("Starting distributed matrix multiplication...")
|
|
3113
|
+
self.logger.info(f"{self.call_prologue}")
|
|
3114
|
+
with utils.cuda_call_ctx(stream_holder, self.blocking, timing=log_info) as (
|
|
3115
|
+
self.last_compute_event,
|
|
3116
|
+
elapsed,
|
|
3117
|
+
):
|
|
3118
|
+
cublasMp.set_stream(self.handle, stream_holder.ptr)
|
|
3119
|
+
|
|
3120
|
+
nullptr = 0
|
|
3121
|
+
cublasMp.matmul(
|
|
3122
|
+
self.handle,
|
|
3123
|
+
self.mm_desc,
|
|
3124
|
+
self.mm_traits.M,
|
|
3125
|
+
self.mm_traits.N,
|
|
3126
|
+
self.mm_traits.K,
|
|
3127
|
+
self.alpha.ctypes.data,
|
|
3128
|
+
a.data_ptr,
|
|
3129
|
+
1,
|
|
3130
|
+
1,
|
|
3131
|
+
self.matrix_descriptors[0],
|
|
3132
|
+
b.data_ptr,
|
|
3133
|
+
1,
|
|
3134
|
+
1,
|
|
3135
|
+
self.matrix_descriptors[1],
|
|
3136
|
+
self.beta.ctypes.data,
|
|
3137
|
+
nullptr if self.num_operands == 2 else self.operands[2].data_ptr,
|
|
3138
|
+
1,
|
|
3139
|
+
1,
|
|
3140
|
+
self.matrix_descriptors[2],
|
|
3141
|
+
result.data_ptr,
|
|
3142
|
+
1,
|
|
3143
|
+
1,
|
|
3144
|
+
self.matrix_descriptors[3],
|
|
3145
|
+
raw_workspace_ptr_device,
|
|
3146
|
+
self.workspace_size_device,
|
|
3147
|
+
self.workspace_host.ctypes.data if self.workspace_size_host > 0 else nullptr, # type: ignore
|
|
3148
|
+
self.workspace_size_host,
|
|
3149
|
+
)
|
|
3150
|
+
|
|
3151
|
+
if log_info and elapsed.data is not None:
|
|
3152
|
+
self.logger.info(f"The distributed matrix multiplication calculation took {elapsed.data:.3f} ms to complete.")
|
|
3153
|
+
|
|
3154
|
+
# Establish ordering wrt the computation and free workspace if requested.
|
|
3155
|
+
if release_workspace:
|
|
3156
|
+
self._release_workspace_memory_perhaps(True)
|
|
3157
|
+
|
|
3158
|
+
# Return the result and auxiliary outputs, if present.
|
|
3159
|
+
all_outputs = self.epilog_outputs | self.aux_outputs
|
|
3160
|
+
if self.memory_space == "cpu":
|
|
3161
|
+
if self.inplace:
|
|
3162
|
+
# Overwrite operand C.
|
|
3163
|
+
assert self.cpu_c_ref is not None, "Internal error."
|
|
3164
|
+
self.cpu_c_ref.copy_(result, stream_holder=stream_holder)
|
|
3165
|
+
out = self.cpu_c_ref.tensor
|
|
3166
|
+
else:
|
|
3167
|
+
out = result.to("cpu", stream_holder=stream_holder).tensor
|
|
3168
|
+
# Copy auxiliary output to CPU.
|
|
3169
|
+
aux = {name: all_outputs[name].to("cpu", stream_holder=stream_holder).tensor for name in all_outputs}
|
|
3170
|
+
else:
|
|
3171
|
+
out = result.tensor
|
|
3172
|
+
# Return the unwrapped epilog output tensor(s).
|
|
3173
|
+
aux = {name: all_outputs[name].tensor for name in all_outputs}
|
|
3174
|
+
|
|
3175
|
+
self.aux_outputs = {}
|
|
3176
|
+
self.epilog_outputs = {}
|
|
3177
|
+
self._reset_workspace_allocation_tracking()
|
|
3178
|
+
|
|
3179
|
+
if aux:
|
|
3180
|
+
return out, aux
|
|
3181
|
+
|
|
3182
|
+
return out
|
|
3183
|
+
|
|
3184
|
+
def free(self):
|
|
3185
|
+
"""Free Matmul resources.
|
|
3186
|
+
|
|
3187
|
+
It is recommended that the :class:`Matmul` object be used within a context, but if
|
|
3188
|
+
it is not possible then this method must be called explicitly to ensure that the
|
|
3189
|
+
matrix multiplication resources (especially internal library objects) are properly
|
|
3190
|
+
cleaned up.
|
|
3191
|
+
"""
|
|
3192
|
+
|
|
3193
|
+
if not self.valid_state:
|
|
3194
|
+
return
|
|
3195
|
+
|
|
3196
|
+
try:
|
|
3197
|
+
# Future operations on the workspace stream should be ordered after the
|
|
3198
|
+
# computation.
|
|
3199
|
+
if self.last_compute_event is not None:
|
|
3200
|
+
self.workspace_stream.wait(self.last_compute_event)
|
|
3201
|
+
self.last_compute_event = None
|
|
3202
|
+
|
|
3203
|
+
self._free_workspace_memory()
|
|
3204
|
+
|
|
3205
|
+
self._free_plan_resources()
|
|
3206
|
+
|
|
3207
|
+
self._deregister_buffers()
|
|
3208
|
+
|
|
3209
|
+
with utils.device_ctx(self.device_id):
|
|
3210
|
+
# Destroy matmul descriptor.
|
|
3211
|
+
if self.mm_desc is not None:
|
|
3212
|
+
cublasMp.matmul_descriptor_destroy(self.mm_desc)
|
|
3213
|
+
self.mm_desc = None
|
|
3214
|
+
|
|
3215
|
+
# NOTE: cuBLASMp grids are stored in the global cache and destroyed
|
|
3216
|
+
# when the cache is cleared (this just clears the references from
|
|
3217
|
+
# this object).
|
|
3218
|
+
self.lib_process_grids = []
|
|
3219
|
+
|
|
3220
|
+
# Destroy cuBLASMp library handle.
|
|
3221
|
+
if self.handle is not None:
|
|
3222
|
+
cublasMp.destroy(self.handle)
|
|
3223
|
+
self.handle = None
|
|
3224
|
+
|
|
3225
|
+
_keep = {"logger", "valid_state"}
|
|
3226
|
+
for attr in list(vars(self)):
|
|
3227
|
+
if attr not in _keep:
|
|
3228
|
+
setattr(self, attr, None)
|
|
3229
|
+
|
|
3230
|
+
except Exception as e:
|
|
3231
|
+
self.logger.critical("Internal error: only part of the Matmul object's resources have been released.")
|
|
3232
|
+
self.logger.critical(str(e))
|
|
3233
|
+
raise e
|
|
3234
|
+
finally:
|
|
3235
|
+
self.valid_state = False
|
|
3236
|
+
|
|
3237
|
+
self.logger.info("The Matmul object's resources have been released.")
|
|
3238
|
+
|
|
3239
|
+
|
|
3240
|
+
@utils.docstring_decorator(SHARED_MM_DOCUMENTATION, skip_missing=False)
|
|
3241
|
+
def matmul(
|
|
3242
|
+
a,
|
|
3243
|
+
b,
|
|
3244
|
+
/,
|
|
3245
|
+
c=None,
|
|
3246
|
+
*,
|
|
3247
|
+
distributions: Sequence[Distribution],
|
|
3248
|
+
alpha=None,
|
|
3249
|
+
beta=None,
|
|
3250
|
+
epilog=None,
|
|
3251
|
+
epilog_inputs=None,
|
|
3252
|
+
qualifiers=None,
|
|
3253
|
+
quantization_scales=None,
|
|
3254
|
+
options: MatmulOptions | dict[str, typing.Any] | None = None,
|
|
3255
|
+
preferences=None,
|
|
3256
|
+
stream: utils.AnyStream | int | None = None,
|
|
3257
|
+
):
|
|
3258
|
+
"""
|
|
3259
|
+
Perform the specified distributed matrix multiplication computation
|
|
3260
|
+
:math:`\\mathcal{{F}}(\\alpha a @ b + \\beta c)`, where :math:`\\mathcal{{F}}`
|
|
3261
|
+
is the epilog. This function-form is a wrapper around the stateful :class:`Matmul`
|
|
3262
|
+
object APIs and is meant for *single* use (the user needs to perform just one matrix
|
|
3263
|
+
multiplication, for example), in which case there is no possibility of amortizing
|
|
3264
|
+
preparatory costs.
|
|
3265
|
+
|
|
3266
|
+
Detailed information on what's happening within this function can be obtained by passing
|
|
3267
|
+
in a :class:`logging.Logger` object to :class:`MatmulOptions` or by setting the
|
|
3268
|
+
appropriate options in the root logger object, which is used by default:
|
|
3269
|
+
|
|
3270
|
+
>>> import logging
|
|
3271
|
+
>>> logging.basicConfig(
|
|
3272
|
+
... level=logging.INFO,
|
|
3273
|
+
... format="%(asctime)s %(levelname)-8s %(message)s",
|
|
3274
|
+
... datefmt="%m-%d %H:%M:%S",
|
|
3275
|
+
... )
|
|
3276
|
+
|
|
3277
|
+
A user can select the desired logging level and, in general, take advantage of all of
|
|
3278
|
+
the functionality offered by the Python `logging` module.
|
|
3279
|
+
|
|
3280
|
+
Args:
|
|
3281
|
+
a: {a}
|
|
3282
|
+
|
|
3283
|
+
b: {b}
|
|
3284
|
+
|
|
3285
|
+
c: {c}
|
|
3286
|
+
|
|
3287
|
+
distributions: {distributions}
|
|
3288
|
+
|
|
3289
|
+
alpha: {alpha}
|
|
3290
|
+
|
|
3291
|
+
beta: {beta}
|
|
3292
|
+
|
|
3293
|
+
epilog: {epilog}
|
|
3294
|
+
|
|
3295
|
+
epilog_inputs: {epilog_inputs}
|
|
3296
|
+
|
|
3297
|
+
qualifiers: {qualifiers}
|
|
3298
|
+
|
|
3299
|
+
quantization_scales: {quantization_scales}
|
|
3300
|
+
|
|
3301
|
+
options: {options}
|
|
3302
|
+
|
|
3303
|
+
preferences: {preferences}
|
|
3304
|
+
|
|
3305
|
+
stream: {stream}
|
|
3306
|
+
|
|
3307
|
+
Returns:
|
|
3308
|
+
{result}
|
|
3309
|
+
|
|
3310
|
+
Semantics:
|
|
3311
|
+
{semantics}
|
|
3312
|
+
|
|
3313
|
+
Narrow-precision support:
|
|
3314
|
+
{narrow_precision}
|
|
3315
|
+
|
|
3316
|
+
.. seealso::
|
|
3317
|
+
:class:`Matmul`, :class:`MatmulOptions`, :class:`MatmulEpilog`,
|
|
3318
|
+
:class:`MatmulPlanPreferences`
|
|
3319
|
+
|
|
3320
|
+
Examples:
|
|
3321
|
+
|
|
3322
|
+
>>> import cupy as cp
|
|
3323
|
+
>>> import nvmath.distributed
|
|
3324
|
+
>>> from nvmath.distributed.distribution import Slab
|
|
3325
|
+
|
|
3326
|
+
Get process group used to initialize nvmath.distributed (for information on
|
|
3327
|
+
initializing ``nvmath.distributed``, you can refer to the documentation or to the
|
|
3328
|
+
Matmul examples in `nvmath/examples/distributed/linalg/advanced
|
|
3329
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/linalg/advanced/matmul>`_):
|
|
3330
|
+
|
|
3331
|
+
>>> process_group = nvmath.distributed.get_context().process_group
|
|
3332
|
+
|
|
3333
|
+
Get my process rank:
|
|
3334
|
+
|
|
3335
|
+
>>> rank = process_group.rank
|
|
3336
|
+
|
|
3337
|
+
Create three float32 ndarrays on the GPU:
|
|
3338
|
+
|
|
3339
|
+
>>> M, N, K = 128, 64, 256
|
|
3340
|
+
>>> a_shape = Slab.X.shape(rank, (M, K))
|
|
3341
|
+
>>> b_shape = Slab.Y.shape(rank, (K, N))
|
|
3342
|
+
>>> c_shape = Slab.X.shape(rank, (M, N))
|
|
3343
|
+
>>> device_id = nvmath.distributed.get_context().device_id
|
|
3344
|
+
>>> with cp.cuda.Device(device_id):
|
|
3345
|
+
... a = cp.asfortranarray(cp.random.rand(*a_shape, dtype=cp.float32))
|
|
3346
|
+
... b = cp.asfortranarray(cp.random.rand(*b_shape, dtype=cp.float32))
|
|
3347
|
+
... c = cp.asfortranarray(cp.random.rand(*c_shape, dtype=cp.float32))
|
|
3348
|
+
|
|
3349
|
+
Perform the operation :math:`\\alpha a @ b + \\beta c` using :func:`matmul`. The
|
|
3350
|
+
result `r` is also a CuPy float32 ndarray:
|
|
3351
|
+
|
|
3352
|
+
>>> distributions = [Slab.X, Slab.Y, Slab.X]
|
|
3353
|
+
>>> r = nvmath.distributed.linalg.advanced.matmul(
|
|
3354
|
+
... a, b, c, alpha=1.23, beta=0.74, distributions=distributions
|
|
3355
|
+
... )
|
|
3356
|
+
|
|
3357
|
+
Note that distributed matrix multiplication is inplace by default when ``c`` is
|
|
3358
|
+
provided, meaning that the result is stored in ``c``. Use the ``inplace=False``
|
|
3359
|
+
option to change this behavior (see options usage example below).
|
|
3360
|
+
|
|
3361
|
+
>>> assert r is c
|
|
3362
|
+
|
|
3363
|
+
Options can be provided to customize the operation:
|
|
3364
|
+
|
|
3365
|
+
>>> compute_type = (
|
|
3366
|
+
... nvmath.distributed.linalg.advanced.MatmulComputeType.COMPUTE_32F_FAST_TF32
|
|
3367
|
+
... )
|
|
3368
|
+
>>> o = nvmath.distributed.linalg.advanced.MatmulOptions(compute_type=compute_type)
|
|
3369
|
+
>>> r = nvmath.distributed.linalg.advanced.matmul(
|
|
3370
|
+
... a, b, distributions=distributions, options=o
|
|
3371
|
+
... )
|
|
3372
|
+
|
|
3373
|
+
See `MatmulOptions` for the complete list of available options.
|
|
3374
|
+
|
|
3375
|
+
The package current stream is used by default, but a stream can be explicitly
|
|
3376
|
+
provided to the Matmul operation. This can be done if the operands are computed on a
|
|
3377
|
+
different stream, for example:
|
|
3378
|
+
|
|
3379
|
+
>>> with cp.cuda.Device(device_id):
|
|
3380
|
+
... s = cp.cuda.Stream()
|
|
3381
|
+
... with s:
|
|
3382
|
+
... a = cp.asfortranarray(cp.random.rand(*a_shape))
|
|
3383
|
+
... b = cp.asfortranarray(cp.random.rand(*b_shape))
|
|
3384
|
+
>>> r = nvmath.distributed.linalg.advanced.matmul(
|
|
3385
|
+
... a, b, distributions=distributions, stream=s
|
|
3386
|
+
... )
|
|
3387
|
+
|
|
3388
|
+
The operation above runs on stream `s` and is ordered with respect to the input
|
|
3389
|
+
computation.
|
|
3390
|
+
|
|
3391
|
+
Create NumPy ndarrays on the CPU.
|
|
3392
|
+
|
|
3393
|
+
>>> import numpy as np
|
|
3394
|
+
>>> a = np.asfortranarray(np.random.rand(*a_shape))
|
|
3395
|
+
>>> b = np.asfortranarray(np.random.rand(*b_shape))
|
|
3396
|
+
|
|
3397
|
+
Provide the NumPy ndarrays to :func:`matmul`, with the result also being a NumPy
|
|
3398
|
+
ndarray:
|
|
3399
|
+
|
|
3400
|
+
>>> r = nvmath.distributed.linalg.advanced.matmul(a, b, distributions=distributions)
|
|
3401
|
+
|
|
3402
|
+
Notes:
|
|
3403
|
+
- This function is a convenience wrapper around :class:`Matmul` and is
|
|
3404
|
+
specifically meant for *single* use.
|
|
3405
|
+
|
|
3406
|
+
Further examples can be found in the `nvmath/distributed/examples/linalg/advanced/matmul
|
|
3407
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/linalg/advanced/matmul>`_
|
|
3408
|
+
directory.
|
|
3409
|
+
"""
|
|
3410
|
+
preferences = utils.check_or_create_options(
|
|
3411
|
+
_configuration.MatmulPlanPreferences, preferences, "Matrix multiplication plan preferences"
|
|
3412
|
+
)
|
|
3413
|
+
|
|
3414
|
+
with Matmul(
|
|
3415
|
+
a,
|
|
3416
|
+
b,
|
|
3417
|
+
c=c,
|
|
3418
|
+
distributions=distributions,
|
|
3419
|
+
alpha=alpha,
|
|
3420
|
+
beta=beta,
|
|
3421
|
+
qualifiers=qualifiers,
|
|
3422
|
+
options=options,
|
|
3423
|
+
stream=stream,
|
|
3424
|
+
quantization_scales=quantization_scales,
|
|
3425
|
+
) as mm:
|
|
3426
|
+
mm.plan(preferences=preferences, epilog=epilog, epilog_inputs=epilog_inputs, stream=stream)
|
|
3427
|
+
|
|
3428
|
+
r = mm.execute(stream=stream)
|
|
3429
|
+
|
|
3430
|
+
return r
|
|
3431
|
+
|
|
3432
|
+
|
|
3433
|
+
class CublasMpMemoryManager(memory.BaseCUDAMemoryManager):
|
|
3434
|
+
"""
|
|
3435
|
+
cuBLASMp memory allocator. Calls cublasMp.malloc()/cublasMp.free()
|
|
3436
|
+
which itself uses ncclMemAlloc/ncclMemFree, and (de)registers the
|
|
3437
|
+
buffers with NCCL subcommunicators associated with the given cuBLASMp
|
|
3438
|
+
process grids.
|
|
3439
|
+
"""
|
|
3440
|
+
|
|
3441
|
+
class SymmetricMemoryPointer(memory.MemoryPointer):
|
|
3442
|
+
def __init__(self, grids, ptr, size):
|
|
3443
|
+
super().__init__(ptr, size, finalizer=None)
|
|
3444
|
+
self.grids = grids
|
|
3445
|
+
|
|
3446
|
+
def free(self):
|
|
3447
|
+
"""**This is a collective call**"""
|
|
3448
|
+
for grid in self.grids[1:][::-1]:
|
|
3449
|
+
cublasMp.buffer_deregister(grid, self.device_ptr)
|
|
3450
|
+
cublasMp.free(self.grids[0], self.device_ptr)
|
|
3451
|
+
|
|
3452
|
+
def __init__(self, device_id: int, grids: list[int], logger: logging.Logger):
|
|
3453
|
+
"""
|
|
3454
|
+
Create CublasMpMemoryManager.
|
|
3455
|
+
|
|
3456
|
+
Args:
|
|
3457
|
+
device_id: The ID (int) of the device on which memory is to be allocated.
|
|
3458
|
+
|
|
3459
|
+
grids: List of pointers to cuBLASMp process grids. Memory allocated
|
|
3460
|
+
with this manager will be registered with the NCCL subcommunicators
|
|
3461
|
+
associated with these grids.
|
|
3462
|
+
|
|
3463
|
+
logger (logging.Logger): Python Logger object.
|
|
3464
|
+
"""
|
|
3465
|
+
self.device_id = device_id
|
|
3466
|
+
self.grids = grids
|
|
3467
|
+
self.logger = logger
|
|
3468
|
+
|
|
3469
|
+
def memalloc(self, size):
|
|
3470
|
+
"""**This is a collective call**"""
|
|
3471
|
+
assert self.grids, "Internal error"
|
|
3472
|
+
# Note: this is a blocking call (it calls ncclMemAlloc which doesn't take a stream).
|
|
3473
|
+
ptr = cublasMp.malloc(self.grids[0], size)
|
|
3474
|
+
for grid in self.grids[1:]:
|
|
3475
|
+
cublasMp.buffer_register(grid, ptr, size)
|
|
3476
|
+
|
|
3477
|
+
self.logger.debug(
|
|
3478
|
+
f"CublasMpMemoryManager (allocate memory): size = {size}, pointer = {ptr}, device_id = {self.device_id}"
|
|
3479
|
+
)
|
|
3480
|
+
|
|
3481
|
+
return CublasMpMemoryManager.SymmetricMemoryPointer(self.grids, ptr, size)
|
|
3482
|
+
|
|
3483
|
+
|
|
3484
|
+
class cuBLASMpProcessGridCache:
|
|
3485
|
+
def __init__(self):
|
|
3486
|
+
self.cache = {}
|
|
3487
|
+
self.cache_alt = {}
|
|
3488
|
+
self.device_id = None
|
|
3489
|
+
import threading
|
|
3490
|
+
|
|
3491
|
+
self.lock = threading.Lock()
|
|
3492
|
+
self.buf_registrations = defaultdict(int)
|
|
3493
|
+
|
|
3494
|
+
def get_library_process_grid(self, process_grid, device_id, nccl_comm, from_alt=False):
|
|
3495
|
+
"""**This is a collective call**. Caller must make sure to set device context."""
|
|
3496
|
+
with self.lock:
|
|
3497
|
+
if self.device_id is None:
|
|
3498
|
+
self.device_id = device_id
|
|
3499
|
+
else:
|
|
3500
|
+
assert self.device_id == device_id
|
|
3501
|
+
cache = self.cache if not from_alt else self.cache_alt
|
|
3502
|
+
if process_grid not in cache:
|
|
3503
|
+
process_grid_cpp = cublasMp.grid_create(
|
|
3504
|
+
process_grid.shape[0],
|
|
3505
|
+
process_grid.shape[1],
|
|
3506
|
+
cublasMp.GridLayout.COL_MAJOR
|
|
3507
|
+
if process_grid.layout == ProcessGrid.Layout.COL_MAJOR
|
|
3508
|
+
else cublasMp.GridLayout.ROW_MAJOR,
|
|
3509
|
+
nccl_comm.ptr,
|
|
3510
|
+
)
|
|
3511
|
+
cache[process_grid] = process_grid_cpp
|
|
3512
|
+
return process_grid_cpp
|
|
3513
|
+
return cache[process_grid]
|
|
3514
|
+
|
|
3515
|
+
def clear(self):
|
|
3516
|
+
"""This is a collective call."""
|
|
3517
|
+
with self.lock:
|
|
3518
|
+
if len(self.buf_registrations) > 0:
|
|
3519
|
+
import warnings
|
|
3520
|
+
|
|
3521
|
+
warnings.warn("There are active NCCL symmetric memory buffer registrations.")
|
|
3522
|
+
|
|
3523
|
+
if len(self.cache) == 0 and len(self.cache_alt) == 0:
|
|
3524
|
+
return
|
|
3525
|
+
with utils.device_ctx(self.device_id):
|
|
3526
|
+
for cache in (self.cache, self.cache_alt):
|
|
3527
|
+
for grid in cache.values():
|
|
3528
|
+
cublasMp.grid_destroy(grid)
|
|
3529
|
+
cache.clear()
|
|
3530
|
+
|
|
3531
|
+
def register_buffer_with_nccl(self, operand, operand_name, grids, logger, ignore_errors=True):
|
|
3532
|
+
"""This is a collective call"""
|
|
3533
|
+
# NOTE: This assumes the operand has dense and contiguous layout, which is currently
|
|
3534
|
+
# a requirement for distributed Matmul.
|
|
3535
|
+
registration_count = 0
|
|
3536
|
+
try:
|
|
3537
|
+
for grid in grids:
|
|
3538
|
+
# This is a collective call, and every rank needs to call with the same
|
|
3539
|
+
# buffer size. This cuBLASMp API is a no-op if called with the same grid,
|
|
3540
|
+
# pointer and size.
|
|
3541
|
+
cublasMp.buffer_register(grid, operand.data_ptr, operand.size * operand.itemsize)
|
|
3542
|
+
registration_count += 1
|
|
3543
|
+
# Keep track of how many Matmul objects have registered this buffer
|
|
3544
|
+
# with this grid, so that when it reaches 0, the buffer will be
|
|
3545
|
+
# deregistered.
|
|
3546
|
+
self.buf_registrations[(operand.data_ptr, grid)] += 1
|
|
3547
|
+
except nvmath.bindings.cublasMp.cuBLASMpError as e:
|
|
3548
|
+
if registration_count > 0:
|
|
3549
|
+
raise RuntimeError(
|
|
3550
|
+
"Fatal error: registration with NCCL symmetric memory was successful with some but not all process grids!"
|
|
3551
|
+
) from e
|
|
3552
|
+
msg = (
|
|
3553
|
+
f"Can't register operand {operand_name} buffer with NCCL symmetric memory. "
|
|
3554
|
+
"Check cuBLASMp logs for more information. Also, consider allocating the "
|
|
3555
|
+
"operand with a VMM-based allocator (e.g. nccl4py)."
|
|
3556
|
+
)
|
|
3557
|
+
if not ignore_errors:
|
|
3558
|
+
raise RuntimeError(msg) from e
|
|
3559
|
+
logger.info(msg)
|
|
3560
|
+
|
|
3561
|
+
def deregister_buffer_with_nccl(self, operand, grids):
|
|
3562
|
+
"""This is a collective call"""
|
|
3563
|
+
for grid in grids:
|
|
3564
|
+
key = (operand.data_ptr, grid)
|
|
3565
|
+
if key not in self.buf_registrations:
|
|
3566
|
+
continue
|
|
3567
|
+
self.buf_registrations[key] -= 1
|
|
3568
|
+
if self.buf_registrations[key] == 0:
|
|
3569
|
+
cublasMp.buffer_deregister(grid, operand.data_ptr)
|
|
3570
|
+
del self.buf_registrations[key]
|
|
3571
|
+
|
|
3572
|
+
|
|
3573
|
+
_grid_cache = cuBLASMpProcessGridCache()
|