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,2094 @@
|
|
|
1
|
+
# Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"Matmul",
|
|
7
|
+
"matmul",
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
import dataclasses
|
|
11
|
+
import logging
|
|
12
|
+
import math
|
|
13
|
+
from collections.abc import Generator, MutableSequence, Sequence
|
|
14
|
+
from typing import Any, Final, Literal, TypeAlias
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
from nvmath import memory
|
|
19
|
+
from nvmath._internal import templates
|
|
20
|
+
from nvmath._internal.utils import LoggerLike, get_addresses_of_elements
|
|
21
|
+
from nvmath.bindings import cublas
|
|
22
|
+
from nvmath.internal import formatters, tensor_wrapper, typemaps, utils
|
|
23
|
+
from nvmath.internal._layout import StridedLayout
|
|
24
|
+
from nvmath.internal.tensor_ifc_ndbuffer import NDBufferTensor
|
|
25
|
+
from nvmath.linalg._internal.batch import BatchTraits
|
|
26
|
+
from nvmath.linalg._internal.layout import BLASMatrixTraits, BLASMMTraitsView, InputMMTraits, check_extents, check_strides
|
|
27
|
+
from nvmath.linalg.advanced.matmulmod import SHARED_MM_DOCUMENTATION
|
|
28
|
+
from nvmath.linalg.generic._configuration import (
|
|
29
|
+
CACHED_LAYOUT_CHECKERS,
|
|
30
|
+
GeneralMatrixQualifier,
|
|
31
|
+
MatmulOptions,
|
|
32
|
+
MatrixQualifier,
|
|
33
|
+
matrix_qualifiers_dtype,
|
|
34
|
+
mm_layout_checker_getter,
|
|
35
|
+
select_blas_group_mm_function,
|
|
36
|
+
select_blas_mm_function,
|
|
37
|
+
vector_to_square,
|
|
38
|
+
)
|
|
39
|
+
from nvmath.linalg.generic._dtype import check_dtype
|
|
40
|
+
|
|
41
|
+
AnyTensor: TypeAlias = utils.AnyTensor
|
|
42
|
+
AnyStream: TypeAlias = utils.AnyStream
|
|
43
|
+
SideMode: TypeAlias = cublas.SideMode
|
|
44
|
+
FillMode: TypeAlias = cublas.FillMode
|
|
45
|
+
DiagType: TypeAlias = cublas.DiagType
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
|
|
49
|
+
class ExecutionCUDA(templates.ExecutionCUDA):
|
|
50
|
+
"""
|
|
51
|
+
A data class for providing GPU execution options.
|
|
52
|
+
|
|
53
|
+
Attributes:
|
|
54
|
+
device_id: CUDA device ordinal (only used if the operand resides on the CPU). The
|
|
55
|
+
default value is 0.
|
|
56
|
+
|
|
57
|
+
.. seealso::
|
|
58
|
+
:class:`ExecutionCPU`, :class:`Matmul`, :func:`matmul`
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
|
|
65
|
+
class ExecutionCPU(templates.ExecutionCPU):
|
|
66
|
+
"""
|
|
67
|
+
A data class for providing CPU execution options.
|
|
68
|
+
|
|
69
|
+
Attributes:
|
|
70
|
+
num_threads: The number of CPU threads used to execute the operation.
|
|
71
|
+
If not specified, defaults to the number of CPU cores available to the
|
|
72
|
+
process.
|
|
73
|
+
|
|
74
|
+
.. seealso::
|
|
75
|
+
:class:`ExecutionCUDA`, :class:`Matmul`, :func:`matmul`,
|
|
76
|
+
:func:`~nvmath.bindings.nvpl.blas.set_num_threads_local`
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class InvalidMatmulState(Exception):
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class _PrefixedLoggerAdapter(logging.LoggerAdapter):
|
|
87
|
+
def process(self, msg, kwargs):
|
|
88
|
+
return f"[{self.extra['prefix']}] {msg}", kwargs
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
GENERIC_MM_DOCUMENTATION = SHARED_MM_DOCUMENTATION.copy()
|
|
92
|
+
GENERIC_MM_DOCUMENTATION.update(
|
|
93
|
+
{
|
|
94
|
+
"a": SHARED_MM_DOCUMENTATION["a"].replace("tensor representing", "tensor or a sequence of tensors representing"),
|
|
95
|
+
#
|
|
96
|
+
"b": SHARED_MM_DOCUMENTATION["b"].replace("tensor representing", "tensor or a sequence of tensors representing"),
|
|
97
|
+
#
|
|
98
|
+
"c": SHARED_MM_DOCUMENTATION["c"].replace("tensor representing", "tensor or a sequence of tensors representing"),
|
|
99
|
+
#
|
|
100
|
+
"alpha": SHARED_MM_DOCUMENTATION["alpha"].replace("scale factor", "scale factor or a sequence of scale factors"),
|
|
101
|
+
#
|
|
102
|
+
"beta": SHARED_MM_DOCUMENTATION["beta"].replace("scale factor", "scale factor or a sequence of scale factors"),
|
|
103
|
+
#
|
|
104
|
+
"qualifiers": """\
|
|
105
|
+
If desired, specify the matrix qualifiers as a :class:`numpy.ndarray` of
|
|
106
|
+
:class:`~nvmath.linalg.generic.matrix_qualifiers_dtype` objects of length <= 3 corresponding to the operands `a`, `b`, and
|
|
107
|
+
`c`. By default, :class:`GeneralMatrixQualifier` is assumed for each tensor. See
|
|
108
|
+
:ref:`matrix-tensor-qualifiers` for the motivation behind qualifiers.""".replace("\n", " "),
|
|
109
|
+
#
|
|
110
|
+
"execution": """\
|
|
111
|
+
Specify execution space options for the Matmul as a :class:`ExecutionCUDA` or :class:`ExecutionCPU` object. If not specified,
|
|
112
|
+
the execution space will be selected to match operand's storage (in GPU or host memory), and the corresponding
|
|
113
|
+
:class:`ExecutionCUDA` or :class:`ExecutionCPU` object will be default-constructed.""".replace("\n", " "),
|
|
114
|
+
#
|
|
115
|
+
"options": """\
|
|
116
|
+
Specify options for the matrix multiplication as a :class:`MatmulOptions` object. Alternatively,
|
|
117
|
+
a `dict` containing the parameters for the ``MatmulOptions`` constructor can also be provided.
|
|
118
|
+
If not specified, the value will be set to the default-constructed ``MatmulOptions`` object.""".replace("\n", " "),
|
|
119
|
+
#
|
|
120
|
+
"result": """\
|
|
121
|
+
The result of the specified matrix multiplication, which remains on the same device and belongs to the
|
|
122
|
+
same package as the input operands.""".replace("\n", " "),
|
|
123
|
+
#
|
|
124
|
+
"semantics": """\
|
|
125
|
+
.. _semantics:
|
|
126
|
+
|
|
127
|
+
Inputs may be batched either **implicitly** and/or **explicitly**.
|
|
128
|
+
|
|
129
|
+
* An **implicitly-batched** input is a higher-dimensional :math:`N \\geq 3D` tensor.
|
|
130
|
+
|
|
131
|
+
* An **explicitly-batched** input is a Python sequence of tensors.
|
|
132
|
+
|
|
133
|
+
There may be only one explicitly-batched dimension, but there may be any number of implicitly-batched dimensions.
|
|
134
|
+
Explicitly-batched inputs are first broadcast to equal length along the explicitly-batched dimension,
|
|
135
|
+
then each explicit batch is treated as implicitly-batched inputs.
|
|
136
|
+
|
|
137
|
+
The semantics of the matrix multiplication follows :external:py:data:`numpy.matmul` semantics, with some restrictions.
|
|
138
|
+
|
|
139
|
+
* Broadcasting along implicitly-batched dimensions of `c` is not supported in this API, but may be supported in the
|
|
140
|
+
future. See the advanced API (:func:`nvmath.linalg.advanced.matmul`) for an API that supports broadcasting `c`.
|
|
141
|
+
|
|
142
|
+
In addition, the semantics for the fused matrix addition are described below:
|
|
143
|
+
|
|
144
|
+
* If arguments `a` and `b` are matrices, they are multiplied according to the rules of matrix multiplication.
|
|
145
|
+
* If argument `a` is 1-D, it is promoted to a matrix by prefixing ``1`` to its dimensions. After matrix
|
|
146
|
+
multiplication, the prefixed ``1`` is removed from the result's dimensions.
|
|
147
|
+
* If argument `b` is 1-D, it is promoted to a matrix by appending ``1`` to its dimensions. After matrix
|
|
148
|
+
multiplication, the appended ``1`` is removed from the result's dimensions.
|
|
149
|
+
* The operand for the matrix addition `c` must be the expected shape of the result of the matrix multiplication.
|
|
150
|
+
|
|
151
|
+
""".strip(),
|
|
152
|
+
#
|
|
153
|
+
"reset_operands_unchecked": utils._reset_operand_unchecked_docstring(True, version_added="1.0"),
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _resolve_execution(
|
|
159
|
+
execution: ExecutionCPU | ExecutionCUDA | str | None,
|
|
160
|
+
operands_device_id: int | Literal["cpu"],
|
|
161
|
+
) -> ExecutionCPU | ExecutionCUDA:
|
|
162
|
+
"""Normalize the user-supplied ``execution`` argument to a concrete instance.
|
|
163
|
+
|
|
164
|
+
``execution`` may be ``None``, the literal strings ``"cuda"``/``"cpu"``, or an
|
|
165
|
+
already-instantiated :class:`ExecutionCPU`/:class:`ExecutionCUDA`. The resolved
|
|
166
|
+
value's ``device_id`` is aligned with ``operands_device_id`` when both sides
|
|
167
|
+
pin a CUDA device.
|
|
168
|
+
"""
|
|
169
|
+
match execution, operands_device_id:
|
|
170
|
+
case (None | "cuda", int()):
|
|
171
|
+
return ExecutionCUDA(device_id=operands_device_id)
|
|
172
|
+
case ("cuda", "cpu"):
|
|
173
|
+
return ExecutionCUDA()
|
|
174
|
+
case (None, "cpu") | ("cpu", _):
|
|
175
|
+
return ExecutionCPU()
|
|
176
|
+
case (ExecutionCUDA(), int()):
|
|
177
|
+
return dataclasses.replace(execution, device_id=operands_device_id)
|
|
178
|
+
case (ExecutionCPU(), _):
|
|
179
|
+
return execution
|
|
180
|
+
case (ExecutionCUDA(), "cpu"):
|
|
181
|
+
return execution
|
|
182
|
+
case _:
|
|
183
|
+
raise ValueError("Matmul.execution must be one of ExecutionCUDA, ExecutionCPU, None, 'cuda', or 'cpu'.")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _resolve_internal_operand_package(execution_name: str, operands_package: str) -> str:
|
|
187
|
+
"""Map ``operands_package`` to the package name used internally for buffers/streams.
|
|
188
|
+
|
|
189
|
+
On CUDA execution, ``numpy`` operands are routed through ``cuda``; on CPU execution,
|
|
190
|
+
``cupy`` operands are routed through ``cupy_host``. All other packages pass through.
|
|
191
|
+
"""
|
|
192
|
+
if execution_name == "cuda":
|
|
193
|
+
return operands_package if operands_package != "numpy" else "cuda"
|
|
194
|
+
return operands_package if operands_package != "cupy" else "cupy_host"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _get_or_create_stream_maybe(
|
|
198
|
+
execution: ExecutionCPU | ExecutionCUDA,
|
|
199
|
+
operands_device_id: int | Literal["cpu"],
|
|
200
|
+
operands_package: str,
|
|
201
|
+
internal_op_package: str,
|
|
202
|
+
stream: utils.AnyStream,
|
|
203
|
+
) -> tuple[utils.StreamHolder | None, utils.StreamHolder | None]:
|
|
204
|
+
"""Return ``(exec_stream_holder, operand_stream_holder)`` for the given spaces.
|
|
205
|
+
|
|
206
|
+
The first holder is used for execution-space work (compute, workspace, output
|
|
207
|
+
buffers). The second is used for input-space data movement. When both are
|
|
208
|
+
non-``None`` and the execution and input spaces coincide, they are the same
|
|
209
|
+
holder.
|
|
210
|
+
"""
|
|
211
|
+
if execution.name == "cuda":
|
|
212
|
+
h = utils.get_or_create_stream(execution.device_id, stream, internal_op_package)
|
|
213
|
+
return h, h
|
|
214
|
+
if isinstance(operands_device_id, int):
|
|
215
|
+
return None, utils.get_or_create_stream(operands_device_id, stream, operands_package)
|
|
216
|
+
return None, None
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _realloc_if_needed_and_copy_to_mirror(
|
|
220
|
+
src_holder: utils.TensorHolder,
|
|
221
|
+
dest_holder: utils.TensorHolder,
|
|
222
|
+
dest_device_id: int | Literal["cpu"],
|
|
223
|
+
stream_holder: utils.StreamHolder | None,
|
|
224
|
+
) -> None:
|
|
225
|
+
"""Copy a source operand into its mirror on ``dest_device_id``.
|
|
226
|
+
|
|
227
|
+
If ``dest_holder.tensor`` is still alive, do an in-place copy into it. Otherwise
|
|
228
|
+
(typically after :meth:`release_operands`), allocate a new buffer via
|
|
229
|
+
``src_holder.to()`` and bind it to ``dest_holder.tensor``.
|
|
230
|
+
"""
|
|
231
|
+
if dest_holder.tensor is not None:
|
|
232
|
+
dest_holder.copy_(src_holder, stream_holder=stream_holder)
|
|
233
|
+
return
|
|
234
|
+
dest_holder.tensor = src_holder.to(dest_device_id, stream_holder).tensor
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class _ImplicitlyBatchedMatmul:
|
|
238
|
+
"""A Matmul object that supports implicitly batched operands.
|
|
239
|
+
|
|
240
|
+
This class has the same public API as :class:`Matmul`, except that the arguments may
|
|
241
|
+
only be implicitly batched.
|
|
242
|
+
|
|
243
|
+
.. important::
|
|
244
|
+
|
|
245
|
+
Explicit batches are always defined as a Python Sequence of the same type and
|
|
246
|
+
implicit batches are always leading dimension of tensors. Otherwise, broadcasting
|
|
247
|
+
behavior would be ambiguous. For example, if some arguments are sequences of tensors
|
|
248
|
+
and other arguments are non-sequence tensors with batch dimensions.
|
|
249
|
+
|
|
250
|
+
.. seealso:: :func:`_zip_broadcast`, :class:`Matmul`, :class:`_ExplicitlyBatchedMatmul`
|
|
251
|
+
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
__slots__ = (
|
|
255
|
+
"_allocator",
|
|
256
|
+
"_batch_traits",
|
|
257
|
+
"_blocking",
|
|
258
|
+
"_call_prologue",
|
|
259
|
+
"_execution_device_id",
|
|
260
|
+
"_function",
|
|
261
|
+
"_input_traits",
|
|
262
|
+
"_internal_op_package",
|
|
263
|
+
"_logger",
|
|
264
|
+
"_memspace_eq_exespace",
|
|
265
|
+
"_mm_traits",
|
|
266
|
+
"_operands_backup",
|
|
267
|
+
"_operands_device_id",
|
|
268
|
+
"_operands_package",
|
|
269
|
+
"_operands",
|
|
270
|
+
"_qualifiers",
|
|
271
|
+
"_result_class",
|
|
272
|
+
"a_dtype_name",
|
|
273
|
+
"a_dtype",
|
|
274
|
+
"alpha",
|
|
275
|
+
"b_dtype_name",
|
|
276
|
+
"b_dtype",
|
|
277
|
+
"beta",
|
|
278
|
+
"c_dtype_name",
|
|
279
|
+
"c_dtype",
|
|
280
|
+
"execution",
|
|
281
|
+
"is_complex",
|
|
282
|
+
"lazy_conjugation",
|
|
283
|
+
"num_operands",
|
|
284
|
+
"operand_extents",
|
|
285
|
+
"operand_strides",
|
|
286
|
+
"options",
|
|
287
|
+
"scale_type_name",
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
options: Final[MatmulOptions]
|
|
291
|
+
_allocator: Final[memory.BaseCUDAMemoryManager | memory.BaseCUDAMemoryManagerAsync | None]
|
|
292
|
+
_blocking: Final[bool]
|
|
293
|
+
_logger: Final[LoggerLike]
|
|
294
|
+
|
|
295
|
+
# Metadata related to execution space
|
|
296
|
+
execution: Final[ExecutionCPU | ExecutionCUDA]
|
|
297
|
+
"""A class which describes the execution space parameters."""
|
|
298
|
+
_internal_op_package: Final[str]
|
|
299
|
+
"""The package of the operands in the execution space."""
|
|
300
|
+
_operands: MutableSequence[utils.TensorHolder]
|
|
301
|
+
"""A copy of the operands in execution space."""
|
|
302
|
+
_result_class: Final[type[utils.TensorHolder]]
|
|
303
|
+
"""The type of TensorHolder to use for the execution space result."""
|
|
304
|
+
|
|
305
|
+
# Metadata about the input/output tensors
|
|
306
|
+
_operands_backup: MutableSequence[utils.TensorHolder | None]
|
|
307
|
+
"""A reference to original operands in their input space."""
|
|
308
|
+
_operands_device_id: Final[int | Literal["cpu"]]
|
|
309
|
+
"""The device_id of the input space."""
|
|
310
|
+
_execution_device_id: Final[int | Literal["cpu"]]
|
|
311
|
+
"""The device_id of the execution space (``"cpu"`` for CPU execution)."""
|
|
312
|
+
_memspace_eq_exespace: Final[bool]
|
|
313
|
+
"""Cached ``_operands_device_id == _execution_device_id``."""
|
|
314
|
+
_operands_package: Final[str]
|
|
315
|
+
"""The package of the operands in the input space."""
|
|
316
|
+
|
|
317
|
+
_call_prologue: Final[str]
|
|
318
|
+
"""Stores a message for logging about blocking behavior"""
|
|
319
|
+
|
|
320
|
+
_input_traits: Final[InputMMTraits]
|
|
321
|
+
_batch_traits: Final[tuple[BatchTraits, BatchTraits, BatchTraits, BatchTraits]]
|
|
322
|
+
_qualifiers: Final[MatrixQualifier]
|
|
323
|
+
|
|
324
|
+
def __init__(
|
|
325
|
+
self,
|
|
326
|
+
a: AnyTensor,
|
|
327
|
+
b: AnyTensor,
|
|
328
|
+
/,
|
|
329
|
+
c: AnyTensor | None = None,
|
|
330
|
+
*,
|
|
331
|
+
alpha: float | complex | None = None,
|
|
332
|
+
beta: float | complex | None = None,
|
|
333
|
+
qualifiers: MatrixQualifier | None = None,
|
|
334
|
+
options: MatmulOptions,
|
|
335
|
+
execution: ExecutionCPU | ExecutionCUDA,
|
|
336
|
+
exec_stream_holder: utils.StreamHolder | None,
|
|
337
|
+
operand_stream_holder: utils.StreamHolder | None,
|
|
338
|
+
_allow_mixed_batch_order: bool = False,
|
|
339
|
+
_logger: LoggerLike | None = None,
|
|
340
|
+
):
|
|
341
|
+
self.options = options
|
|
342
|
+
self._logger = _logger if _logger is not None else self.options.logger
|
|
343
|
+
self.execution = execution
|
|
344
|
+
|
|
345
|
+
assert not (c is None and options.inplace), "Internal Error: Matmul should have rejected inplace=True with c=None."
|
|
346
|
+
|
|
347
|
+
a = tensor_wrapper.wrap_operand(a)
|
|
348
|
+
check_dtype(a.dtype, "A")
|
|
349
|
+
check_extents(a.shape, "A")
|
|
350
|
+
check_strides(a.strides, "A")
|
|
351
|
+
|
|
352
|
+
b = tensor_wrapper.wrap_operand(b)
|
|
353
|
+
check_dtype(b.dtype, "B")
|
|
354
|
+
check_extents(b.shape, "B")
|
|
355
|
+
check_strides(b.strides, "B")
|
|
356
|
+
operands = [a, b]
|
|
357
|
+
|
|
358
|
+
self.num_operands = 2
|
|
359
|
+
if c is not None:
|
|
360
|
+
self.num_operands = 3
|
|
361
|
+
c = tensor_wrapper.wrap_operand(c)
|
|
362
|
+
check_dtype(c.dtype, "C")
|
|
363
|
+
check_extents(c.shape, "C")
|
|
364
|
+
check_strides(c.strides, "C")
|
|
365
|
+
operands.append(c)
|
|
366
|
+
|
|
367
|
+
self._operands_device_id = utils.get_operands_device_id(operands)
|
|
368
|
+
self._execution_device_id = getattr(self.execution, "device_id", "cpu")
|
|
369
|
+
self._memspace_eq_exespace = self._operands_device_id == self._execution_device_id
|
|
370
|
+
self._operands_package = utils.get_operands_package(operands)
|
|
371
|
+
self._internal_op_package = _resolve_internal_operand_package(self.execution.name, self._operands_package)
|
|
372
|
+
|
|
373
|
+
operands_backup: list[utils.TensorHolder | None] = [None] * len(operands)
|
|
374
|
+
for i in range(len(operands)):
|
|
375
|
+
# Copy the operand to execution_space's device if needed.
|
|
376
|
+
operands[i], operands_backup[i] = templates.copy_operand_perhaps(
|
|
377
|
+
None,
|
|
378
|
+
operands[i],
|
|
379
|
+
operand_stream_holder,
|
|
380
|
+
self._execution_device_id,
|
|
381
|
+
self._operands_device_id,
|
|
382
|
+
)
|
|
383
|
+
self._operands = operands
|
|
384
|
+
self._operands_backup = operands_backup
|
|
385
|
+
|
|
386
|
+
# The result's package and device.
|
|
387
|
+
self._result_class = self._operands[0].__class__
|
|
388
|
+
|
|
389
|
+
# Set blocking or non-blocking behavior.
|
|
390
|
+
self._blocking = self.options.blocking != "auto" or self._operands_device_id == "cpu" or self.execution.name == "cpu"
|
|
391
|
+
if self._blocking:
|
|
392
|
+
call_prologue = "This call is blocking and will return only after the operation is complete."
|
|
393
|
+
else:
|
|
394
|
+
call_prologue = (
|
|
395
|
+
"This call is non-blocking and will return immediately after the operation is launched on the device."
|
|
396
|
+
)
|
|
397
|
+
self._call_prologue = call_prologue
|
|
398
|
+
|
|
399
|
+
# Set memory allocator.
|
|
400
|
+
allocator: memory.BaseCUDAMemoryManager | memory.BaseCUDAMemoryManagerAsync | None
|
|
401
|
+
match self.execution:
|
|
402
|
+
case ExecutionCUDA():
|
|
403
|
+
allocator = (
|
|
404
|
+
memory._MEMORY_MANAGER[self._internal_op_package](self.execution.device_id, self._logger)
|
|
405
|
+
if self.options.allocator is None
|
|
406
|
+
else self.options.allocator
|
|
407
|
+
)
|
|
408
|
+
case ExecutionCPU() | _:
|
|
409
|
+
allocator = None # currently, the nvpl/fftw does not support custom workspace allocation
|
|
410
|
+
self._allocator = allocator
|
|
411
|
+
|
|
412
|
+
self._logger.info(f"The data type of operand A is '{a.dtype}', and that of operand B is '{b.dtype}'.")
|
|
413
|
+
if c is not None:
|
|
414
|
+
self._logger.info(f"The data type of operand C is '{c.dtype}'.")
|
|
415
|
+
|
|
416
|
+
if self.options.inplace:
|
|
417
|
+
self._logger.info("The operation will be performed inplace with operand C.")
|
|
418
|
+
assert c is not None # guaranteed by the inplace-with-c assertion above
|
|
419
|
+
|
|
420
|
+
# Inplace writes back into C, so the C layout must be unique.
|
|
421
|
+
# Otherwise distinct logical indices alias the same memory cell,
|
|
422
|
+
# producing overlapping writes / undefined behavior.
|
|
423
|
+
if not StridedLayout(c.shape, c.strides, itemsize=1).is_unique:
|
|
424
|
+
raise ValueError(
|
|
425
|
+
f"The layout for C with shape = {c.shape} and strides = {c.strides} is not "
|
|
426
|
+
f"supported for inplace operations: the index-to-memory mapping is not "
|
|
427
|
+
f"injective, which would cause overlapping writes into the same memory."
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
if c is not None and beta is None:
|
|
431
|
+
raise ValueError("A value for beta must be provided if operand C is provided.")
|
|
432
|
+
|
|
433
|
+
assert self.num_operands == 2 or self.num_operands == 3, "Internal Error."
|
|
434
|
+
|
|
435
|
+
if a.dtype != b.dtype or (c is not None and a.dtype != c.dtype):
|
|
436
|
+
raise ValueError(
|
|
437
|
+
"Unsupported combination of dtypes. "
|
|
438
|
+
f"A ({a.dtype}), B ({b.dtype}), and C ({getattr(c, 'dtype', None)}) must all have the same dtype."
|
|
439
|
+
)
|
|
440
|
+
# Determine the data types for a and b.
|
|
441
|
+
self.a_dtype = typemaps.NAME_TO_DATA_TYPE[a.dtype]
|
|
442
|
+
self.b_dtype = typemaps.NAME_TO_DATA_TYPE[b.dtype]
|
|
443
|
+
self.a_dtype_name = a.dtype
|
|
444
|
+
self.b_dtype_name = b.dtype
|
|
445
|
+
|
|
446
|
+
self.is_complex = "complex" in self.a_dtype_name or "complex" in self.b_dtype_name
|
|
447
|
+
|
|
448
|
+
# Determine the data types for c.
|
|
449
|
+
if c is None:
|
|
450
|
+
self.c_dtype = self.a_dtype
|
|
451
|
+
else:
|
|
452
|
+
self.c_dtype = typemaps.NAME_TO_DATA_TYPE[c.dtype]
|
|
453
|
+
self.c_dtype_name = typemaps.DATA_TYPE_TO_NAME[self.c_dtype]
|
|
454
|
+
|
|
455
|
+
self._logger.info(f"The data type for the result D is '{self.c_dtype_name}'.")
|
|
456
|
+
|
|
457
|
+
self.scale_type_name = self.a_dtype_name
|
|
458
|
+
|
|
459
|
+
# Set alpha and beta.
|
|
460
|
+
self.alpha = np.zeros((1,), dtype=self.scale_type_name)
|
|
461
|
+
try:
|
|
462
|
+
self.alpha[0] = alpha if alpha is not None else 1
|
|
463
|
+
except (ValueError, TypeError) as e:
|
|
464
|
+
raise ValueError(f"The value provided for alpha {alpha} is not convertible to dtype '{self.alpha.dtype}'.") from e
|
|
465
|
+
|
|
466
|
+
self.beta = np.zeros((1,), dtype=self.scale_type_name)
|
|
467
|
+
if beta is not None and c is None:
|
|
468
|
+
self._logger.warning(f"Matmul: The provided beta value {beta} is ignored since operand C is not specified.")
|
|
469
|
+
try:
|
|
470
|
+
self.beta[0] = beta if beta is not None and c is not None else 0
|
|
471
|
+
except (ValueError, TypeError) as e:
|
|
472
|
+
raise ValueError(f"The value provided for beta {beta} is not convertible to dtype '{self.beta.dtype}'.") from e
|
|
473
|
+
|
|
474
|
+
if qualifiers is None:
|
|
475
|
+
new_qualifiers = np.empty(3, dtype=matrix_qualifiers_dtype)
|
|
476
|
+
new_qualifiers[:] = GeneralMatrixQualifier.create()
|
|
477
|
+
else:
|
|
478
|
+
if not ((len(qualifiers) == 3) or (len(qualifiers) == 2 and c is None)):
|
|
479
|
+
raise ValueError("The number of MatrixQualifiers must match the number of operands.")
|
|
480
|
+
new_qualifiers = np.empty(3, dtype=matrix_qualifiers_dtype)
|
|
481
|
+
new_qualifiers[:2] = qualifiers[:2]
|
|
482
|
+
new_qualifiers[2] = GeneralMatrixQualifier.create() if len(qualifiers) < 3 else qualifiers[2]
|
|
483
|
+
self._qualifiers = new_qualifiers
|
|
484
|
+
self._logger.info(
|
|
485
|
+
f"The matrix multiplication qualifiers are:\n"
|
|
486
|
+
f" A = {GeneralMatrixQualifier.to_string(self._qualifiers[0])}\n"
|
|
487
|
+
f" B = {GeneralMatrixQualifier.to_string(self._qualifiers[1])}\n"
|
|
488
|
+
f" C = {GeneralMatrixQualifier.to_string(self._qualifiers[2])}"
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
# Set qualifiers based on torch lazy conjugation flag if not provided.
|
|
492
|
+
self._qualifiers[0]["conjugate"] = self._qualifiers[0]["conjugate"] ^ self._operands[0].is_conjugate
|
|
493
|
+
self._qualifiers[1]["conjugate"] = self._qualifiers[1]["conjugate"] ^ self._operands[1].is_conjugate
|
|
494
|
+
self.lazy_conjugation = (self._operands[0].is_conjugate, self._operands[1].is_conjugate, False)
|
|
495
|
+
if c is not None:
|
|
496
|
+
self._qualifiers[2]["conjugate"] = self._qualifiers[2]["conjugate"] ^ self._operands[2].is_conjugate
|
|
497
|
+
if self._qualifiers[2]["conjugate"]:
|
|
498
|
+
raise ValueError("The conjugate flag is currently not supported for operand C.")
|
|
499
|
+
|
|
500
|
+
# Capture operand extents and strides for consistency check when resetting operands.
|
|
501
|
+
self.operand_extents = tuple(o.shape for o in self._operands)
|
|
502
|
+
self.operand_strides = tuple(o.strides for o in self._operands)
|
|
503
|
+
|
|
504
|
+
# Create operand layouts.
|
|
505
|
+
a_layout = BLASMatrixTraits(
|
|
506
|
+
self.a_dtype,
|
|
507
|
+
*vector_to_square(
|
|
508
|
+
self._operands[0].shape[-2:],
|
|
509
|
+
self._operands[0].strides[-2:],
|
|
510
|
+
self._qualifiers[0],
|
|
511
|
+
),
|
|
512
|
+
is_conjugate=bool(self._qualifiers[0]["conjugate"]),
|
|
513
|
+
is_transpose=False,
|
|
514
|
+
is_lower=self._qualifiers[0]["uplo"] == FillMode.LOWER,
|
|
515
|
+
)
|
|
516
|
+
self._logger.info("Operand A has traits of %s", a_layout)
|
|
517
|
+
b_layout = BLASMatrixTraits(
|
|
518
|
+
self.b_dtype,
|
|
519
|
+
*vector_to_square(
|
|
520
|
+
self._operands[1].shape[-2:],
|
|
521
|
+
self._operands[1].strides[-2:],
|
|
522
|
+
self._qualifiers[1],
|
|
523
|
+
),
|
|
524
|
+
is_conjugate=bool(self._qualifiers[1]["conjugate"]),
|
|
525
|
+
is_transpose=False,
|
|
526
|
+
is_lower=self._qualifiers[1]["uplo"] == FillMode.LOWER,
|
|
527
|
+
)
|
|
528
|
+
self._logger.info("Operand B has traits of %s", b_layout)
|
|
529
|
+
c_layout = (
|
|
530
|
+
None
|
|
531
|
+
if c is None
|
|
532
|
+
else BLASMatrixTraits(
|
|
533
|
+
self.c_dtype,
|
|
534
|
+
*vector_to_square(
|
|
535
|
+
self._operands[2].shape[-2:],
|
|
536
|
+
self._operands[2].strides[-2:],
|
|
537
|
+
self._qualifiers[2],
|
|
538
|
+
),
|
|
539
|
+
is_conjugate=bool(self._qualifiers[2]["conjugate"]),
|
|
540
|
+
is_transpose=False,
|
|
541
|
+
is_lower=self._qualifiers[2]["uplo"] == FillMode.LOWER,
|
|
542
|
+
)
|
|
543
|
+
)
|
|
544
|
+
self._logger.info("Operand C has traits of %s", c_layout)
|
|
545
|
+
|
|
546
|
+
# Get the operation traits.
|
|
547
|
+
self._input_traits = InputMMTraits.from_layouts(a_layout, b_layout, c_layout, self.options.inplace, self._logger)
|
|
548
|
+
|
|
549
|
+
a_batch = BatchTraits.from_full_shape_and_strides(
|
|
550
|
+
self._operands[0].shape,
|
|
551
|
+
self._operands[0].strides,
|
|
552
|
+
num_trailing_dims=2,
|
|
553
|
+
overlap_allowed=True,
|
|
554
|
+
)
|
|
555
|
+
b_batch = BatchTraits.from_full_shape_and_strides(
|
|
556
|
+
self._operands[1].shape,
|
|
557
|
+
self._operands[1].strides,
|
|
558
|
+
num_trailing_dims=2,
|
|
559
|
+
overlap_allowed=True,
|
|
560
|
+
)
|
|
561
|
+
c_batch = (
|
|
562
|
+
BatchTraits(shape=(), strides=())
|
|
563
|
+
if c is None
|
|
564
|
+
else BatchTraits.from_full_shape_and_strides(
|
|
565
|
+
self._operands[2].shape,
|
|
566
|
+
self._operands[2].strides,
|
|
567
|
+
num_trailing_dims=2,
|
|
568
|
+
overlap_allowed=False,
|
|
569
|
+
)
|
|
570
|
+
)
|
|
571
|
+
# BatchTraits overloads the * operator with a batch combination behavior
|
|
572
|
+
abc_batch, axis_order = a_batch.combine(
|
|
573
|
+
b_batch.combine(c_batch, allow_mixed_axis_order=_allow_mixed_batch_order),
|
|
574
|
+
allow_mixed_axis_order=_allow_mixed_batch_order,
|
|
575
|
+
)
|
|
576
|
+
d_batch = (
|
|
577
|
+
c_batch
|
|
578
|
+
if self.options.inplace
|
|
579
|
+
else BatchTraits.from_batch_shape_and_size(
|
|
580
|
+
shape=abc_batch,
|
|
581
|
+
axis_order=axis_order,
|
|
582
|
+
batch_stride=math.prod(self._input_traits.d_layout_traits.shape),
|
|
583
|
+
)
|
|
584
|
+
)
|
|
585
|
+
self._logger.info("Operand A has batch traits of %s.", a_batch)
|
|
586
|
+
self._logger.info("Operand B has batch traits of %s.", b_batch)
|
|
587
|
+
self._logger.info("Operand C has batch traits of %s.", c_batch)
|
|
588
|
+
self._logger.info("Result D has batch traits of %s.", d_batch)
|
|
589
|
+
self._batch_traits = (a_batch, b_batch, c_batch, d_batch)
|
|
590
|
+
|
|
591
|
+
def _get_or_create_stream_maybe(
|
|
592
|
+
self, stream: utils.AnyStream
|
|
593
|
+
) -> tuple[utils.StreamHolder | None, utils.StreamHolder | None]:
|
|
594
|
+
return _get_or_create_stream_maybe(
|
|
595
|
+
self.execution,
|
|
596
|
+
self._operands_device_id,
|
|
597
|
+
self._operands_package,
|
|
598
|
+
self._internal_op_package,
|
|
599
|
+
stream,
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
def plan(self, *, stream: AnyStream | int | None = None, log_info=False) -> None:
|
|
603
|
+
# NOTE: timing context called from Matmul.plan to avoid redundancy
|
|
604
|
+
mm_layout_checker = mm_layout_checker_getter(self._qualifiers)
|
|
605
|
+
|
|
606
|
+
mm_traits = BLASMMTraitsView.from_input_traits(
|
|
607
|
+
self._input_traits,
|
|
608
|
+
mm_layout_checker,
|
|
609
|
+
self._logger,
|
|
610
|
+
lookup_table_table=CACHED_LAYOUT_CHECKERS,
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
self._mm_traits = mm_traits
|
|
614
|
+
|
|
615
|
+
self._function, function_name = select_blas_mm_function(
|
|
616
|
+
(*self._batch_traits[:2], self._batch_traits[3]),
|
|
617
|
+
mm_traits,
|
|
618
|
+
self._qualifiers,
|
|
619
|
+
self._logger,
|
|
620
|
+
self.execution,
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
if log_info:
|
|
624
|
+
flop_count = 2 * mm_traits.M * mm_traits.N * mm_traits.K
|
|
625
|
+
|
|
626
|
+
self._logger.info(f"The plan found 1 suitable algorithm named {function_name}.")
|
|
627
|
+
self._logger.info(f"The base matrix multiplication FLOP count is {formatters.FLOPSStr(flop_count, 'FLOP')}.")
|
|
628
|
+
|
|
629
|
+
def _check_and_set_operand(
|
|
630
|
+
self,
|
|
631
|
+
new_operand: utils.TensorHolder,
|
|
632
|
+
operand_name: str,
|
|
633
|
+
stream_holder: utils.StreamHolder | None,
|
|
634
|
+
*,
|
|
635
|
+
operand_index: int,
|
|
636
|
+
dtype: str | None = None,
|
|
637
|
+
extents: Sequence[int] | None = None,
|
|
638
|
+
strides: Sequence[int] | None = None,
|
|
639
|
+
):
|
|
640
|
+
"""
|
|
641
|
+
Check to make sure that the provided operand is consistent with the one it's
|
|
642
|
+
updating, and update it.
|
|
643
|
+
"""
|
|
644
|
+
# Make sure that the data type and extents match.
|
|
645
|
+
utils.check_attribute_match(dtype, new_operand.dtype, "data type")
|
|
646
|
+
utils.check_attribute_match(extents, new_operand.shape, "extents")
|
|
647
|
+
|
|
648
|
+
# Package must be the same to preserve stream ordering
|
|
649
|
+
if self._operands_package != new_operand.name:
|
|
650
|
+
raise TypeError(
|
|
651
|
+
f"Library package mismatch: The operand {operand_name} must from the same package ({new_operand.name}) "
|
|
652
|
+
f"as the original operand ({self._operands_package})."
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
if self._operands_device_id != new_operand.device_id:
|
|
656
|
+
raise ValueError(
|
|
657
|
+
f"The operand {operand_name} must be on the same device ({new_operand.device_id}) as the original operand "
|
|
658
|
+
f"({self._operands_device_id})."
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
# Conjugate flag of the provided operands must match the original qualifiers
|
|
662
|
+
if self.lazy_conjugation[operand_index] != new_operand.is_conjugate:
|
|
663
|
+
raise ValueError(f"The provided operand {operand_name} has different conjugate flag than the original operand")
|
|
664
|
+
|
|
665
|
+
# Wrappers in self._operands / self._operands_backup are preserved for the
|
|
666
|
+
# lifetime of this object (release_operands clears only their .tensor); rebind
|
|
667
|
+
# in same-space and use the realloc-or-copy helper in cross-space, mirroring
|
|
668
|
+
# the unchecked path so both reset flavors maintain that invariant.
|
|
669
|
+
if self._memspace_eq_exespace:
|
|
670
|
+
self._operands[operand_index].tensor = new_operand.tensor
|
|
671
|
+
else:
|
|
672
|
+
_realloc_if_needed_and_copy_to_mirror(
|
|
673
|
+
new_operand,
|
|
674
|
+
self._operands[operand_index],
|
|
675
|
+
self._execution_device_id,
|
|
676
|
+
stream_holder,
|
|
677
|
+
)
|
|
678
|
+
self._operands_backup[operand_index] = new_operand
|
|
679
|
+
|
|
680
|
+
# After the rebind/copy above, check that the strides of the buffer the matmul
|
|
681
|
+
# will actually read (the user's tensor in same-space, the mirror in cross-space)
|
|
682
|
+
# still match what was planned for at construction.
|
|
683
|
+
# FIXME: a stride mismatch here leaves _operands[operand_index] already mutated;
|
|
684
|
+
# if the caller catches this they can observe a half-applied reset.
|
|
685
|
+
utils.check_attribute_match(strides, self._operands[operand_index].strides, "strides")
|
|
686
|
+
|
|
687
|
+
self._logger.info(f"Operand '{operand_name}' has been reset to the new value.")
|
|
688
|
+
|
|
689
|
+
return
|
|
690
|
+
|
|
691
|
+
def reset_operands(
|
|
692
|
+
self,
|
|
693
|
+
*,
|
|
694
|
+
a=None,
|
|
695
|
+
b=None,
|
|
696
|
+
c=None,
|
|
697
|
+
alpha=None,
|
|
698
|
+
beta=None,
|
|
699
|
+
stream: AnyStream | int | None = None,
|
|
700
|
+
_operands_released=True,
|
|
701
|
+
):
|
|
702
|
+
if c is not None and self.num_operands == 2:
|
|
703
|
+
raise ValueError(
|
|
704
|
+
"The matrix multiplication problem specification does not include operand C, so it cannot be reset."
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
# If operands have been released, all required operands must be provided
|
|
708
|
+
if _operands_released:
|
|
709
|
+
all_provided = a is not None and b is not None and (self.num_operands != 3 or c is not None)
|
|
710
|
+
if not all_provided:
|
|
711
|
+
raise ValueError(
|
|
712
|
+
"After release_operands(), all required operands must be provided to reset_operands(). "
|
|
713
|
+
f"Required: a, b{', c' if self.num_operands == 3 else ''}"
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
# Update alpha.
|
|
717
|
+
if alpha is not None:
|
|
718
|
+
try:
|
|
719
|
+
self.alpha[0] = alpha
|
|
720
|
+
except (ValueError, TypeError) as e:
|
|
721
|
+
raise ValueError(
|
|
722
|
+
f"The value provided for alpha {alpha} is not convertible to dtype '{self.alpha.dtype}'."
|
|
723
|
+
) from e
|
|
724
|
+
|
|
725
|
+
# Update beta.
|
|
726
|
+
if beta is not None:
|
|
727
|
+
if self.num_operands == 2:
|
|
728
|
+
self._logger.warning("Matmul: The provided beta value %s is ignored since operand C is not specified.", beta)
|
|
729
|
+
else:
|
|
730
|
+
try:
|
|
731
|
+
self.beta[0] = beta
|
|
732
|
+
except (ValueError, TypeError) as e:
|
|
733
|
+
raise ValueError(
|
|
734
|
+
f"The value provided for beta {beta} is not convertible to dtype '{self.beta.dtype}'."
|
|
735
|
+
) from e
|
|
736
|
+
|
|
737
|
+
_, operand_stream_holder = self._get_or_create_stream_maybe(stream)
|
|
738
|
+
|
|
739
|
+
# Reset the provided operands.
|
|
740
|
+
if a is not None:
|
|
741
|
+
a = tensor_wrapper.wrap_operand(a)
|
|
742
|
+
index = 0
|
|
743
|
+
self._check_and_set_operand(
|
|
744
|
+
a,
|
|
745
|
+
"A",
|
|
746
|
+
operand_stream_holder,
|
|
747
|
+
operand_index=index,
|
|
748
|
+
dtype=self.a_dtype_name,
|
|
749
|
+
extents=self.operand_extents[index],
|
|
750
|
+
strides=self.operand_strides[index],
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
if b is not None:
|
|
754
|
+
b = tensor_wrapper.wrap_operand(b)
|
|
755
|
+
index = 1
|
|
756
|
+
self._check_and_set_operand(
|
|
757
|
+
b,
|
|
758
|
+
"B",
|
|
759
|
+
operand_stream_holder,
|
|
760
|
+
operand_index=index,
|
|
761
|
+
dtype=self.b_dtype_name,
|
|
762
|
+
extents=self.operand_extents[index],
|
|
763
|
+
strides=self.operand_strides[index],
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
if c is not None: # If we get here, we know that C is one of the operands in the problem specification.
|
|
767
|
+
c = tensor_wrapper.wrap_operand(c)
|
|
768
|
+
index = 2
|
|
769
|
+
self._check_and_set_operand(
|
|
770
|
+
c,
|
|
771
|
+
"C",
|
|
772
|
+
operand_stream_holder,
|
|
773
|
+
operand_index=index,
|
|
774
|
+
dtype=self.c_dtype_name,
|
|
775
|
+
extents=self.operand_extents[index],
|
|
776
|
+
strides=self.operand_strides[index],
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
def reset_operands_unchecked(
|
|
780
|
+
self,
|
|
781
|
+
*,
|
|
782
|
+
a=None,
|
|
783
|
+
b=None,
|
|
784
|
+
c=None,
|
|
785
|
+
alpha=None,
|
|
786
|
+
beta=None,
|
|
787
|
+
stream: AnyStream | int | None = None,
|
|
788
|
+
):
|
|
789
|
+
if alpha is not None:
|
|
790
|
+
self.alpha[0] = alpha
|
|
791
|
+
if beta is not None:
|
|
792
|
+
self.beta[0] = beta
|
|
793
|
+
|
|
794
|
+
if self._memspace_eq_exespace:
|
|
795
|
+
if a is not None:
|
|
796
|
+
self._operands[0].tensor = a
|
|
797
|
+
if b is not None:
|
|
798
|
+
self._operands[1].tensor = b
|
|
799
|
+
if c is not None and self.num_operands == 3:
|
|
800
|
+
self._operands[2].tensor = c
|
|
801
|
+
return
|
|
802
|
+
|
|
803
|
+
operands = ((0, a), (1, b)) if self.num_operands == 2 else ((0, a), (1, b), (2, c))
|
|
804
|
+
_, operand_stream_holder = self._get_or_create_stream_maybe(stream)
|
|
805
|
+
for idx, value in operands:
|
|
806
|
+
if value is None:
|
|
807
|
+
continue
|
|
808
|
+
wrapped = tensor_wrapper.wrap_operand(value)
|
|
809
|
+
_realloc_if_needed_and_copy_to_mirror(
|
|
810
|
+
wrapped, self._operands[idx], self._execution_device_id, operand_stream_holder
|
|
811
|
+
)
|
|
812
|
+
self._operands_backup[idx] = wrapped
|
|
813
|
+
|
|
814
|
+
def _allocate_result_tensor_perhaps(self, *, stream_holder: utils.StreamHolder | None) -> utils.TensorHolder:
|
|
815
|
+
log_debug = self._logger.isEnabledFor(logging.DEBUG)
|
|
816
|
+
# We must handle all valid combinations of:
|
|
817
|
+
# - c-provided and c-not-provided
|
|
818
|
+
# - results in-place and out-of-place
|
|
819
|
+
|
|
820
|
+
# Create empty tensor for the result.
|
|
821
|
+
if self.num_operands == 2 or not self.options.inplace:
|
|
822
|
+
if log_debug:
|
|
823
|
+
self._logger.debug("Beginning output (empty) tensor creation...")
|
|
824
|
+
self._logger.debug(
|
|
825
|
+
f"The output tensor shape = {self._input_traits.d_layout_traits.shape} with strides = "
|
|
826
|
+
f"{self._input_traits.d_layout_traits.strides} and data type '{self.c_dtype_name}'."
|
|
827
|
+
)
|
|
828
|
+
self._logger.debug(
|
|
829
|
+
f"The output tensor has batch dimensions with shape {self._batch_traits[3].shape} "
|
|
830
|
+
f"and strides {self._batch_traits[3].strides}."
|
|
831
|
+
)
|
|
832
|
+
result = utils.create_empty_tensor(
|
|
833
|
+
self._result_class,
|
|
834
|
+
(*self._batch_traits[3].shape, *self._input_traits.d_layout_traits.shape),
|
|
835
|
+
self.c_dtype_name,
|
|
836
|
+
self._execution_device_id,
|
|
837
|
+
stream_holder,
|
|
838
|
+
# verify_strides=False because we need strides to be exactly what we
|
|
839
|
+
# request; not arbitrary if the strides aren't contiguous and dense.
|
|
840
|
+
# Otherwise, the layout parameters will mismatch what we pass to the matmul
|
|
841
|
+
# implementation.
|
|
842
|
+
verify_strides=False,
|
|
843
|
+
strides=(*self._batch_traits[3].strides, *self._input_traits.d_layout_traits.strides),
|
|
844
|
+
)
|
|
845
|
+
if log_debug:
|
|
846
|
+
self._logger.debug("The output (empty) tensor has been created.")
|
|
847
|
+
else: # num_operands == 3 and self.options.inplace
|
|
848
|
+
result = self._operands[2]
|
|
849
|
+
self._logger.debug("The output tensor is C (in-place execution).")
|
|
850
|
+
|
|
851
|
+
if self.num_operands == 3 and not self.options.inplace:
|
|
852
|
+
result.copy_(self._operands[2], stream_holder)
|
|
853
|
+
self._logger.debug("Operand C copied to result tensor (out-of-place execution).")
|
|
854
|
+
|
|
855
|
+
return result
|
|
856
|
+
|
|
857
|
+
def _copy_result_tensor_perhaps(self, *, result: utils.TensorHolder, stream_holder: utils.StreamHolder | None) -> AnyTensor:
|
|
858
|
+
# Return the result and auxiliary outputs, if present.
|
|
859
|
+
if self._memspace_eq_exespace:
|
|
860
|
+
out = result.tensor
|
|
861
|
+
else:
|
|
862
|
+
if self.options.inplace:
|
|
863
|
+
c = self._operands_backup[2]
|
|
864
|
+
assert c is not None and c.tensor is not None, (
|
|
865
|
+
"Internal Error. "
|
|
866
|
+
"Inplace operation was requested, but the execution space was different from the input space, "
|
|
867
|
+
"and we didn't keep a reference to the input tensor."
|
|
868
|
+
)
|
|
869
|
+
c.copy_(result, stream_holder=stream_holder)
|
|
870
|
+
out = c.tensor
|
|
871
|
+
else:
|
|
872
|
+
out = result.to(self._operands_device_id, stream_holder=stream_holder).tensor
|
|
873
|
+
|
|
874
|
+
return out
|
|
875
|
+
|
|
876
|
+
def execute(self, *, log_info, exec_stream_holder, operand_stream_holder) -> AnyTensor:
|
|
877
|
+
result = self._allocate_result_tensor_perhaps(stream_holder=exec_stream_holder)
|
|
878
|
+
|
|
879
|
+
a, b = self._operands[0], self._operands[1]
|
|
880
|
+
if log_info:
|
|
881
|
+
self._logger.info("Starting matrix multiplication...")
|
|
882
|
+
self._logger.info(f"{self._call_prologue}")
|
|
883
|
+
|
|
884
|
+
if self.execution.name == "cuda":
|
|
885
|
+
assert exec_stream_holder is not None
|
|
886
|
+
with utils.cuda_call_ctx(exec_stream_holder, self._blocking, timing=log_info) as (
|
|
887
|
+
_,
|
|
888
|
+
elapsed,
|
|
889
|
+
):
|
|
890
|
+
self._function(
|
|
891
|
+
a,
|
|
892
|
+
b,
|
|
893
|
+
result,
|
|
894
|
+
self.alpha,
|
|
895
|
+
self.beta,
|
|
896
|
+
exec_stream_holder,
|
|
897
|
+
)
|
|
898
|
+
else:
|
|
899
|
+
with utils.host_call_ctx(timing=log_info) as elapsed:
|
|
900
|
+
self._function(
|
|
901
|
+
a,
|
|
902
|
+
b,
|
|
903
|
+
result,
|
|
904
|
+
self.alpha,
|
|
905
|
+
self.beta,
|
|
906
|
+
exec_stream_holder,
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
if log_info and elapsed.data is not None:
|
|
910
|
+
self._logger.info(f"The matrix multiplication calculation took {elapsed.data:.3f} ms to complete.")
|
|
911
|
+
|
|
912
|
+
return self._copy_result_tensor_perhaps(result=result, stream_holder=operand_stream_holder)
|
|
913
|
+
|
|
914
|
+
def release_operands(self):
|
|
915
|
+
# Same space: _operands hold direct user references.
|
|
916
|
+
# Cross space: _operands hold internal device mirrors,
|
|
917
|
+
# _operands_backup hold direct user references.
|
|
918
|
+
# We preserve the TensorHolder wrappers and only clear the underlying tensor
|
|
919
|
+
# reference. This lets reset_operands_unchecked subsequently reuse the existing
|
|
920
|
+
# wrappers (and, in cross space, an existing device buffer) when present.
|
|
921
|
+
for op in self._operands:
|
|
922
|
+
op.tensor = None
|
|
923
|
+
if not self._memspace_eq_exespace:
|
|
924
|
+
for op in self._operands_backup:
|
|
925
|
+
op.tensor = None
|
|
926
|
+
|
|
927
|
+
def free(self):
|
|
928
|
+
# No ordering needed: CPU execution is blocking, and GPU execution operates on
|
|
929
|
+
# user-owned operands (use-after-free is the user's responsibility).
|
|
930
|
+
for attr in self.__slots__:
|
|
931
|
+
if attr != "_logger":
|
|
932
|
+
setattr(self, attr, None)
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
def _effective_batch_len(value: Any) -> int | None:
|
|
936
|
+
"""Effective broadcast length: None for None, len() for a non-str Sequence, else 1."""
|
|
937
|
+
if value is None:
|
|
938
|
+
return None
|
|
939
|
+
if isinstance(value, Sequence) and not isinstance(value, str):
|
|
940
|
+
return len(value)
|
|
941
|
+
return 1
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def _inplace_singleton_c_with_broadcast(a, b, c, *, alpha, beta, qualifiers) -> bool:
|
|
945
|
+
"""Return True iff ``c`` is a singleton but at least one of ``a``/``b``/
|
|
946
|
+
``qualifiers``/``alpha``/``beta`` would drive ``_zip_broadcast`` to create
|
|
947
|
+
more than one group. In that case the same ``C`` buffer is reused across
|
|
948
|
+
groups; under ``inplace=True`` the per-group updates would alias that buffer
|
|
949
|
+
and overlap, so the constructor must reject the configuration."""
|
|
950
|
+
c_is_singleton = not isinstance(c, Sequence) or len(c) == 1
|
|
951
|
+
if not c_is_singleton:
|
|
952
|
+
return False
|
|
953
|
+
return any(isinstance(x, Sequence) and len(x) > 1 for x in (a, b, qualifiers, alpha, beta))
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def _zip_broadcast(*args: Any, **kwargs: Any) -> Generator[tuple[tuple[Any, ...], dict[str, Any]], None, None]:
|
|
957
|
+
"""Like zip(), but handles keyword args and broadcasts inputs to the longest length.
|
|
958
|
+
|
|
959
|
+
Raises:
|
|
960
|
+
ValueError: If the inputs are not singleton or the same length as longest input.
|
|
961
|
+
|
|
962
|
+
Returns:
|
|
963
|
+
A generator that yields tuples of (args, kwargs), broadcasted to longest length.
|
|
964
|
+
"""
|
|
965
|
+
# Pre-process args such that all args are a sequence and they are all the same length or
|
|
966
|
+
# singleton.
|
|
967
|
+
arg_items: list[Sequence[Any]] = []
|
|
968
|
+
kwarg_items: dict[str, Sequence[Any]] = {}
|
|
969
|
+
lengths: list[int] = []
|
|
970
|
+
|
|
971
|
+
# Process positional arguments
|
|
972
|
+
for arg in args:
|
|
973
|
+
item = arg if isinstance(arg, Sequence) and not isinstance(arg, str) else [arg]
|
|
974
|
+
arg_items.append(item)
|
|
975
|
+
lengths.append(len(item))
|
|
976
|
+
|
|
977
|
+
# Process keyword arguments
|
|
978
|
+
for key, value in kwargs.items():
|
|
979
|
+
item = value if isinstance(value, Sequence) else [value]
|
|
980
|
+
kwarg_items[key] = item
|
|
981
|
+
lengths.append(len(item))
|
|
982
|
+
|
|
983
|
+
# lengths may be empty if no args or kwargs are provided
|
|
984
|
+
if not lengths:
|
|
985
|
+
return
|
|
986
|
+
|
|
987
|
+
# num_batch may be 0 if empty sequences are passed to args and kwargs
|
|
988
|
+
# num_batch may be positive if at least one non-empty args or kwargs are provided
|
|
989
|
+
num_batch = max(lengths)
|
|
990
|
+
|
|
991
|
+
# Check that all lengths are either 1 or num_batch; if num_batch is 0, then we already
|
|
992
|
+
# know all args will have length 0.
|
|
993
|
+
if num_batch != 0:
|
|
994
|
+
illegal_args = []
|
|
995
|
+
for i, item in enumerate(arg_items):
|
|
996
|
+
if len(item) not in (1, num_batch):
|
|
997
|
+
illegal_args.append(i)
|
|
998
|
+
|
|
999
|
+
illegal_kwargs = []
|
|
1000
|
+
for key, item in kwarg_items.items():
|
|
1001
|
+
if len(item) not in (1, num_batch):
|
|
1002
|
+
illegal_kwargs.append(key)
|
|
1003
|
+
|
|
1004
|
+
if illegal_args or illegal_kwargs:
|
|
1005
|
+
error_parts = []
|
|
1006
|
+
if illegal_args:
|
|
1007
|
+
error_parts.append(f"positional args at indices {illegal_args}")
|
|
1008
|
+
if illegal_kwargs:
|
|
1009
|
+
error_parts.append(f"keyword args {illegal_kwargs}")
|
|
1010
|
+
raise ValueError(
|
|
1011
|
+
f"All inputs must have the same length or be a singleton. "
|
|
1012
|
+
f"The following do not satisfy this condition: {' and '.join(error_parts)}."
|
|
1013
|
+
)
|
|
1014
|
+
|
|
1015
|
+
# Yield tuples of (args, kwargs) for each batch
|
|
1016
|
+
for batch_idx in range(num_batch):
|
|
1017
|
+
batch_args = tuple(item[batch_idx] if len(item) > 1 else item[0] for item in arg_items)
|
|
1018
|
+
batch_kwargs = {key: item[batch_idx] if len(item) > 1 else item[0] for key, item in kwarg_items.items()}
|
|
1019
|
+
yield (batch_args, batch_kwargs)
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
class _ExplicitlyBatchedMatmul:
|
|
1023
|
+
"""A Matmul object that supports explicitly batched operands.
|
|
1024
|
+
|
|
1025
|
+
This class has the same public API as :class:`Matmul`, except that the arguments may
|
|
1026
|
+
only be explicitly batched. Internally, this class just delegates to
|
|
1027
|
+
:class:`_ImplicitlyBatchedMatmul` for each batch.
|
|
1028
|
+
|
|
1029
|
+
.. important::
|
|
1030
|
+
|
|
1031
|
+
Explicit batches are always defined as a Python Sequence of the same type and
|
|
1032
|
+
implicit batches are always leading dimension of tensors. Otherwise, broadcasting
|
|
1033
|
+
behavior would be ambiguous. For example, if some arguments are sequences of tensors
|
|
1034
|
+
and other arguments are non-sequence tensors with batch dimensions.
|
|
1035
|
+
|
|
1036
|
+
.. seealso:: :func:`_zip_broadcast`, :class:`Matmul`, :class:`_ImplicitlyBatchedMatmul`
|
|
1037
|
+
|
|
1038
|
+
"""
|
|
1039
|
+
|
|
1040
|
+
__slots__ = (
|
|
1041
|
+
"_function",
|
|
1042
|
+
"_logger",
|
|
1043
|
+
"_original_batch_lens",
|
|
1044
|
+
"batches",
|
|
1045
|
+
"d_Aarray",
|
|
1046
|
+
"d_Barray",
|
|
1047
|
+
"d_Carray",
|
|
1048
|
+
"execution",
|
|
1049
|
+
"h_Aarray",
|
|
1050
|
+
"h_Barray",
|
|
1051
|
+
"h_Carray",
|
|
1052
|
+
"last_compute_event",
|
|
1053
|
+
"options",
|
|
1054
|
+
"workspace_stream",
|
|
1055
|
+
)
|
|
1056
|
+
|
|
1057
|
+
batches: Final[Sequence[_ImplicitlyBatchedMatmul]]
|
|
1058
|
+
_logger: Final[LoggerLike]
|
|
1059
|
+
execution: ExecutionCPU | ExecutionCUDA
|
|
1060
|
+
|
|
1061
|
+
def _get_or_create_stream_maybe(
|
|
1062
|
+
self, stream: utils.AnyStream
|
|
1063
|
+
) -> tuple[utils.StreamHolder | None, utils.StreamHolder | None]:
|
|
1064
|
+
return self.batches[0]._get_or_create_stream_maybe(stream)
|
|
1065
|
+
|
|
1066
|
+
def __init__(
|
|
1067
|
+
self,
|
|
1068
|
+
a: Sequence[AnyTensor] | AnyTensor,
|
|
1069
|
+
b: Sequence[AnyTensor] | AnyTensor,
|
|
1070
|
+
/,
|
|
1071
|
+
c: Sequence[AnyTensor] | AnyTensor | None = None,
|
|
1072
|
+
*,
|
|
1073
|
+
alpha: float | complex | Sequence[float | complex] | None = None,
|
|
1074
|
+
beta: float | complex | Sequence[float | complex] | None = None,
|
|
1075
|
+
qualifiers: Sequence[MatrixQualifier] | MatrixQualifier | None = None,
|
|
1076
|
+
options: MatmulOptions,
|
|
1077
|
+
execution: ExecutionCPU | ExecutionCUDA,
|
|
1078
|
+
exec_stream_holder: utils.StreamHolder | None,
|
|
1079
|
+
operand_stream_holder: utils.StreamHolder | None,
|
|
1080
|
+
):
|
|
1081
|
+
self.options = options
|
|
1082
|
+
self._logger = self.options.logger
|
|
1083
|
+
self.execution = execution
|
|
1084
|
+
|
|
1085
|
+
assert not (options.inplace and c is None), "Internal Error: Matmul should have rejected inplace=True with c=None."
|
|
1086
|
+
assert not (
|
|
1087
|
+
options.inplace and _inplace_singleton_c_with_broadcast(a, b, c, alpha=alpha, beta=beta, qualifiers=qualifiers)
|
|
1088
|
+
), "Internal Error: Matmul should have rejected inplace with broadcast c."
|
|
1089
|
+
|
|
1090
|
+
# None entries record "operand not provided" — distinguishable from a length-1
|
|
1091
|
+
# supply, so reset_operands can reject attempts to add an operand that wasn't there
|
|
1092
|
+
# originally.
|
|
1093
|
+
self._original_batch_lens: dict[str, int | None] = {
|
|
1094
|
+
"a": _effective_batch_len(a),
|
|
1095
|
+
"b": _effective_batch_len(b),
|
|
1096
|
+
"c": _effective_batch_len(c),
|
|
1097
|
+
"alpha": _effective_batch_len(alpha),
|
|
1098
|
+
"beta": _effective_batch_len(beta),
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
batches: list[_ImplicitlyBatchedMatmul] = []
|
|
1102
|
+
package: str = ""
|
|
1103
|
+
device_id: str | int = ""
|
|
1104
|
+
dtype: str = ""
|
|
1105
|
+
# Our zip functionality needs to be kwarg aware because some args are keyword only!
|
|
1106
|
+
for i, (batch_args, batch_kwargs) in enumerate(
|
|
1107
|
+
_zip_broadcast(
|
|
1108
|
+
a,
|
|
1109
|
+
b,
|
|
1110
|
+
c,
|
|
1111
|
+
qualifiers=qualifiers,
|
|
1112
|
+
alpha=alpha,
|
|
1113
|
+
beta=beta,
|
|
1114
|
+
)
|
|
1115
|
+
):
|
|
1116
|
+
batch = _ImplicitlyBatchedMatmul(
|
|
1117
|
+
*batch_args,
|
|
1118
|
+
**batch_kwargs,
|
|
1119
|
+
options=options,
|
|
1120
|
+
execution=execution,
|
|
1121
|
+
exec_stream_holder=exec_stream_holder,
|
|
1122
|
+
operand_stream_holder=operand_stream_holder,
|
|
1123
|
+
_allow_mixed_batch_order=True,
|
|
1124
|
+
_logger=_PrefixedLoggerAdapter(options.logger, {"prefix": f"batch {i}"}),
|
|
1125
|
+
)
|
|
1126
|
+
if package == "":
|
|
1127
|
+
package = batch._operands_package
|
|
1128
|
+
elif package != batch._operands_package:
|
|
1129
|
+
raise TypeError(
|
|
1130
|
+
f"Library package mismatch: All operands must come from the same package ({batch._operands_package}) "
|
|
1131
|
+
f"as the original operand ({package})."
|
|
1132
|
+
)
|
|
1133
|
+
if device_id == "":
|
|
1134
|
+
device_id = batch._operands_device_id
|
|
1135
|
+
elif device_id != batch._operands_device_id:
|
|
1136
|
+
raise TypeError(
|
|
1137
|
+
f"All operands must come from the same device ({batch._operands_device_id}) "
|
|
1138
|
+
f"as the original operand ({device_id})."
|
|
1139
|
+
)
|
|
1140
|
+
if dtype == "":
|
|
1141
|
+
dtype = batch.a_dtype_name
|
|
1142
|
+
elif dtype != batch.a_dtype_name:
|
|
1143
|
+
raise ValueError(
|
|
1144
|
+
f"All batches must have the same dtype. Batch {i} has dtype "
|
|
1145
|
+
f"'{batch.a_dtype_name}', but an earlier batch has dtype '{dtype}'."
|
|
1146
|
+
)
|
|
1147
|
+
batches.append(batch)
|
|
1148
|
+
self.batches = batches
|
|
1149
|
+
|
|
1150
|
+
num_pointers_all_groups = sum([max(1, batch._batch_traits[3].count) for batch in self.batches])
|
|
1151
|
+
self.h_Aarray = tensor_wrapper.wrap_operand(np.empty(num_pointers_all_groups, dtype=np.int64))
|
|
1152
|
+
self.h_Barray = tensor_wrapper.wrap_operand(np.empty(num_pointers_all_groups, dtype=np.int64))
|
|
1153
|
+
self.h_Carray = tensor_wrapper.wrap_operand(np.empty(num_pointers_all_groups, dtype=np.int64))
|
|
1154
|
+
|
|
1155
|
+
if self.execution.name == "cuda":
|
|
1156
|
+
# exec_stream_holder.package may be cupy/torch/etc. (not "cuda") for non-numpy
|
|
1157
|
+
# operands. Safe here because NDBufferTensor.empty only reads .obj.
|
|
1158
|
+
assert exec_stream_holder is not None
|
|
1159
|
+
self.d_Aarray = NDBufferTensor.empty(
|
|
1160
|
+
(num_pointers_all_groups,), device_id=self.execution.device_id, dtype="int64", stream_holder=exec_stream_holder
|
|
1161
|
+
)
|
|
1162
|
+
self.d_Barray = NDBufferTensor.empty(
|
|
1163
|
+
(num_pointers_all_groups,), device_id=self.execution.device_id, dtype="int64", stream_holder=exec_stream_holder
|
|
1164
|
+
)
|
|
1165
|
+
self.d_Carray = NDBufferTensor.empty(
|
|
1166
|
+
(num_pointers_all_groups,), device_id=self.execution.device_id, dtype="int64", stream_holder=exec_stream_holder
|
|
1167
|
+
)
|
|
1168
|
+
self.workspace_stream = exec_stream_holder.obj
|
|
1169
|
+
else:
|
|
1170
|
+
self.workspace_stream = None
|
|
1171
|
+
self.last_compute_event = None
|
|
1172
|
+
|
|
1173
|
+
def plan(self, *, stream: AnyStream | int | None = None, log_info=False) -> None:
|
|
1174
|
+
# NOTE: timing context called from Matmul.plan to avoid redundancy
|
|
1175
|
+
group_batch_traits = []
|
|
1176
|
+
group_mm_traits = []
|
|
1177
|
+
group_qualifiers = []
|
|
1178
|
+
flop_count = 0
|
|
1179
|
+
|
|
1180
|
+
for batch in self.batches:
|
|
1181
|
+
mm_layout_checker = mm_layout_checker_getter(batch._qualifiers)
|
|
1182
|
+
|
|
1183
|
+
mm_traits = BLASMMTraitsView.from_input_traits(
|
|
1184
|
+
batch._input_traits,
|
|
1185
|
+
mm_layout_checker,
|
|
1186
|
+
batch._logger,
|
|
1187
|
+
lookup_table_table=CACHED_LAYOUT_CHECKERS,
|
|
1188
|
+
)
|
|
1189
|
+
|
|
1190
|
+
batch._mm_traits = mm_traits
|
|
1191
|
+
|
|
1192
|
+
group_batch_traits.append(batch._batch_traits[3])
|
|
1193
|
+
group_mm_traits.append(batch._mm_traits)
|
|
1194
|
+
group_qualifiers.append(batch._qualifiers)
|
|
1195
|
+
|
|
1196
|
+
if log_info:
|
|
1197
|
+
flop_count += 2 * mm_traits.M * mm_traits.N * mm_traits.K
|
|
1198
|
+
|
|
1199
|
+
self._function, function_name = select_blas_group_mm_function(
|
|
1200
|
+
group_batch_traits,
|
|
1201
|
+
group_mm_traits,
|
|
1202
|
+
group_qualifiers,
|
|
1203
|
+
self._logger,
|
|
1204
|
+
self.execution,
|
|
1205
|
+
)
|
|
1206
|
+
|
|
1207
|
+
if log_info:
|
|
1208
|
+
self._logger.info(f"The plan found 1 suitable algorithm named {function_name}.")
|
|
1209
|
+
self._logger.info(f"The base matrix multiplication FLOP count is {formatters.FLOPSStr(flop_count, 'FLOP')}.")
|
|
1210
|
+
|
|
1211
|
+
def reset_operands(
|
|
1212
|
+
self,
|
|
1213
|
+
*,
|
|
1214
|
+
a=None,
|
|
1215
|
+
b=None,
|
|
1216
|
+
c=None,
|
|
1217
|
+
alpha=None,
|
|
1218
|
+
beta=None,
|
|
1219
|
+
stream: AnyStream | int | None = None,
|
|
1220
|
+
_operands_released=True,
|
|
1221
|
+
):
|
|
1222
|
+
# An operand value of None means "skip"; otherwise its effective length must equal
|
|
1223
|
+
# what was supplied at construction time, since changing the broadcast type of an
|
|
1224
|
+
# operand may be unsafe.
|
|
1225
|
+
unprovided: list[str] = []
|
|
1226
|
+
mismatches: list[tuple[str, int, int]] = []
|
|
1227
|
+
for name, value in (("a", a), ("b", b), ("c", c), ("alpha", alpha), ("beta", beta)):
|
|
1228
|
+
if value is None:
|
|
1229
|
+
continue
|
|
1230
|
+
new_len = _effective_batch_len(value)
|
|
1231
|
+
orig_len = self._original_batch_lens[name]
|
|
1232
|
+
if orig_len is None:
|
|
1233
|
+
unprovided.append(name)
|
|
1234
|
+
elif new_len != orig_len:
|
|
1235
|
+
assert new_len is not None # value is not None implies new_len is int (mypy)
|
|
1236
|
+
mismatches.append((name, orig_len, new_len))
|
|
1237
|
+
if unprovided:
|
|
1238
|
+
details = ", ".join(unprovided)
|
|
1239
|
+
raise ValueError(
|
|
1240
|
+
f"reset_operands cannot set operand(s) {details} because they were not "
|
|
1241
|
+
"provided to the constructor; reset_operands may only update operands "
|
|
1242
|
+
"that were originally supplied."
|
|
1243
|
+
)
|
|
1244
|
+
if mismatches:
|
|
1245
|
+
details = ", ".join(
|
|
1246
|
+
f"{name} was originally length {orig} but reset_operands received length {new}"
|
|
1247
|
+
for name, orig, new in mismatches
|
|
1248
|
+
)
|
|
1249
|
+
raise ValueError(
|
|
1250
|
+
"reset_operands received operands whose explicit-batch length does not "
|
|
1251
|
+
f"match the original construction: {details}. Each operand's effective "
|
|
1252
|
+
"length (len(seq) for a sequence, otherwise 1) must equal the length "
|
|
1253
|
+
"supplied at construction time; broadcasting cannot be introduced or "
|
|
1254
|
+
"removed by reset_operands."
|
|
1255
|
+
)
|
|
1256
|
+
|
|
1257
|
+
# NOTE: self.batches is included in the zip, so operands are broadcast correctly
|
|
1258
|
+
# when reset.
|
|
1259
|
+
for batch_args, batch_kwargs in _zip_broadcast(
|
|
1260
|
+
self.batches,
|
|
1261
|
+
a=a,
|
|
1262
|
+
b=b,
|
|
1263
|
+
c=c,
|
|
1264
|
+
alpha=alpha,
|
|
1265
|
+
beta=beta,
|
|
1266
|
+
):
|
|
1267
|
+
batch_args[0].reset_operands(
|
|
1268
|
+
*batch_args[1:],
|
|
1269
|
+
**batch_kwargs,
|
|
1270
|
+
stream=stream,
|
|
1271
|
+
_operands_released=_operands_released,
|
|
1272
|
+
)
|
|
1273
|
+
|
|
1274
|
+
def reset_operands_unchecked(
|
|
1275
|
+
self,
|
|
1276
|
+
*,
|
|
1277
|
+
a=None,
|
|
1278
|
+
b=None,
|
|
1279
|
+
c=None,
|
|
1280
|
+
alpha=None,
|
|
1281
|
+
beta=None,
|
|
1282
|
+
stream: AnyStream | int | None = None,
|
|
1283
|
+
):
|
|
1284
|
+
for batch_args, batch_kwargs in _zip_broadcast(
|
|
1285
|
+
self.batches,
|
|
1286
|
+
a=a,
|
|
1287
|
+
b=b,
|
|
1288
|
+
c=c,
|
|
1289
|
+
alpha=alpha,
|
|
1290
|
+
beta=beta,
|
|
1291
|
+
):
|
|
1292
|
+
batch_args[0].reset_operands_unchecked(
|
|
1293
|
+
*batch_args[1:],
|
|
1294
|
+
**batch_kwargs,
|
|
1295
|
+
stream=stream,
|
|
1296
|
+
)
|
|
1297
|
+
|
|
1298
|
+
def execute(self, *, log_info, exec_stream_holder, operand_stream_holder) -> Sequence[AnyTensor]:
|
|
1299
|
+
# Get the data type to determine which function to call
|
|
1300
|
+
dtype = self.batches[0].a_dtype_name
|
|
1301
|
+
|
|
1302
|
+
results: list[utils.TensorHolder] = []
|
|
1303
|
+
|
|
1304
|
+
lo, hi = (0, 0)
|
|
1305
|
+
|
|
1306
|
+
for batch in self.batches:
|
|
1307
|
+
# Allocate result tensors and collect their pointers
|
|
1308
|
+
result = batch._allocate_result_tensor_perhaps(stream_holder=exec_stream_holder)
|
|
1309
|
+
results.append(result)
|
|
1310
|
+
|
|
1311
|
+
# Collect all matrix pointers from each batch
|
|
1312
|
+
num_pointers_this_batch = max(1, batch._batch_traits[3].count)
|
|
1313
|
+
hi += num_pointers_this_batch
|
|
1314
|
+
A_pointers = get_addresses_of_elements(
|
|
1315
|
+
batch._batch_traits[0].shape,
|
|
1316
|
+
batch._batch_traits[0].strides,
|
|
1317
|
+
batch._operands[0].data_ptr,
|
|
1318
|
+
itemsize=batch._operands[0].itemsize,
|
|
1319
|
+
)
|
|
1320
|
+
B_pointers = get_addresses_of_elements(
|
|
1321
|
+
batch._batch_traits[1].shape,
|
|
1322
|
+
batch._batch_traits[1].strides,
|
|
1323
|
+
batch._operands[1].data_ptr,
|
|
1324
|
+
itemsize=batch._operands[1].itemsize,
|
|
1325
|
+
)
|
|
1326
|
+
C_pointers = get_addresses_of_elements(
|
|
1327
|
+
batch._batch_traits[3].shape,
|
|
1328
|
+
batch._batch_traits[3].strides,
|
|
1329
|
+
result.data_ptr,
|
|
1330
|
+
itemsize=result.itemsize,
|
|
1331
|
+
)
|
|
1332
|
+
|
|
1333
|
+
# Handle broadcast operands within an implicit batch
|
|
1334
|
+
if len(A_pointers) < num_pointers_this_batch:
|
|
1335
|
+
A_pointers = np.tile(A_pointers, num_pointers_this_batch)
|
|
1336
|
+
if len(B_pointers) < num_pointers_this_batch:
|
|
1337
|
+
B_pointers = np.tile(B_pointers, num_pointers_this_batch)
|
|
1338
|
+
|
|
1339
|
+
# Swap A and B pointers if the matrices are swapped because of layout
|
|
1340
|
+
# compatibility
|
|
1341
|
+
if batch._mm_traits.is_swapped_AB:
|
|
1342
|
+
A_pointers, B_pointers = B_pointers, A_pointers
|
|
1343
|
+
|
|
1344
|
+
self.h_Aarray.tensor[lo:hi] = A_pointers
|
|
1345
|
+
self.h_Barray.tensor[lo:hi] = B_pointers
|
|
1346
|
+
self.h_Carray.tensor[lo:hi] = C_pointers
|
|
1347
|
+
|
|
1348
|
+
lo = hi
|
|
1349
|
+
|
|
1350
|
+
# Collect alpha and beta values
|
|
1351
|
+
alpha_array = np.array([batch.alpha[0] for batch in self.batches], dtype=dtype)
|
|
1352
|
+
beta_array = np.array([batch.beta[0] for batch in self.batches], dtype=dtype)
|
|
1353
|
+
|
|
1354
|
+
if log_info:
|
|
1355
|
+
self._logger.info("Starting matrix multiplication...")
|
|
1356
|
+
self._logger.info(f"{self.batches[0]._call_prologue}")
|
|
1357
|
+
|
|
1358
|
+
# Call the appropriate grouped batched gemm function based on dtype
|
|
1359
|
+
if self.execution.name == "cpu":
|
|
1360
|
+
with utils.host_call_ctx(timing=log_info) as elapsed:
|
|
1361
|
+
self._function(
|
|
1362
|
+
self.h_Aarray,
|
|
1363
|
+
self.h_Barray,
|
|
1364
|
+
self.h_Carray,
|
|
1365
|
+
alpha_array,
|
|
1366
|
+
beta_array,
|
|
1367
|
+
exec_stream_holder,
|
|
1368
|
+
)
|
|
1369
|
+
else:
|
|
1370
|
+
assert exec_stream_holder is not None
|
|
1371
|
+
|
|
1372
|
+
# Copy pointer arrays to device using the appropriate wrapper
|
|
1373
|
+
self.d_Aarray.copy_(self.h_Aarray, exec_stream_holder)
|
|
1374
|
+
self.d_Barray.copy_(self.h_Barray, exec_stream_holder)
|
|
1375
|
+
self.d_Carray.copy_(self.h_Carray, exec_stream_holder)
|
|
1376
|
+
|
|
1377
|
+
with utils.cuda_call_ctx(exec_stream_holder, self.batches[0]._blocking, timing=log_info) as (
|
|
1378
|
+
self.last_compute_event,
|
|
1379
|
+
elapsed,
|
|
1380
|
+
):
|
|
1381
|
+
self._function(
|
|
1382
|
+
self.d_Aarray,
|
|
1383
|
+
self.d_Barray,
|
|
1384
|
+
self.d_Carray,
|
|
1385
|
+
alpha_array,
|
|
1386
|
+
beta_array,
|
|
1387
|
+
exec_stream_holder,
|
|
1388
|
+
)
|
|
1389
|
+
|
|
1390
|
+
if log_info and elapsed.data is not None:
|
|
1391
|
+
self._logger.info(f"The matrix multiplication calculation took {elapsed.data:.3f} ms to complete.")
|
|
1392
|
+
|
|
1393
|
+
# Return results (copy if needed)
|
|
1394
|
+
return [
|
|
1395
|
+
batch._copy_result_tensor_perhaps(result=result, stream_holder=operand_stream_holder)
|
|
1396
|
+
for batch, result in zip(self.batches, results, strict=True)
|
|
1397
|
+
]
|
|
1398
|
+
|
|
1399
|
+
def release_operands(self):
|
|
1400
|
+
for b in self.batches:
|
|
1401
|
+
b.release_operands()
|
|
1402
|
+
|
|
1403
|
+
def free(self):
|
|
1404
|
+
# Ensure ordering with respect to the last computation
|
|
1405
|
+
# to avoid race conditions when releasing internal resources.
|
|
1406
|
+
if self.last_compute_event is not None:
|
|
1407
|
+
if self.workspace_stream is not None:
|
|
1408
|
+
self.workspace_stream.wait(self.last_compute_event)
|
|
1409
|
+
self.last_compute_event = None
|
|
1410
|
+
|
|
1411
|
+
for batch in self.batches:
|
|
1412
|
+
batch.free()
|
|
1413
|
+
|
|
1414
|
+
for attr in self.__slots__:
|
|
1415
|
+
if attr != "_logger":
|
|
1416
|
+
setattr(self, attr, None)
|
|
1417
|
+
|
|
1418
|
+
|
|
1419
|
+
@utils.docstring_decorator(GENERIC_MM_DOCUMENTATION, skip_missing=False)
|
|
1420
|
+
class Matmul:
|
|
1421
|
+
"""
|
|
1422
|
+
Create a stateful object encapsulating the specified matrix multiplication computation
|
|
1423
|
+
:math:`\\alpha a @ b + \\beta c` and the required resources to perform the operation. A
|
|
1424
|
+
stateful object can be used to amortize the cost of preparation (planning in the case of
|
|
1425
|
+
matrix multiplication) across multiple executions (also see the :ref:`Stateful APIs
|
|
1426
|
+
<host api types>` section).
|
|
1427
|
+
|
|
1428
|
+
The function-form API :func:`matmul` is a convenient alternative to using stateful
|
|
1429
|
+
objects for *single* use (the user needs to perform just one matrix multiplication, for
|
|
1430
|
+
example), in which case there is no possibility of amortizing preparatory costs. The
|
|
1431
|
+
function-form APIs are just convenience wrappers around the stateful object APIs.
|
|
1432
|
+
|
|
1433
|
+
Using the stateful object typically involves the following steps:
|
|
1434
|
+
|
|
1435
|
+
1. **Problem Specification**: Initialize the object with a defined operation and
|
|
1436
|
+
options.
|
|
1437
|
+
2. **Preparation**: Use :meth:`plan` to determine the best algorithmic implementation
|
|
1438
|
+
for this specific matrix multiplication operation.
|
|
1439
|
+
3. **Execution**: Perform the matrix multiplication computation with :meth:`execute`.
|
|
1440
|
+
|
|
1441
|
+
Detailed information on what's happening in the various phases described above can be
|
|
1442
|
+
obtained by passing in a :class:`logging.Logger` object to :class:`MatmulOptions` or by
|
|
1443
|
+
setting the appropriate options in the root logger object, which is used by default:
|
|
1444
|
+
|
|
1445
|
+
>>> import logging
|
|
1446
|
+
>>> logging.basicConfig(
|
|
1447
|
+
... level=logging.INFO,
|
|
1448
|
+
... format="%(asctime)s %(levelname)-8s %(message)s",
|
|
1449
|
+
... datefmt="%m-%d %H:%M:%S",
|
|
1450
|
+
... )
|
|
1451
|
+
|
|
1452
|
+
A user can select the desired logging level and, in general, take advantage of all of
|
|
1453
|
+
the functionality offered by the Python `logging` module.
|
|
1454
|
+
|
|
1455
|
+
Args:
|
|
1456
|
+
a: {a}
|
|
1457
|
+
|
|
1458
|
+
b: {b}
|
|
1459
|
+
|
|
1460
|
+
c: {c}
|
|
1461
|
+
|
|
1462
|
+
alpha: {alpha}
|
|
1463
|
+
|
|
1464
|
+
beta: {beta}
|
|
1465
|
+
|
|
1466
|
+
qualifiers: {qualifiers}
|
|
1467
|
+
|
|
1468
|
+
options: {options}
|
|
1469
|
+
|
|
1470
|
+
execution: {execution}
|
|
1471
|
+
|
|
1472
|
+
stream: {stream}
|
|
1473
|
+
|
|
1474
|
+
Semantics:
|
|
1475
|
+
{semantics}
|
|
1476
|
+
|
|
1477
|
+
Thread Safety:
|
|
1478
|
+
{thread_safety}
|
|
1479
|
+
|
|
1480
|
+
.. seealso::
|
|
1481
|
+
:meth:`reset_operands`, :meth:`reset_operands_unchecked`, :meth:`execute`
|
|
1482
|
+
|
|
1483
|
+
Examples:
|
|
1484
|
+
|
|
1485
|
+
>>> import numpy as np
|
|
1486
|
+
>>> import nvmath
|
|
1487
|
+
|
|
1488
|
+
Create two 2-D float64 ndarrays on the CPU:
|
|
1489
|
+
|
|
1490
|
+
>>> M, N, K = 1024, 1024, 1024
|
|
1491
|
+
>>> a = np.random.rand(M, K)
|
|
1492
|
+
>>> b = np.random.rand(K, N)
|
|
1493
|
+
|
|
1494
|
+
We will define a matrix multiplication operation using the generic matrix
|
|
1495
|
+
multiplication interface.
|
|
1496
|
+
|
|
1497
|
+
Create a Matmul object encapsulating the problem specification above:
|
|
1498
|
+
|
|
1499
|
+
>>> mm = nvmath.linalg.Matmul(a, b)
|
|
1500
|
+
|
|
1501
|
+
Options can be provided above to control the behavior of the operation using the
|
|
1502
|
+
`options` argument (see :class:`MatmulOptions`).
|
|
1503
|
+
|
|
1504
|
+
Next, plan the operation. The operands' layouts, qualifiers, and dtypes will be
|
|
1505
|
+
considered to select an appropriate matrix multiplication:
|
|
1506
|
+
|
|
1507
|
+
>>> mm.plan()
|
|
1508
|
+
|
|
1509
|
+
Now execute the matrix multiplication, and obtain the result `r1` as a NumPy
|
|
1510
|
+
ndarray.
|
|
1511
|
+
|
|
1512
|
+
>>> r1 = mm.execute()
|
|
1513
|
+
|
|
1514
|
+
Note that all :class:`Matmul` methods execute on the current stream by default.
|
|
1515
|
+
Alternatively, the `stream` argument can be used to run a method on a specified
|
|
1516
|
+
stream.
|
|
1517
|
+
|
|
1518
|
+
Let's now look at the same problem with CuPy ndarrays on the GPU.
|
|
1519
|
+
|
|
1520
|
+
Create a 2-D CuPy ndarray on the GPU:
|
|
1521
|
+
|
|
1522
|
+
>>> import cupy as cp
|
|
1523
|
+
>>> a = cp.random.rand(M, K)
|
|
1524
|
+
>>> b = cp.random.rand(K, N)
|
|
1525
|
+
|
|
1526
|
+
Create an Matmul object encapsulating the problem specification described earlier
|
|
1527
|
+
and use it as a context manager.
|
|
1528
|
+
|
|
1529
|
+
>>> with nvmath.linalg.Matmul(a, b) as mm:
|
|
1530
|
+
... # Plan the operation.
|
|
1531
|
+
... mm.plan()
|
|
1532
|
+
...
|
|
1533
|
+
... # Execute the operation to get the first result.
|
|
1534
|
+
... r1 = mm.execute()
|
|
1535
|
+
...
|
|
1536
|
+
... # Update operands 'a' and 'b' in-place (see reset_operands() for an
|
|
1537
|
+
... # alternative).
|
|
1538
|
+
... a[:] = cp.random.rand(M, K)
|
|
1539
|
+
... b[:] = cp.random.rand(K, N)
|
|
1540
|
+
...
|
|
1541
|
+
... # Execute the operation to get the new result.
|
|
1542
|
+
... r2 = mm.execute()
|
|
1543
|
+
|
|
1544
|
+
|
|
1545
|
+
All the resources used by the object are released at the end of the block.
|
|
1546
|
+
|
|
1547
|
+
Further examples can be found in the `examples/linalg/generic/matmul
|
|
1548
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/linalg/generic/matmul>`_
|
|
1549
|
+
directory.
|
|
1550
|
+
"""
|
|
1551
|
+
|
|
1552
|
+
__slots__ = (
|
|
1553
|
+
"_delegate",
|
|
1554
|
+
"_has_plan",
|
|
1555
|
+
"_logger",
|
|
1556
|
+
"_operands_released",
|
|
1557
|
+
"options",
|
|
1558
|
+
"valid_state",
|
|
1559
|
+
)
|
|
1560
|
+
|
|
1561
|
+
_delegate: Final[_ImplicitlyBatchedMatmul | _ExplicitlyBatchedMatmul]
|
|
1562
|
+
|
|
1563
|
+
def __init__(
|
|
1564
|
+
self,
|
|
1565
|
+
a: AnyTensor | Sequence[AnyTensor],
|
|
1566
|
+
b: AnyTensor | Sequence[AnyTensor],
|
|
1567
|
+
/,
|
|
1568
|
+
c: AnyTensor | Sequence[AnyTensor] | None = None,
|
|
1569
|
+
*,
|
|
1570
|
+
alpha: float | complex | Sequence[float | complex] | None = None,
|
|
1571
|
+
beta: float | complex | Sequence[float | complex] | None = None,
|
|
1572
|
+
qualifiers: MatrixQualifier | Sequence[MatrixQualifier] | None = None,
|
|
1573
|
+
options: MatmulOptions | dict[str, Any] | None = None,
|
|
1574
|
+
execution: ExecutionCPU | ExecutionCUDA | str | None = None,
|
|
1575
|
+
stream: AnyStream | int | None = None,
|
|
1576
|
+
):
|
|
1577
|
+
if isinstance(options, Sequence):
|
|
1578
|
+
raise TypeError("options must be a single value.")
|
|
1579
|
+
if isinstance(execution, Sequence) and not isinstance(execution, str):
|
|
1580
|
+
raise TypeError("execution must be a single value.")
|
|
1581
|
+
if isinstance(stream, Sequence):
|
|
1582
|
+
raise TypeError("stream must be a single value.")
|
|
1583
|
+
|
|
1584
|
+
options = utils.check_or_create_options(MatmulOptions, options, "Matrix multiplication options")
|
|
1585
|
+
assert options is not None
|
|
1586
|
+
self.options = options
|
|
1587
|
+
self._logger = self.options.logger
|
|
1588
|
+
self._logger.info("= SPECIFICATION PHASE =")
|
|
1589
|
+
|
|
1590
|
+
if (
|
|
1591
|
+
isinstance(a, Sequence)
|
|
1592
|
+
and isinstance(b, Sequence)
|
|
1593
|
+
and (c is None or isinstance(c, Sequence))
|
|
1594
|
+
and (qualifiers is None or isinstance(qualifiers, Sequence))
|
|
1595
|
+
):
|
|
1596
|
+
self._logger.info("The matrix multiplication is explicitly batched.")
|
|
1597
|
+
delegate_class: type = _ExplicitlyBatchedMatmul
|
|
1598
|
+
elif (
|
|
1599
|
+
not isinstance(a, Sequence)
|
|
1600
|
+
and not isinstance(b, Sequence)
|
|
1601
|
+
and (c is None or not isinstance(c, Sequence))
|
|
1602
|
+
and (qualifiers is None or not isinstance(qualifiers, Sequence))
|
|
1603
|
+
):
|
|
1604
|
+
self._logger.info("The matrix multiplication is implicitly batched.")
|
|
1605
|
+
delegate_class = _ImplicitlyBatchedMatmul
|
|
1606
|
+
else:
|
|
1607
|
+
raise ValueError(
|
|
1608
|
+
"All arguments a, b, c, and qualifiers must be either "
|
|
1609
|
+
"implicitly batched (tensors) or explicitly batched (sequences of tensors). "
|
|
1610
|
+
f"a is a {type(a)}, "
|
|
1611
|
+
f"b is a {type(b)}, "
|
|
1612
|
+
f"c is a {type(c)}, and "
|
|
1613
|
+
f"qualifiers is a {type(qualifiers)}."
|
|
1614
|
+
)
|
|
1615
|
+
|
|
1616
|
+
empty = [
|
|
1617
|
+
name
|
|
1618
|
+
for name, value in (("a", a), ("b", b), ("c", c), ("qualifiers", qualifiers))
|
|
1619
|
+
if isinstance(value, Sequence) and len(value) == 0
|
|
1620
|
+
]
|
|
1621
|
+
if empty:
|
|
1622
|
+
raise ValueError(
|
|
1623
|
+
"At least one batch must be provided for explicitly batched matrix multiplication. "
|
|
1624
|
+
f"The following argument(s) are empty: {', '.join(empty)}."
|
|
1625
|
+
)
|
|
1626
|
+
|
|
1627
|
+
nested = [
|
|
1628
|
+
name
|
|
1629
|
+
for name, value in (("a", a), ("b", b), ("c", c), ("qualifiers", qualifiers))
|
|
1630
|
+
if isinstance(value, Sequence) and isinstance(value[0], Sequence)
|
|
1631
|
+
]
|
|
1632
|
+
if nested:
|
|
1633
|
+
raise ValueError(
|
|
1634
|
+
"Explicitly batched operands cannot be nested (sequence of sequences). "
|
|
1635
|
+
f"The following argument(s) are nested: {', '.join(nested)}."
|
|
1636
|
+
)
|
|
1637
|
+
|
|
1638
|
+
if options.inplace:
|
|
1639
|
+
if c is None:
|
|
1640
|
+
raise ValueError("Operation cannot be inplace if operand C is not provided.")
|
|
1641
|
+
if delegate_class is _ExplicitlyBatchedMatmul and _inplace_singleton_c_with_broadcast(
|
|
1642
|
+
a, b, c, alpha=alpha, beta=beta, qualifiers=qualifiers
|
|
1643
|
+
):
|
|
1644
|
+
raise ValueError("Operation cannot be inplace if operand C is broadcast.")
|
|
1645
|
+
|
|
1646
|
+
representatives = []
|
|
1647
|
+
for operand in (a, b, c):
|
|
1648
|
+
if operand is None:
|
|
1649
|
+
continue
|
|
1650
|
+
representative = operand[0] if isinstance(operand, Sequence) else operand
|
|
1651
|
+
representatives.append(tensor_wrapper.wrap_operand(representative))
|
|
1652
|
+
operands_device_id = utils.get_operands_device_id(representatives)
|
|
1653
|
+
|
|
1654
|
+
execution = _resolve_execution(execution, operands_device_id)
|
|
1655
|
+
|
|
1656
|
+
self._logger.info(
|
|
1657
|
+
f"The input tensors are located on device {operands_device_id}, and the execution space "
|
|
1658
|
+
f"is {execution.name}, with device {getattr(execution, 'device_id', 'cpu')}."
|
|
1659
|
+
)
|
|
1660
|
+
|
|
1661
|
+
operands_package = utils.get_operands_package(representatives)
|
|
1662
|
+
internal_op_package = _resolve_internal_operand_package(execution.name, operands_package)
|
|
1663
|
+
exec_stream_holder, operand_stream_holder = _get_or_create_stream_maybe(
|
|
1664
|
+
execution,
|
|
1665
|
+
operands_device_id,
|
|
1666
|
+
operands_package,
|
|
1667
|
+
internal_op_package,
|
|
1668
|
+
stream,
|
|
1669
|
+
)
|
|
1670
|
+
holder = exec_stream_holder or operand_stream_holder
|
|
1671
|
+
self._logger.info(f"The specified stream for the Matmul ctor is {holder.obj if holder else None}.")
|
|
1672
|
+
|
|
1673
|
+
self._delegate = delegate_class(
|
|
1674
|
+
a,
|
|
1675
|
+
b,
|
|
1676
|
+
c=c,
|
|
1677
|
+
alpha=alpha,
|
|
1678
|
+
beta=beta,
|
|
1679
|
+
qualifiers=qualifiers,
|
|
1680
|
+
options=options,
|
|
1681
|
+
execution=execution,
|
|
1682
|
+
exec_stream_holder=exec_stream_holder,
|
|
1683
|
+
operand_stream_holder=operand_stream_holder,
|
|
1684
|
+
)
|
|
1685
|
+
self._has_plan = False
|
|
1686
|
+
# Track whether user-provided operands have been released
|
|
1687
|
+
self._operands_released = False
|
|
1688
|
+
self.valid_state = True
|
|
1689
|
+
self._logger.info("The Matmul operation has been created.")
|
|
1690
|
+
|
|
1691
|
+
def _check_valid_matmul(self, *args, **kwargs):
|
|
1692
|
+
"""
|
|
1693
|
+
Check if the Matmul object is alive and well.
|
|
1694
|
+
"""
|
|
1695
|
+
if not self.valid_state:
|
|
1696
|
+
raise InvalidMatmulState("The Matmul object cannot be used after resources are free'd")
|
|
1697
|
+
|
|
1698
|
+
def _check_planned(self, *args, **kwargs):
|
|
1699
|
+
"""
|
|
1700
|
+
Check if plan() has been called on this Matmul object.
|
|
1701
|
+
"""
|
|
1702
|
+
what = kwargs["what"]
|
|
1703
|
+
if not self._has_plan:
|
|
1704
|
+
raise RuntimeError(f"{what} cannot be performed before plan() has been called.")
|
|
1705
|
+
|
|
1706
|
+
def _check_valid_operands(self, *args, **kwargs):
|
|
1707
|
+
"""
|
|
1708
|
+
Check if operands are available for operations.
|
|
1709
|
+
"""
|
|
1710
|
+
if self._operands_released:
|
|
1711
|
+
raise ValueError(
|
|
1712
|
+
"Operands have been released. Call reset_operands() to provide new operands before using this method."
|
|
1713
|
+
)
|
|
1714
|
+
|
|
1715
|
+
@utils.precondition(_check_valid_matmul)
|
|
1716
|
+
@utils.precondition(_check_planned, "Execution")
|
|
1717
|
+
@utils.precondition(_check_valid_operands)
|
|
1718
|
+
def execute(self, *, stream: AnyStream | int | None = None) -> AnyTensor | Sequence[AnyTensor]:
|
|
1719
|
+
"""
|
|
1720
|
+
Execute a prepared (planned) matrix multiplication.
|
|
1721
|
+
|
|
1722
|
+
Args:
|
|
1723
|
+
stream: {stream}
|
|
1724
|
+
|
|
1725
|
+
Returns:
|
|
1726
|
+
{result}
|
|
1727
|
+
"""
|
|
1728
|
+
exec_stream_holder, operand_stream_holder = self._delegate._get_or_create_stream_maybe(stream)
|
|
1729
|
+
|
|
1730
|
+
if log_info := self._logger.isEnabledFor(logging.INFO):
|
|
1731
|
+
self._logger.info("= EXECUTION PHASE =")
|
|
1732
|
+
self._logger.info(
|
|
1733
|
+
"The specified stream for execute() is "
|
|
1734
|
+
f"{getattr(exec_stream_holder or operand_stream_holder, 'obj', 'no stream')}."
|
|
1735
|
+
)
|
|
1736
|
+
|
|
1737
|
+
return self._delegate.execute(
|
|
1738
|
+
log_info=log_info,
|
|
1739
|
+
exec_stream_holder=exec_stream_holder,
|
|
1740
|
+
operand_stream_holder=operand_stream_holder,
|
|
1741
|
+
)
|
|
1742
|
+
|
|
1743
|
+
def execute_unchecked(self, *, stream: AnyStream | int | None = None) -> AnyTensor | Sequence[AnyTensor]:
|
|
1744
|
+
"""
|
|
1745
|
+
.. experimental:: method
|
|
1746
|
+
|
|
1747
|
+
.. versionadded:: 1.0
|
|
1748
|
+
|
|
1749
|
+
This method is a performance-optimized alternative to :meth:`execute` that
|
|
1750
|
+
eliminates validation and logging overhead, making it ideal for performance-critical
|
|
1751
|
+
loops where operand compatibility is guaranteed by the caller.
|
|
1752
|
+
|
|
1753
|
+
Args:
|
|
1754
|
+
stream: {stream}
|
|
1755
|
+
|
|
1756
|
+
Returns:
|
|
1757
|
+
{result}
|
|
1758
|
+
|
|
1759
|
+
.. seealso::
|
|
1760
|
+
:meth:`execute`: Safe, validated method for executing the matrix multiplication.
|
|
1761
|
+
"""
|
|
1762
|
+
exec_stream_holder, operand_stream_holder = self._delegate._get_or_create_stream_maybe(stream)
|
|
1763
|
+
|
|
1764
|
+
return self._delegate.execute(
|
|
1765
|
+
log_info=False, exec_stream_holder=exec_stream_holder, operand_stream_holder=operand_stream_holder
|
|
1766
|
+
)
|
|
1767
|
+
|
|
1768
|
+
@utils.precondition(_check_valid_matmul)
|
|
1769
|
+
def plan(self, *, stream: AnyStream | int | None = None) -> None:
|
|
1770
|
+
"""
|
|
1771
|
+
Plan the matrix multiplication operation.
|
|
1772
|
+
|
|
1773
|
+
Unlike :py:meth:`nvmath.linalg.advanced.Matmul.plan`, this method takes no
|
|
1774
|
+
tuning parameters. Its primary function is to find the correct matrix multiplication
|
|
1775
|
+
implementation based on the operands and options provided to the constructor.
|
|
1776
|
+
|
|
1777
|
+
Args:
|
|
1778
|
+
stream: {stream}
|
|
1779
|
+
|
|
1780
|
+
Returns:
|
|
1781
|
+
None
|
|
1782
|
+
"""
|
|
1783
|
+
if log_info := self._logger.isEnabledFor(logging.INFO):
|
|
1784
|
+
self._logger.info("= PLANNING PHASE =")
|
|
1785
|
+
if self._has_plan:
|
|
1786
|
+
self._logger.info("Matmul is already planned. Replanning is skipped.")
|
|
1787
|
+
else:
|
|
1788
|
+
self._logger.info("Starting matrix multiplication planning...")
|
|
1789
|
+
|
|
1790
|
+
if self._has_plan:
|
|
1791
|
+
return
|
|
1792
|
+
|
|
1793
|
+
with utils.host_call_ctx(timing=log_info) as elapsed:
|
|
1794
|
+
self._delegate.plan(stream=stream, log_info=log_info)
|
|
1795
|
+
|
|
1796
|
+
if log_info and elapsed.data is not None:
|
|
1797
|
+
self._logger.info(f"The matrix multiplication planning phase took {elapsed.data:.3f} ms to complete.")
|
|
1798
|
+
|
|
1799
|
+
self._has_plan = True
|
|
1800
|
+
|
|
1801
|
+
@utils.precondition(_check_valid_matmul)
|
|
1802
|
+
def reset_operands(
|
|
1803
|
+
self,
|
|
1804
|
+
*,
|
|
1805
|
+
a=None,
|
|
1806
|
+
b=None,
|
|
1807
|
+
c=None,
|
|
1808
|
+
alpha=None,
|
|
1809
|
+
beta=None,
|
|
1810
|
+
stream: AnyStream | int | None = None,
|
|
1811
|
+
):
|
|
1812
|
+
"""
|
|
1813
|
+
Reset one or more of the operands held by this :class:`Matmul` instance.
|
|
1814
|
+
|
|
1815
|
+
.. versionchanged:: 0.9
|
|
1816
|
+
All parameters are now keyword-only.
|
|
1817
|
+
|
|
1818
|
+
Args:
|
|
1819
|
+
a: {a}
|
|
1820
|
+
|
|
1821
|
+
b: {b}
|
|
1822
|
+
|
|
1823
|
+
c: {c}
|
|
1824
|
+
|
|
1825
|
+
alpha: {alpha}
|
|
1826
|
+
|
|
1827
|
+
beta: {beta}
|
|
1828
|
+
|
|
1829
|
+
stream: {stream}
|
|
1830
|
+
|
|
1831
|
+
Semantics:
|
|
1832
|
+
- Only the operands explicitly passed are updated. At least one operand
|
|
1833
|
+
is required (all of them after :meth:`release_operands`), otherwise
|
|
1834
|
+
a :class:`ValueError` is raised.
|
|
1835
|
+
|
|
1836
|
+
- This method will perform various checks on the new operands to make sure:
|
|
1837
|
+
|
|
1838
|
+
- The explicit batch counts, shapes, strides, datatypes match
|
|
1839
|
+
those of the old ones.
|
|
1840
|
+
- The packages that the operands belong to match those of the old ones.
|
|
1841
|
+
- If input tensors are on GPU, the device must match.
|
|
1842
|
+
|
|
1843
|
+
Examples:
|
|
1844
|
+
|
|
1845
|
+
>>> import cupy as cp
|
|
1846
|
+
>>> import nvmath
|
|
1847
|
+
|
|
1848
|
+
Create two 3-D float64 ndarrays on the GPU:
|
|
1849
|
+
|
|
1850
|
+
>>> M, N, K = 128, 128, 256
|
|
1851
|
+
>>> a = cp.random.rand(M, K)
|
|
1852
|
+
>>> b = cp.random.rand(K, N)
|
|
1853
|
+
|
|
1854
|
+
Create an matrix multiplication object as a context manager
|
|
1855
|
+
|
|
1856
|
+
>>> with nvmath.linalg.Matmul(a, b) as mm:
|
|
1857
|
+
... # Plan the operation.
|
|
1858
|
+
... mm.plan()
|
|
1859
|
+
...
|
|
1860
|
+
... # Execute the MM to get the first result.
|
|
1861
|
+
... r1 = mm.execute()
|
|
1862
|
+
...
|
|
1863
|
+
... # Reset the operands to new CuPy ndarrays.
|
|
1864
|
+
... a_new = cp.random.rand(M, K)
|
|
1865
|
+
... b_new = cp.random.rand(K, N)
|
|
1866
|
+
... mm.reset_operands(a=a_new, b=b_new)
|
|
1867
|
+
...
|
|
1868
|
+
... # Execute to get the new result corresponding to the updated operands.
|
|
1869
|
+
... r2 = mm.execute()
|
|
1870
|
+
|
|
1871
|
+
Note that if only a subset of operands are reset, the operands that are not
|
|
1872
|
+
reset hold their original values.
|
|
1873
|
+
|
|
1874
|
+
With :meth:`reset_operands`, minimal overhead is achieved as problem
|
|
1875
|
+
specification and planning are only performed once.
|
|
1876
|
+
|
|
1877
|
+
For the particular example above, a slower alternative to calling
|
|
1878
|
+
:meth:`reset_operands` would be to modify the operands in-place (e.g.,
|
|
1879
|
+
``a[:] = a_new`` and ``b[:] = b_new``), but this approach is less efficient
|
|
1880
|
+
as it involves copying data rather than just updating pointers.
|
|
1881
|
+
|
|
1882
|
+
For more details, please refer to `inplace update example
|
|
1883
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/linalg/generic/matmul/example05_stateful_inplace.py>`_.
|
|
1884
|
+
"""
|
|
1885
|
+
# When operands have not been released and all arguments are None, there is
|
|
1886
|
+
# nothing to update, so reject the call. In the released state we fall through to
|
|
1887
|
+
# the delegate, which enforces post-release completeness.
|
|
1888
|
+
if not self._operands_released and all(arg is None for arg in (a, b, c, alpha, beta)):
|
|
1889
|
+
raise ValueError("reset_operands() requires at least one operand to be provided.")
|
|
1890
|
+
|
|
1891
|
+
self._delegate.reset_operands(
|
|
1892
|
+
a=a,
|
|
1893
|
+
b=b,
|
|
1894
|
+
c=c,
|
|
1895
|
+
alpha=alpha,
|
|
1896
|
+
beta=beta,
|
|
1897
|
+
stream=stream,
|
|
1898
|
+
# _operands_released is forwarded to the delegate so it can enforce
|
|
1899
|
+
# the post-release requirement that reset_operands() must receive every
|
|
1900
|
+
# operand so the operation has a fully valid state to execute on.
|
|
1901
|
+
_operands_released=self._operands_released,
|
|
1902
|
+
)
|
|
1903
|
+
# Clear the released flag since we now have valid operands, this is
|
|
1904
|
+
# guaranteed since the delegate above raised if the post-release
|
|
1905
|
+
# contract was violated.
|
|
1906
|
+
self._operands_released = False
|
|
1907
|
+
|
|
1908
|
+
def reset_operands_unchecked(
|
|
1909
|
+
self,
|
|
1910
|
+
*,
|
|
1911
|
+
a=None,
|
|
1912
|
+
b=None,
|
|
1913
|
+
c=None,
|
|
1914
|
+
alpha=None,
|
|
1915
|
+
beta=None,
|
|
1916
|
+
stream: AnyStream | int | None = None,
|
|
1917
|
+
):
|
|
1918
|
+
"""
|
|
1919
|
+
{reset_operands_unchecked}
|
|
1920
|
+
"""
|
|
1921
|
+
# The unchecked path waives all validation, so forwarding _operands_released
|
|
1922
|
+
# is not needed because the parameter would go unused anyway.
|
|
1923
|
+
# The caller is responsible for providing a complete and consistent operand set.
|
|
1924
|
+
self._delegate.reset_operands_unchecked(a=a, b=b, c=c, alpha=alpha, beta=beta, stream=stream)
|
|
1925
|
+
self._operands_released = False
|
|
1926
|
+
|
|
1927
|
+
@utils.precondition(_check_valid_matmul)
|
|
1928
|
+
def release_operands(self):
|
|
1929
|
+
"""
|
|
1930
|
+
{release_operands}
|
|
1931
|
+
"""
|
|
1932
|
+
if self._operands_released:
|
|
1933
|
+
self._logger.info("Operands have already been released; nothing to do.")
|
|
1934
|
+
return
|
|
1935
|
+
self._delegate.release_operands()
|
|
1936
|
+
self._operands_released = True
|
|
1937
|
+
self._logger.info("User-provided operands have been released.")
|
|
1938
|
+
|
|
1939
|
+
def free(self):
|
|
1940
|
+
"""Free Matmul resources.
|
|
1941
|
+
|
|
1942
|
+
It is recommended that the :class:`Matmul` object be used within a context,
|
|
1943
|
+
but if it is not possible then this method must be called explicitly to ensure
|
|
1944
|
+
that the Matmul object doesn't hold unnecessary references to operands.
|
|
1945
|
+
"""
|
|
1946
|
+
if not self.valid_state:
|
|
1947
|
+
return
|
|
1948
|
+
try:
|
|
1949
|
+
self._delegate.free()
|
|
1950
|
+
except Exception as e:
|
|
1951
|
+
self._logger.critical("Internal error: only part of the Matmul object's resources have been released.")
|
|
1952
|
+
self._logger.critical(str(e))
|
|
1953
|
+
raise e
|
|
1954
|
+
finally:
|
|
1955
|
+
self.valid_state = False
|
|
1956
|
+
|
|
1957
|
+
self._logger.info("The Matmul object's resources have been released.")
|
|
1958
|
+
|
|
1959
|
+
def __enter__(self):
|
|
1960
|
+
return self
|
|
1961
|
+
|
|
1962
|
+
def __exit__(self, *args, **kwargs):
|
|
1963
|
+
self.free()
|
|
1964
|
+
|
|
1965
|
+
|
|
1966
|
+
@utils.docstring_decorator(GENERIC_MM_DOCUMENTATION, skip_missing=False)
|
|
1967
|
+
def matmul(
|
|
1968
|
+
a: AnyTensor | Sequence[AnyTensor],
|
|
1969
|
+
b: AnyTensor | Sequence[AnyTensor],
|
|
1970
|
+
/,
|
|
1971
|
+
c: AnyTensor | Sequence[AnyTensor] | None = None,
|
|
1972
|
+
*,
|
|
1973
|
+
alpha: float | complex | Sequence[float | complex] | None = None,
|
|
1974
|
+
beta: float | complex | Sequence[float | complex] | None = None,
|
|
1975
|
+
qualifiers: MatrixQualifier | Sequence[MatrixQualifier] | None = None,
|
|
1976
|
+
options: MatmulOptions | dict[str, Any] | None = None,
|
|
1977
|
+
execution: ExecutionCPU | ExecutionCUDA | str | None = None,
|
|
1978
|
+
stream: AnyStream | int | None = None,
|
|
1979
|
+
) -> AnyTensor | Sequence[AnyTensor]:
|
|
1980
|
+
"""
|
|
1981
|
+
Perform the specified matrix multiplication computation :math:`\\alpha a @ b + \\beta
|
|
1982
|
+
c`. This function-form is a wrapper around the stateful :class:`Matmul` object APIs and
|
|
1983
|
+
is meant for *single* use (the user needs to perform just one matrix multiplication, for
|
|
1984
|
+
example), in which case there is no possibility of amortizing preparatory costs.
|
|
1985
|
+
|
|
1986
|
+
Detailed information on what's happening within this function can be obtained by passing
|
|
1987
|
+
in a :class:`logging.Logger` object to :class:`MatmulOptions` or by setting the
|
|
1988
|
+
appropriate options in the root logger object, which is used by default:
|
|
1989
|
+
|
|
1990
|
+
>>> import logging
|
|
1991
|
+
>>> logging.basicConfig(
|
|
1992
|
+
... level=logging.INFO,
|
|
1993
|
+
... format="%(asctime)s %(levelname)-8s %(message)s",
|
|
1994
|
+
... datefmt="%m-%d %H:%M:%S",
|
|
1995
|
+
... )
|
|
1996
|
+
|
|
1997
|
+
A user can select the desired logging level and, in general, take advantage of all of
|
|
1998
|
+
the functionality offered by the Python `logging` module.
|
|
1999
|
+
|
|
2000
|
+
Args:
|
|
2001
|
+
a: {a}
|
|
2002
|
+
|
|
2003
|
+
b: {b}
|
|
2004
|
+
|
|
2005
|
+
c: {c}
|
|
2006
|
+
|
|
2007
|
+
alpha: {alpha}
|
|
2008
|
+
|
|
2009
|
+
beta: {beta}
|
|
2010
|
+
|
|
2011
|
+
qualifiers: {qualifiers}
|
|
2012
|
+
|
|
2013
|
+
options: {options}
|
|
2014
|
+
|
|
2015
|
+
execution: {execution}
|
|
2016
|
+
|
|
2017
|
+
stream: {stream}
|
|
2018
|
+
|
|
2019
|
+
Returns:
|
|
2020
|
+
{result}
|
|
2021
|
+
|
|
2022
|
+
Semantics:
|
|
2023
|
+
{semantics}
|
|
2024
|
+
|
|
2025
|
+
.. seealso::
|
|
2026
|
+
:class:`Matmul`, :class:`MatmulOptions`, :class:`matrix_qualifiers_dtype`,
|
|
2027
|
+
:class:`MatrixQualifier`
|
|
2028
|
+
|
|
2029
|
+
Examples:
|
|
2030
|
+
|
|
2031
|
+
>>> import cupy as cp
|
|
2032
|
+
>>> import nvmath
|
|
2033
|
+
|
|
2034
|
+
Create three float32 ndarrays on the GPU:
|
|
2035
|
+
|
|
2036
|
+
>>> M, N, K = 128, 64, 256
|
|
2037
|
+
>>> a = cp.random.rand(M, K, dtype=cp.float32)
|
|
2038
|
+
>>> b = cp.random.rand(K, N, dtype=cp.float32)
|
|
2039
|
+
>>> c = cp.random.rand(M, N, dtype=cp.float32)
|
|
2040
|
+
|
|
2041
|
+
Perform the operation :math:`\\alpha a @ b + \\beta c` using :func:`matmul`. The
|
|
2042
|
+
result `r` is also a CuPy float32 ndarray:
|
|
2043
|
+
|
|
2044
|
+
>>> r = nvmath.linalg.matmul(a, b, c, alpha=1.23, beta=0.74)
|
|
2045
|
+
|
|
2046
|
+
The package current stream is used by default, but a stream can be explicitly
|
|
2047
|
+
provided to the Matmul operation. This can be done if the operands are computed on a
|
|
2048
|
+
different stream, for example:
|
|
2049
|
+
|
|
2050
|
+
>>> s = cp.cuda.Stream()
|
|
2051
|
+
>>> with s:
|
|
2052
|
+
... a = cp.random.rand(M, K)
|
|
2053
|
+
... b = cp.random.rand(K, N)
|
|
2054
|
+
>>> r = nvmath.linalg.matmul(a, b, stream=s)
|
|
2055
|
+
|
|
2056
|
+
The operation above runs on stream `s` and is ordered with respect to the input
|
|
2057
|
+
computation.
|
|
2058
|
+
|
|
2059
|
+
Create NumPy ndarrays on the CPU.
|
|
2060
|
+
|
|
2061
|
+
>>> import numpy as np
|
|
2062
|
+
>>> a = np.random.rand(M, K)
|
|
2063
|
+
>>> b = np.random.rand(K, N)
|
|
2064
|
+
|
|
2065
|
+
Provide the NumPy ndarrays to :func:`matmul`, with the result also being a NumPy
|
|
2066
|
+
ndarray:
|
|
2067
|
+
|
|
2068
|
+
>>> r = nvmath.linalg.matmul(a, b)
|
|
2069
|
+
|
|
2070
|
+
Notes:
|
|
2071
|
+
- This function is a convenience wrapper around :class:`Matmul` and is
|
|
2072
|
+
specifically meant for *single* use.
|
|
2073
|
+
|
|
2074
|
+
Further examples can be found in the `examples/linalg/generic/matmul
|
|
2075
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/linalg/generic/matmul>`_
|
|
2076
|
+
directory.
|
|
2077
|
+
"""
|
|
2078
|
+
|
|
2079
|
+
with Matmul(
|
|
2080
|
+
a,
|
|
2081
|
+
b,
|
|
2082
|
+
c=c,
|
|
2083
|
+
alpha=alpha,
|
|
2084
|
+
beta=beta,
|
|
2085
|
+
qualifiers=qualifiers,
|
|
2086
|
+
options=options,
|
|
2087
|
+
execution=execution,
|
|
2088
|
+
stream=stream,
|
|
2089
|
+
) as mm:
|
|
2090
|
+
mm.plan(stream=stream)
|
|
2091
|
+
|
|
2092
|
+
r = mm.execute(stream=stream)
|
|
2093
|
+
|
|
2094
|
+
return r
|