nvmath-python 0.5.0__cp313-cp313-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nvmath/__init__.pxd +0 -0
- nvmath/__init__.py +37 -0
- nvmath/_internal/__init__.py +0 -0
- nvmath/_internal/layout.py +58 -0
- nvmath/_utils.py +363 -0
- nvmath/bindings/__init__.pxd +0 -0
- nvmath/bindings/__init__.py +44 -0
- nvmath/bindings/_internal/__init__.pxd +0 -0
- nvmath/bindings/_internal/__init__.py +0 -0
- nvmath/bindings/_internal/cublas.cp313-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cublas.pxd +518 -0
- nvmath/bindings/_internal/cublasLt.cp313-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cublasLt.pxd +54 -0
- nvmath/bindings/_internal/cudss.cp313-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cudss.pxd +44 -0
- nvmath/bindings/_internal/cufft.cp313-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cufft.pxd +69 -0
- nvmath/bindings/_internal/cufftMp.pxd +77 -0
- nvmath/bindings/_internal/curand.cp313-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/curand.pxd +42 -0
- nvmath/bindings/_internal/cusolver.cp313-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cusolver.pxd +15 -0
- nvmath/bindings/_internal/cusolverDn.cp313-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cusolverDn.pxd +386 -0
- nvmath/bindings/_internal/cusparse.cp313-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cusparse.pxd +270 -0
- nvmath/bindings/_internal/mathdx.cp313-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/mathdx.pxd +79 -0
- nvmath/bindings/_internal/nvshmem.pxd +29 -0
- nvmath/bindings/_internal/utils.cp313-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/utils.pxd +186 -0
- nvmath/bindings/cublas.cp313-win_amd64.pyd +0 -0
- nvmath/bindings/cublas.pxd +545 -0
- nvmath/bindings/cublas.pyi +18 -0
- nvmath/bindings/cublasLt.cp313-win_amd64.pyd +0 -0
- nvmath/bindings/cublasLt.pxd +99 -0
- nvmath/bindings/cudss.cp313-win_amd64.pyd +0 -0
- nvmath/bindings/cudss.pxd +83 -0
- nvmath/bindings/cudss.pyi +249 -0
- nvmath/bindings/cufft.cp313-win_amd64.pyd +0 -0
- nvmath/bindings/cufft.pxd +114 -0
- nvmath/bindings/cufftMp.pxd +124 -0
- nvmath/bindings/curand.cp313-win_amd64.pyd +0 -0
- nvmath/bindings/curand.pxd +71 -0
- nvmath/bindings/cusolver.cp313-win_amd64.pyd +0 -0
- nvmath/bindings/cusolver.pxd +60 -0
- nvmath/bindings/cusolverDn.cp313-win_amd64.pyd +0 -0
- nvmath/bindings/cusolverDn.pxd +410 -0
- nvmath/bindings/cusparse.cp313-win_amd64.pyd +0 -0
- nvmath/bindings/cusparse.pxd +329 -0
- nvmath/bindings/cycublas.cp313-win_amd64.pyd +0 -0
- nvmath/bindings/cycublas.pxd +657 -0
- nvmath/bindings/cycublasLt.cp313-win_amd64.pyd +0 -0
- nvmath/bindings/cycublasLt.pxd +1010 -0
- nvmath/bindings/cycudss.cp313-win_amd64.pyd +0 -0
- nvmath/bindings/cycudss.pxd +191 -0
- nvmath/bindings/cycufft.cp313-win_amd64.pyd +0 -0
- nvmath/bindings/cycufft.pxd +320 -0
- nvmath/bindings/cycufftMp.pxd +333 -0
- nvmath/bindings/cycurand.cp313-win_amd64.pyd +0 -0
- nvmath/bindings/cycurand.pxd +146 -0
- nvmath/bindings/cycusolver.cp313-win_amd64.pyd +0 -0
- nvmath/bindings/cycusolver.pxd +143 -0
- nvmath/bindings/cycusolverDn.cp313-win_amd64.pyd +0 -0
- nvmath/bindings/cycusolverDn.pxd +414 -0
- nvmath/bindings/cycusparse.cp313-win_amd64.pyd +0 -0
- nvmath/bindings/cycusparse.pxd +476 -0
- nvmath/bindings/cymathdx.cp313-win_amd64.pyd +0 -0
- nvmath/bindings/cymathdx.pxd +363 -0
- nvmath/bindings/cynvshmem.pxd +126 -0
- nvmath/bindings/mathdx.cp313-win_amd64.pyd +0 -0
- nvmath/bindings/mathdx.pxd +131 -0
- nvmath/bindings/mathdx.pyi +686 -0
- nvmath/bindings/nvpl/__init__.pxd +0 -0
- nvmath/bindings/nvpl/__init__.py +11 -0
- nvmath/bindings/nvpl/_internal/__init__.pxd +0 -0
- nvmath/bindings/nvpl/_internal/__init__.py +0 -0
- nvmath/bindings/nvpl/_internal/fft.pxd +36 -0
- nvmath/bindings/nvpl/cyfft.pxd +87 -0
- nvmath/bindings/nvpl/fft.pxd +100 -0
- nvmath/bindings/nvshmem.pxd +54 -0
- nvmath/device/__init__.py +23 -0
- nvmath/device/_deprecated.py +33 -0
- nvmath/device/caching.py +51 -0
- nvmath/device/common.py +131 -0
- nvmath/device/common_backend.py +125 -0
- nvmath/device/common_cuda.py +156 -0
- nvmath/device/common_mathdx.py +97 -0
- nvmath/device/common_numba.py +159 -0
- nvmath/device/common_opaque_tensor.py +128 -0
- nvmath/device/cublasdx.py +1085 -0
- nvmath/device/cublasdx_backend.py +552 -0
- nvmath/device/cublasdx_numba.py +528 -0
- nvmath/device/cufftdx.py +551 -0
- nvmath/device/cufftdx_backend.py +194 -0
- nvmath/device/cufftdx_numba.py +123 -0
- nvmath/device/curand_kernel.py +9147 -0
- nvmath/device/nvrtc.py +95 -0
- nvmath/device/patch.py +35 -0
- nvmath/device/random.py +417 -0
- nvmath/device/random_helpers.py +23 -0
- nvmath/device/random_states.py +189 -0
- nvmath/device/types.py +11 -0
- nvmath/device/vector_types_numba.py +203 -0
- nvmath/distributed/__init__.py +117 -0
- nvmath/distributed/_internal/__init__.py +0 -0
- nvmath/distributed/_internal/nvshmem.py +243 -0
- nvmath/distributed/_internal/tensor_ifc_cupy.py +111 -0
- nvmath/distributed/_internal/tensor_ifc_numpy.py +53 -0
- nvmath/distributed/_internal/tensor_ifc_torch.py +108 -0
- nvmath/distributed/_internal/tensor_wrapper.py +59 -0
- nvmath/distributed/_utils.py +152 -0
- nvmath/distributed/fft/__init__.py +6 -0
- nvmath/distributed/fft/_configuration.py +86 -0
- nvmath/distributed/fft/fft.py +1737 -0
- nvmath/distributed/reshape/__init__.py +6 -0
- nvmath/distributed/reshape/_configuration.py +39 -0
- nvmath/distributed/reshape/reshape.py +1249 -0
- nvmath/fft/__init__.py +7 -0
- nvmath/fft/_configuration.py +208 -0
- nvmath/fft/_exec_utils.py +116 -0
- nvmath/fft/_helpers.py +260 -0
- nvmath/fft/fft.py +2346 -0
- nvmath/internal/__init__.py +5 -0
- nvmath/internal/enum_utils.py +142 -0
- nvmath/internal/formatters.py +87 -0
- nvmath/internal/mem_limit.py +51 -0
- nvmath/internal/package_ifc.py +119 -0
- nvmath/internal/package_ifc_cuda.py +43 -0
- nvmath/internal/package_ifc_cupy.py +49 -0
- nvmath/internal/package_ifc_torch.py +31 -0
- nvmath/internal/package_wrapper.py +14 -0
- nvmath/internal/tensor_ifc.py +143 -0
- nvmath/internal/tensor_ifc_cupy.py +185 -0
- nvmath/internal/tensor_ifc_numpy.py +157 -0
- nvmath/internal/tensor_ifc_torch.py +115 -0
- nvmath/internal/tensor_wrapper.py +160 -0
- nvmath/internal/typemaps.py +154 -0
- nvmath/internal/utils.py +634 -0
- nvmath/linalg/__init__.py +11 -0
- nvmath/linalg/_internal/__init__.py +3 -0
- nvmath/linalg/_internal/algo_cap_ifc.py +86 -0
- nvmath/linalg/_internal/algo_config_ifc.py +87 -0
- nvmath/linalg/_internal/enum_to_tuples.py +64 -0
- nvmath/linalg/_internal/epilog_protocol.py +738 -0
- nvmath/linalg/_internal/matmul_desc_ifc.py +72 -0
- nvmath/linalg/_internal/matmul_pref_ifc.py +65 -0
- nvmath/linalg/_internal/matrix_layout_ifc.py +59 -0
- nvmath/linalg/_internal/typemaps.py +135 -0
- nvmath/linalg/_internal/utils.py +96 -0
- nvmath/linalg/advanced/__init__.py +8 -0
- nvmath/linalg/advanced/_algorithmmod.py +142 -0
- nvmath/linalg/advanced/_configuration.py +320 -0
- nvmath/linalg/advanced/helpers/__init__.py +5 -0
- nvmath/linalg/advanced/helpers/matmul.py +173 -0
- nvmath/linalg/advanced/matmulmod.py +2855 -0
- nvmath/memory.py +266 -0
- nvmath/sparse/__init__.py +18 -0
- nvmath/sparse/_internal/__init__.py +21 -0
- nvmath/sparse/_internal/common_utils.py +81 -0
- nvmath/sparse/_internal/cudss_config_ifc.py +584 -0
- nvmath/sparse/_internal/cudss_data_ifc.py +291 -0
- nvmath/sparse/_internal/cudss_utils.py +398 -0
- nvmath/sparse/_internal/sparse_csr_ifc.py +154 -0
- nvmath/sparse/_internal/sparse_format_helpers.py +88 -0
- nvmath/sparse/_internal/sparse_tensor_ifc.py +80 -0
- nvmath/sparse/advanced/__init__.py +7 -0
- nvmath/sparse/advanced/_configuration.py +150 -0
- nvmath/sparse/advanced/direct_solver.py +1705 -0
- nvmath_python-0.5.0.dist-info/METADATA +122 -0
- nvmath_python-0.5.0.dist-info/RECORD +174 -0
- nvmath_python-0.5.0.dist-info/WHEEL +5 -0
- nvmath_python-0.5.0.dist-info/licenses/LICENSE +177 -0
- nvmath_python-0.5.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,2855 @@
|
|
|
1
|
+
# Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
__all__ = ["MatmulComputeType", "Matmul", "matmul"]
|
|
6
|
+
|
|
7
|
+
import copy
|
|
8
|
+
from collections import namedtuple
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
import functools
|
|
12
|
+
import logging
|
|
13
|
+
import operator
|
|
14
|
+
import typing
|
|
15
|
+
import random
|
|
16
|
+
|
|
17
|
+
import cuda.core.experimental as ccx
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
import cupy as cp
|
|
21
|
+
except ImportError:
|
|
22
|
+
cp = None
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
from nvmath import memory
|
|
26
|
+
|
|
27
|
+
from nvmath.linalg.advanced import _algorithmmod
|
|
28
|
+
from nvmath.linalg.advanced import _configuration
|
|
29
|
+
from nvmath.bindings import cublas
|
|
30
|
+
from nvmath.bindings import cublasLt as cublaslt # type: ignore
|
|
31
|
+
|
|
32
|
+
from nvmath.internal import formatters
|
|
33
|
+
from nvmath.internal import tensor_wrapper
|
|
34
|
+
from nvmath.internal import typemaps
|
|
35
|
+
from nvmath.internal import utils
|
|
36
|
+
|
|
37
|
+
from nvmath.linalg._internal import matmul_desc_ifc, matmul_pref_ifc, matrix_layout_ifc
|
|
38
|
+
from nvmath.linalg._internal.typemaps import (
|
|
39
|
+
NAMES_TO_DEFAULT_SCALE_TYPE,
|
|
40
|
+
NAMES_TO_DEFAULT_COMPUTE_TYPE,
|
|
41
|
+
COMPUTE_TYPE_TO_DEFAULT_SCALE_TYPE,
|
|
42
|
+
SCALE_TYPE_TO_DEFAULT_COMPUTE_TYPE,
|
|
43
|
+
SUPPORTED_TYPES,
|
|
44
|
+
)
|
|
45
|
+
from nvmath.linalg._internal.utils import (
|
|
46
|
+
axis_order_in_memory,
|
|
47
|
+
calculate_strides,
|
|
48
|
+
check_batch_tileable,
|
|
49
|
+
get_handle,
|
|
50
|
+
pointer_aligned_to,
|
|
51
|
+
)
|
|
52
|
+
from nvmath.linalg._internal.epilog_protocol import (
|
|
53
|
+
EPILOG_INPUT_HANDLERS_MAP,
|
|
54
|
+
EPILOG_OUTPUT_HANDLERS_MAP,
|
|
55
|
+
EPILOG_MINIMUM_VERSIONS_MAP,
|
|
56
|
+
BATCHED_EPILOG_MINIMUM_VERSIONS_MAP,
|
|
57
|
+
EpilogOutputHandler,
|
|
58
|
+
)
|
|
59
|
+
from nvmath._utils import CudaDataType
|
|
60
|
+
|
|
61
|
+
MatmulComputeType = cublas.ComputeType
|
|
62
|
+
|
|
63
|
+
EpilogInputTraits = namedtuple("EpilogInputTraits", ["dtype", "extents", "strides"])
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class MatrixLayout:
|
|
68
|
+
"""An internal data class for capturing the tensor layout."""
|
|
69
|
+
|
|
70
|
+
shape: Sequence[int]
|
|
71
|
+
strides: Sequence[int]
|
|
72
|
+
is_conjugate: bool = False # Used to support is_conjugate via conjugate_transpose.
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class LayoutTraits:
|
|
77
|
+
"""An internal data class for capturing the matrix multiplication traits."""
|
|
78
|
+
|
|
79
|
+
order: cublaslt.Order
|
|
80
|
+
ld: int
|
|
81
|
+
batch_offset: int # Based on strides
|
|
82
|
+
is_conjugate: bool # Used to support is_conjugate via conjugate_transpose.
|
|
83
|
+
mm_shape: Sequence[int] | None = None
|
|
84
|
+
mm_strides: Sequence[int] | None = None
|
|
85
|
+
|
|
86
|
+
def get_mm_layout(self, transpose=False):
|
|
87
|
+
if self.is_conjugate:
|
|
88
|
+
transpose = True
|
|
89
|
+
if not transpose:
|
|
90
|
+
return *self.mm_shape, self.ld, self.order
|
|
91
|
+
|
|
92
|
+
# Use of transpose is supported only for A and B for two specific use cases till the
|
|
93
|
+
# C library directly supports these use cases:
|
|
94
|
+
#
|
|
95
|
+
# 1. When A or B has the conjugate qualifier, we transpose it internally and then
|
|
96
|
+
# use conjugate transpose in the MM (A @ B.conj() == A @ B.T.H).
|
|
97
|
+
#
|
|
98
|
+
# 2. When the epilog is BGRADB, we transpose B internally and use transpose in the
|
|
99
|
+
# MM since this epilog requires B to be transposed (A @ B == A @ B.T.T).
|
|
100
|
+
#
|
|
101
|
+
# This requires that the layout order be ROW or COL (no special layouts such as
|
|
102
|
+
# structured or hierarchical).
|
|
103
|
+
assert self.mm_shape is not None and self.mm_strides is not None, "Internal Error."
|
|
104
|
+
assert self.ld != 0, "Internal Error."
|
|
105
|
+
|
|
106
|
+
mm_shape = self.mm_shape[1], self.mm_shape[0]
|
|
107
|
+
if self.order == cublaslt.Order.ROW:
|
|
108
|
+
order = cublaslt.Order.COL
|
|
109
|
+
ld = max(self.mm_shape[1], self.mm_strides[0])
|
|
110
|
+
elif self.order == cublaslt.Order.COL:
|
|
111
|
+
order = cublaslt.Order.ROW
|
|
112
|
+
ld = max(self.mm_shape[0], self.mm_strides[1])
|
|
113
|
+
else:
|
|
114
|
+
raise AssertionError("Internal Error. Invalid layout order.")
|
|
115
|
+
|
|
116
|
+
return *mm_shape, ld, order
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class MMTraits:
|
|
121
|
+
"""An internal data class for capturing the matrix multiplication traits. The
|
|
122
|
+
result traits are captured separately, because we need to wait for the
|
|
123
|
+
epilog to be provided.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
M: int
|
|
127
|
+
N: int
|
|
128
|
+
K: int
|
|
129
|
+
d_mm_shape: Sequence[int]
|
|
130
|
+
a_layout_traits: LayoutTraits
|
|
131
|
+
b_layout_traits: LayoutTraits
|
|
132
|
+
c_layout_traits: LayoutTraits
|
|
133
|
+
batch_count: int
|
|
134
|
+
batch_shape: Sequence[int]
|
|
135
|
+
batch_axis_order: Sequence[int]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass
|
|
139
|
+
class ResultTraits:
|
|
140
|
+
"""An internal data class for capturing the result matrix's traits."""
|
|
141
|
+
|
|
142
|
+
d_layout_traits: LayoutTraits
|
|
143
|
+
result_shape: Sequence[int]
|
|
144
|
+
result_strides: Sequence[int]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def get_matrix_layout_traits(
|
|
148
|
+
mm_shape: Sequence[int],
|
|
149
|
+
mm_strides: Sequence[int],
|
|
150
|
+
batch_strides: Sequence[int],
|
|
151
|
+
col_bcast: bool,
|
|
152
|
+
ordering: cublaslt.Order | None = None,
|
|
153
|
+
orientation: cublaslt.Order | None = None,
|
|
154
|
+
) -> tuple[cublaslt.Order, int, int]:
|
|
155
|
+
"""
|
|
156
|
+
The 'ordering' option specifies the layout order, if it's not None, as in the case of
|
|
157
|
+
the D matrix whose layout is determined by the other operands' layout. It is required if
|
|
158
|
+
the matrix is degenerate (a vector or scalar: len(mm_shape) < 2).
|
|
159
|
+
|
|
160
|
+
The 'orientation' option (ROW or COL) is required to infer the correct leading dimension
|
|
161
|
+
for degenerate matrices (a vector or scalar: len(mm_shape) < 2).
|
|
162
|
+
"""
|
|
163
|
+
if len(mm_shape) < 2: # The result D can be a scalar or vector.
|
|
164
|
+
assert ordering is not None, "Internal Error: 'ordering' must be specified for degenerate matrices."
|
|
165
|
+
assert orientation is not None, "Internal Error: 'orientation' must be specified for degenerate matrices."
|
|
166
|
+
batch_offset = min(batch_strides) if batch_strides else 0
|
|
167
|
+
order = ordering
|
|
168
|
+
if len(mm_shape) < 1:
|
|
169
|
+
ld = 1
|
|
170
|
+
elif order != orientation:
|
|
171
|
+
# For a ROW vector in COL order, the LD should be 1 since we promote the row
|
|
172
|
+
# vector to a matrix as (1, M). Similarly, for a COL vector in ROW order, the LD
|
|
173
|
+
# should be 1 as well since we promote the column vector to a matrix as (M, 1).
|
|
174
|
+
ld = 1
|
|
175
|
+
else:
|
|
176
|
+
ld = max(mm_strides[0], mm_shape[0])
|
|
177
|
+
return order, ld, batch_offset
|
|
178
|
+
|
|
179
|
+
M, N = mm_shape
|
|
180
|
+
|
|
181
|
+
if ordering is not None:
|
|
182
|
+
order = ordering
|
|
183
|
+
message = f"Internal Error: incompatible ordering '{ordering}' and strides {mm_strides}"
|
|
184
|
+
if order == cublaslt.Order.ROW:
|
|
185
|
+
assert mm_strides[0] >= mm_strides[1] and mm_strides[1] == 1, message
|
|
186
|
+
else:
|
|
187
|
+
assert mm_strides[1] >= mm_strides[0] and mm_strides[0] == 1, message
|
|
188
|
+
else:
|
|
189
|
+
# Important: start with the first dimension so that cases like (M, 1) : (1, 1) or
|
|
190
|
+
# (1, M) : (1, 1) in CuTe notation map to COL.
|
|
191
|
+
if mm_strides[0] == 1:
|
|
192
|
+
order = cublaslt.Order.COL
|
|
193
|
+
elif mm_strides[1] == 1:
|
|
194
|
+
order = cublaslt.Order.ROW
|
|
195
|
+
else:
|
|
196
|
+
if M == 1:
|
|
197
|
+
order = cublaslt.Order.COL
|
|
198
|
+
elif N == 1:
|
|
199
|
+
order = cublaslt.Order.ROW
|
|
200
|
+
else:
|
|
201
|
+
raise ValueError("Unsupported layout.")
|
|
202
|
+
|
|
203
|
+
# We need to handle broadcast dimensions with zero-stride for the c matrix.
|
|
204
|
+
if col_bcast and N == 1:
|
|
205
|
+
ld = 0
|
|
206
|
+
else:
|
|
207
|
+
ld = max(M, mm_strides[1]) if order == cublaslt.Order.COL else max(N, mm_strides[0])
|
|
208
|
+
|
|
209
|
+
# Batch dimensions should be contiguous in memory, which we have already checked. The
|
|
210
|
+
# batch_offset should be based on the lowest stride in the batch dimension to account
|
|
211
|
+
# for embedded matrices.
|
|
212
|
+
batch_offset = min(batch_strides) if batch_strides else 0
|
|
213
|
+
|
|
214
|
+
return order, ld, batch_offset
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def get_mm_traits(a_layout, b_layout, c_layout, logger):
|
|
218
|
+
"""
|
|
219
|
+
First check A and B compatibility:
|
|
220
|
+
|
|
221
|
+
1. Check MM compatibility (K):
|
|
222
|
+
a. First pad A and/or B MM dimensions if 1-D according to NumPy convention.
|
|
223
|
+
b. The padding is used to determine M, N, and K but should not appear in the output
|
|
224
|
+
dimensions.
|
|
225
|
+
c. If both A and B are N-D, the dimensions must match.
|
|
226
|
+
2. Check batch dimensions:
|
|
227
|
+
a. One of A or B can have missing batch extents, in which case it is broadcast,
|
|
228
|
+
otherwise
|
|
229
|
+
b. A and B must have the same batch ordering.
|
|
230
|
+
c. In addition, the batch dimensions must be tileable (contiguous in memory).
|
|
231
|
+
|
|
232
|
+
Then check C:
|
|
233
|
+
|
|
234
|
+
C can be None. If C is passed in, it must be matrix. Batching rule is the
|
|
235
|
+
same as above.
|
|
236
|
+
"""
|
|
237
|
+
a_shape, a_strides = list(a_layout.shape), list(a_layout.strides)
|
|
238
|
+
b_shape, b_strides = list(b_layout.shape), list(b_layout.strides)
|
|
239
|
+
|
|
240
|
+
a_batch_shape, a_mm_shape = a_shape[:-2], a_shape[-2:]
|
|
241
|
+
b_batch_shape, b_mm_shape = b_shape[:-2], b_shape[-2:]
|
|
242
|
+
|
|
243
|
+
a_batch_strides, a_mm_strides = a_strides[:-2], a_strides[-2:]
|
|
244
|
+
b_batch_strides, b_mm_strides = b_strides[:-2], b_strides[-2:]
|
|
245
|
+
d_mm_shape = []
|
|
246
|
+
if len(a_mm_shape) == 1:
|
|
247
|
+
s, d = a_mm_shape[0], a_mm_strides[0]
|
|
248
|
+
a_mm_shape = [1] + a_mm_shape
|
|
249
|
+
a_mm_strides = [s * d] + a_mm_strides
|
|
250
|
+
else:
|
|
251
|
+
d_mm_shape.append(a_mm_shape[0]) # The first mode for d applies only when a is not a vector.
|
|
252
|
+
|
|
253
|
+
if len(b_mm_shape) == 1:
|
|
254
|
+
s, d = b_mm_shape[0], b_mm_strides[0]
|
|
255
|
+
b_mm_shape = b_mm_shape + [1]
|
|
256
|
+
b_mm_strides = b_mm_strides + [s * d]
|
|
257
|
+
else:
|
|
258
|
+
d_mm_shape.append(b_mm_shape[1]) # The second mode for d applies only when b is not a vector.
|
|
259
|
+
|
|
260
|
+
logger.debug(f"The MM shape for operand A is {a_mm_shape} with strides {a_mm_strides}.")
|
|
261
|
+
logger.debug(f"The MM shape for operand B is {b_mm_shape} with strides {b_mm_strides}.")
|
|
262
|
+
logger.debug(f"The MM shape for operand D is {d_mm_shape}.")
|
|
263
|
+
|
|
264
|
+
M0, K0 = a_mm_shape
|
|
265
|
+
K1, N0 = b_mm_shape
|
|
266
|
+
if K0 != K1:
|
|
267
|
+
raise ValueError(
|
|
268
|
+
f"The 'K' extent must match for the operands: K={K0} in operand A is not equal to K={K1} in operand B."
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Check if batch dimensions of A and B are tileable as well as compatible.
|
|
272
|
+
batch_shape, batch_axis_order = [], ()
|
|
273
|
+
if len(a_batch_shape) > 0:
|
|
274
|
+
if not check_batch_tileable(a_batch_shape, a_batch_strides):
|
|
275
|
+
message = (
|
|
276
|
+
f"The batch layout for A corresponding to shape = {a_batch_shape} and strides = {a_batch_strides} "
|
|
277
|
+
"is currently not supported because it is not tileable."
|
|
278
|
+
)
|
|
279
|
+
raise ValueError(message)
|
|
280
|
+
logger.debug(
|
|
281
|
+
f"The batch layout for A corresponding to shape = {a_batch_shape} and strides = {a_batch_strides} IS tileable."
|
|
282
|
+
)
|
|
283
|
+
batch_shape = a_batch_shape
|
|
284
|
+
batch_axis_order = a_batch_axis_order = axis_order_in_memory(a_batch_strides)
|
|
285
|
+
|
|
286
|
+
if len(b_batch_shape) > 0:
|
|
287
|
+
if not check_batch_tileable(b_batch_shape, b_batch_strides):
|
|
288
|
+
message = (
|
|
289
|
+
f"The batch layout for B corresponding to shape = {b_batch_shape} and strides = {b_batch_strides} "
|
|
290
|
+
"is currently not supported because it is not tileable."
|
|
291
|
+
)
|
|
292
|
+
raise ValueError(message)
|
|
293
|
+
logger.debug(
|
|
294
|
+
f"The batch layout for B corresponding to shape = {b_batch_shape} and strides = {b_batch_strides} IS tileable."
|
|
295
|
+
)
|
|
296
|
+
batch_shape = b_batch_shape
|
|
297
|
+
batch_axis_order = b_batch_axis_order = axis_order_in_memory(b_batch_strides)
|
|
298
|
+
|
|
299
|
+
if len(a_batch_shape) > 0 and len(b_batch_shape) > 0:
|
|
300
|
+
if a_batch_shape != b_batch_shape:
|
|
301
|
+
raise ValueError(f"The batch dimensions of operands A {a_batch_shape} and B {b_batch_shape} must match.")
|
|
302
|
+
if a_batch_axis_order != b_batch_axis_order:
|
|
303
|
+
raise ValueError(f"The batch order of operands A {a_batch_axis_order} and B {b_batch_axis_order} must match.")
|
|
304
|
+
|
|
305
|
+
logger.debug(f"The batch shape is {batch_shape} with batch axis order {batch_axis_order}.")
|
|
306
|
+
|
|
307
|
+
batch_count = functools.reduce(operator.mul, batch_shape, 1)
|
|
308
|
+
|
|
309
|
+
# Create matrix layout traits.
|
|
310
|
+
a_order, a_ld, a_batch_offset = get_matrix_layout_traits(a_mm_shape, a_mm_strides, a_batch_strides, col_bcast=False)
|
|
311
|
+
a_layout_traits = LayoutTraits(
|
|
312
|
+
order=a_order,
|
|
313
|
+
ld=a_ld,
|
|
314
|
+
batch_offset=a_batch_offset,
|
|
315
|
+
is_conjugate=a_layout.is_conjugate,
|
|
316
|
+
mm_shape=a_mm_shape,
|
|
317
|
+
mm_strides=a_mm_strides,
|
|
318
|
+
)
|
|
319
|
+
logger.debug(f"The layout order for operand A is {a_order.name}, with LD {a_ld}, and batch offset {a_batch_offset}.")
|
|
320
|
+
|
|
321
|
+
b_order, b_ld, b_batch_offset = get_matrix_layout_traits(b_mm_shape, b_mm_strides, b_batch_strides, col_bcast=False)
|
|
322
|
+
b_layout_traits = LayoutTraits(
|
|
323
|
+
order=b_order,
|
|
324
|
+
ld=b_ld,
|
|
325
|
+
batch_offset=b_batch_offset,
|
|
326
|
+
is_conjugate=b_layout.is_conjugate,
|
|
327
|
+
mm_shape=b_mm_shape,
|
|
328
|
+
mm_strides=b_mm_strides,
|
|
329
|
+
)
|
|
330
|
+
logger.debug(f"The layout order for operand B is {b_order.name}, with LD {b_ld}, and batch offset {b_batch_offset}.")
|
|
331
|
+
|
|
332
|
+
# Process matrix c, if provided.
|
|
333
|
+
c_layout_traits = None
|
|
334
|
+
if c_layout is not None:
|
|
335
|
+
# 1. C cannot be a vector.
|
|
336
|
+
# 2. C can be a matrix of dimension (M, N) or (M, 1), broadcast in the latter case
|
|
337
|
+
# and has to have contiguous strides.
|
|
338
|
+
# 3. C can be batched matrices of dimension (..., M, N) or (..., M, 1), broadcast in
|
|
339
|
+
# the latter case and has to have contiguous strides.
|
|
340
|
+
c_shape, c_strides = list(c_layout.shape), list(c_layout.strides)
|
|
341
|
+
|
|
342
|
+
c_batch_shape, c_mm_shape = c_shape[:-2], c_shape[-2:]
|
|
343
|
+
c_batch_strides, c_mm_strides = c_strides[:-2], c_strides[-2:]
|
|
344
|
+
if len(c_mm_shape) == 1:
|
|
345
|
+
raise ValueError(f"C cannot be a vector. C shape: {c_mm_shape}")
|
|
346
|
+
logger.debug(f"The MM shape for operand C is {c_mm_shape} with strides {c_mm_strides}.")
|
|
347
|
+
|
|
348
|
+
Mc, Nc = c_mm_shape
|
|
349
|
+
if Mc != M0:
|
|
350
|
+
raise ValueError(f"The M dimension of the C matrix ({Mc}) must match the M dimension of A.")
|
|
351
|
+
|
|
352
|
+
if Nc != 1 and Nc != N0:
|
|
353
|
+
raise ValueError(f"The N dimension of the C matrix ({Nc}) must match the N dimension of B.")
|
|
354
|
+
|
|
355
|
+
if len(c_batch_shape) > 0:
|
|
356
|
+
if c_batch_shape != batch_shape:
|
|
357
|
+
raise ValueError(
|
|
358
|
+
f"The batch dimension of operand C {c_batch_shape} must match with that of the other operands "
|
|
359
|
+
f"{batch_shape}."
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
if (c_batch_axis_order := axis_order_in_memory(c_batch_strides)) != batch_axis_order:
|
|
363
|
+
raise ValueError(
|
|
364
|
+
f"The batch axis order of operand C {c_batch_axis_order} must match with that of the other "
|
|
365
|
+
f"operands {batch_axis_order}."
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
if not check_batch_tileable(c_batch_shape, c_batch_strides):
|
|
369
|
+
message = (
|
|
370
|
+
f"The batch layout for C corresponding to shape = {c_batch_shape} and strides = "
|
|
371
|
+
f"{c_batch_strides} is currently not supported because it is not tileable."
|
|
372
|
+
)
|
|
373
|
+
raise ValueError(message)
|
|
374
|
+
|
|
375
|
+
c_order, c_ld, c_batch_offset = get_matrix_layout_traits(c_mm_shape, c_mm_strides, c_batch_strides, col_bcast=True)
|
|
376
|
+
c_layout_traits = LayoutTraits(order=c_order, ld=c_ld, batch_offset=c_batch_offset, is_conjugate=c_layout.is_conjugate)
|
|
377
|
+
logger.debug(f"The layout order for operand C is {c_order.name}, with LD {c_ld}, and batch offset {c_batch_offset}.")
|
|
378
|
+
|
|
379
|
+
return MMTraits(
|
|
380
|
+
M=M0,
|
|
381
|
+
N=N0,
|
|
382
|
+
K=K0,
|
|
383
|
+
d_mm_shape=d_mm_shape,
|
|
384
|
+
batch_count=batch_count,
|
|
385
|
+
batch_shape=batch_shape,
|
|
386
|
+
batch_axis_order=batch_axis_order,
|
|
387
|
+
a_layout_traits=a_layout_traits,
|
|
388
|
+
b_layout_traits=b_layout_traits,
|
|
389
|
+
c_layout_traits=c_layout_traits,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def get_result_traits(mm_traits: MMTraits, epilog_ordering: cublaslt.Order, logger: logging.Logger) -> ResultTraits:
|
|
394
|
+
"""
|
|
395
|
+
epilog_ordering = value of type cublaslt.Order or None.
|
|
396
|
+
|
|
397
|
+
The result layout is determined from:
|
|
398
|
+
- the ordering of operand c, if it is provided, or
|
|
399
|
+
- the epilog requirement, if it exists, or
|
|
400
|
+
- the ordering of operand a.
|
|
401
|
+
|
|
402
|
+
The result batch dimensions must have the same extents and axis order as the inputs. The
|
|
403
|
+
MM layout can be C or F.
|
|
404
|
+
"""
|
|
405
|
+
# The result shape is the batch shape + d_mm_shape.
|
|
406
|
+
result_shape = (*mm_traits.batch_shape, *mm_traits.d_mm_shape)
|
|
407
|
+
|
|
408
|
+
if mm_traits.c_layout_traits is not None:
|
|
409
|
+
result_ordering = mm_traits.c_layout_traits.order
|
|
410
|
+
elif epilog_ordering is not None:
|
|
411
|
+
result_ordering = epilog_ordering
|
|
412
|
+
else:
|
|
413
|
+
result_ordering = mm_traits.a_layout_traits.order
|
|
414
|
+
|
|
415
|
+
if result_ordering == cublaslt.Order.ROW:
|
|
416
|
+
d_order = list(range(len(mm_traits.d_mm_shape) - 1, -1, -1))
|
|
417
|
+
elif result_ordering == cublaslt.Order.COL:
|
|
418
|
+
d_order = list(range(len(mm_traits.d_mm_shape)))
|
|
419
|
+
else:
|
|
420
|
+
raise AssertionError("Internal Error.")
|
|
421
|
+
|
|
422
|
+
result_axis_order = [len(mm_traits.batch_axis_order) + a for a in d_order] + list(mm_traits.batch_axis_order)
|
|
423
|
+
|
|
424
|
+
# Calculate the result strides.
|
|
425
|
+
result_strides = calculate_strides(result_shape, result_axis_order)
|
|
426
|
+
|
|
427
|
+
# For degenerate matrices, we need to specify the result orientation.
|
|
428
|
+
result_orientation = None
|
|
429
|
+
if len(mm_traits.d_mm_shape) < 2:
|
|
430
|
+
if mm_traits.M == 1:
|
|
431
|
+
result_orientation = cublaslt.Order.ROW
|
|
432
|
+
elif mm_traits.N == 1:
|
|
433
|
+
result_orientation = cublaslt.Order.COL
|
|
434
|
+
|
|
435
|
+
# The result's traits.
|
|
436
|
+
d_batch_strides, d_mm_strides = (
|
|
437
|
+
result_strides[: len(mm_traits.batch_shape)],
|
|
438
|
+
result_strides[len(mm_traits.batch_shape) :],
|
|
439
|
+
)
|
|
440
|
+
order, d_ld, d_batch_offset = get_matrix_layout_traits(
|
|
441
|
+
mm_traits.d_mm_shape,
|
|
442
|
+
d_mm_strides,
|
|
443
|
+
d_batch_strides,
|
|
444
|
+
col_bcast=False,
|
|
445
|
+
ordering=result_ordering,
|
|
446
|
+
orientation=result_orientation,
|
|
447
|
+
)
|
|
448
|
+
assert order == result_ordering, (
|
|
449
|
+
f"Internal Error: d_order = {order.name}, result_ordering = {result_ordering.name}, mm_traits = {mm_traits}."
|
|
450
|
+
)
|
|
451
|
+
d_layout_traits = LayoutTraits(order=order, ld=d_ld, batch_offset=d_batch_offset, is_conjugate=False)
|
|
452
|
+
logger.debug(f"The layout order for operand D is {order.name}, with LD {d_ld}, and batch offset {d_batch_offset}.")
|
|
453
|
+
|
|
454
|
+
return ResultTraits(result_shape=result_shape, result_strides=result_strides, d_layout_traits=d_layout_traits)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
SHARED_MM_DOCUMENTATION = utils.COMMON_SHARED_DOC_MAP.copy()
|
|
458
|
+
SHARED_MM_DOCUMENTATION.update(
|
|
459
|
+
{
|
|
460
|
+
"a": """\
|
|
461
|
+
A tensor representing the first operand to the matrix multiplication (see `Semantics`_). The currently supported types
|
|
462
|
+
are :class:`numpy.ndarray`, :class:`cupy.ndarray`, and :class:`torch.Tensor`.""".replace("\n", " "),
|
|
463
|
+
#
|
|
464
|
+
"b": """\
|
|
465
|
+
A tensor representing the second operand to the matrix multiplication (see `Semantics`_). The currently supported types
|
|
466
|
+
are :class:`numpy.ndarray`, :class:`cupy.ndarray`, and :class:`torch.Tensor`.""".replace("\n", " "),
|
|
467
|
+
#
|
|
468
|
+
"c": """\
|
|
469
|
+
(Optional) A tensor representing the operand to add to the matrix multiplication result (see `Semantics`_). The currently
|
|
470
|
+
supported types are :class:`numpy.ndarray`, :class:`cupy.ndarray`, and :class:`torch.Tensor`.""".replace("\n", " "),
|
|
471
|
+
#
|
|
472
|
+
"c_admonitions": """
|
|
473
|
+
.. versionchanged:: 0.3.0
|
|
474
|
+
In order to avoid broadcasting behavior ambiguity, nvmath-python no longer
|
|
475
|
+
accepts a 1-D (vector) `c`. Use a singleton dimension to convert your input
|
|
476
|
+
array to 2-D.
|
|
477
|
+
""",
|
|
478
|
+
#
|
|
479
|
+
"alpha": """\
|
|
480
|
+
The scale factor for the matrix multiplication term as a real or complex number. The default is
|
|
481
|
+
:math:`1.0`.""".replace("\n", " "),
|
|
482
|
+
#
|
|
483
|
+
"beta": """\
|
|
484
|
+
The scale factor for the matrix addition term as a real or complex number. A value for `beta` must be provided if
|
|
485
|
+
operand `c` is specified.""".replace("\n", " "),
|
|
486
|
+
#
|
|
487
|
+
"quantization_scales": """\
|
|
488
|
+
Specify scale factors for the matrix multiplication as a :class:`~nvmath.linalg.advanced.MatmulQuantizationScales`
|
|
489
|
+
object. Alternatively, a `dict` containing the parameters for the
|
|
490
|
+
:class:`~nvmath.linalg.advanced.MatmulQuantizationScales`
|
|
491
|
+
constructor can also be provided.
|
|
492
|
+
Allowed and required only for narrow-precision (FP8 and lower) operations.""".replace("\n", " "),
|
|
493
|
+
#
|
|
494
|
+
"algorithms": """\
|
|
495
|
+
A sequence of :class:`Algorithm` objects that can be directly provided to bypass planning. The algorithm objects must be
|
|
496
|
+
compatible with the matrix multiplication. A typical use for this option is to provide algorithms serialized (pickled)
|
|
497
|
+
from a previously planned and autotuned matrix multiplication.""".replace("\n", " "),
|
|
498
|
+
#
|
|
499
|
+
"epilog": """\
|
|
500
|
+
Specify an epilog :math:`F` as an object of type :class:`MatmulEpilog` to apply to the result of the matrix
|
|
501
|
+
multiplication: :math:`F(\\alpha A @ B + \\beta C`). The default is no epilog. See `cuBLASLt documentation
|
|
502
|
+
<https://docs.nvidia.com/cuda/cublas/#cublasltepilogue-t>`_ for the list of available epilogs.""".replace("\n", " "),
|
|
503
|
+
#
|
|
504
|
+
"epilog_inputs": """\
|
|
505
|
+
Specify the additional inputs needed for the selected epilog as a dictionary, where the key is the epilog input name and
|
|
506
|
+
the value is the epilog input. The epilog input must be a tensor with the same package and in the same memory space as
|
|
507
|
+
the operands (see the constructor for more information on the operands). If the required epilog inputs are not provided,
|
|
508
|
+
an exception is raised that lists the required epilog inputs. Some epilog inputs are generated by other epilogs. For
|
|
509
|
+
example, the epilog input for :class:`MatmulEpilog.DRELU` is generated by matrix multiplication with the same operands
|
|
510
|
+
using :class:`MatmulEpilog.RELU_AUX`. """.replace("\n", " "),
|
|
511
|
+
#
|
|
512
|
+
"qualifiers": """\
|
|
513
|
+
If desired, specify the matrix qualifiers as a :class:`numpy.ndarray` of
|
|
514
|
+
:class:`~nvmath.linalg.advanced.matrix_qualifiers_dtype` objects of length 3 corresponding to the operands `a`, `b`, and
|
|
515
|
+
`c`.""".replace("\n", " "),
|
|
516
|
+
#
|
|
517
|
+
"options": """\
|
|
518
|
+
Specify options for the matrix multiplication as a :class:`~nvmath.linalg.advanced.MatmulOptions` object. Alternatively,
|
|
519
|
+
a `dict` containing the parameters for the ``MatmulOptions`` constructor can also be provided. If not specified, the
|
|
520
|
+
value will be set to the default-constructed ``MatmulOptions`` object.""".replace("\n", " "),
|
|
521
|
+
#
|
|
522
|
+
"preferences": """\
|
|
523
|
+
This parameter specifies the preferences for planning as a :class:`MatmulPlanPreferences` object. Alternatively, a
|
|
524
|
+
dictionary containing the parameters for the :class:`MatmulPlanPreferences` constructor can also be provided. If not
|
|
525
|
+
specified, the value will be set to the default-constructed :class:`MatmulPlanPreferences` object.
|
|
526
|
+
""".replace("\n", " "),
|
|
527
|
+
#
|
|
528
|
+
"result": """\
|
|
529
|
+
The result of the specified matrix multiplication (epilog applied), which remains on the same device and belong to the
|
|
530
|
+
same package as the input operands. If an epilog (like :attr:`nvmath.linalg.advanced.MatmulEpilog.RELU_AUX`) that
|
|
531
|
+
results in extra output is used, or an extra output is requested (for example by setting
|
|
532
|
+
:attr:`~nvmath.linalg.advanced.MatmulOptions.result_amax` option in ``options`` argument),
|
|
533
|
+
a tuple is returned with the first element being the matrix multiplication result (epilog applied) and the second element
|
|
534
|
+
being the auxiliary output provided as a `dict`. """.replace("\n", " "),
|
|
535
|
+
#
|
|
536
|
+
"narrow_precision": """\
|
|
537
|
+
Matrix multiplication with narrow-precision operands is supported, in both FP8 and MXFP8 formats.
|
|
538
|
+
|
|
539
|
+
.. note::
|
|
540
|
+
|
|
541
|
+
Narrow-precision matrix multiplication in nvmath-python requires **CUDA Toolkit 12.8 or newer**.
|
|
542
|
+
**FP8 requires a device with compute capability 8.9 or higher** (Ada, Hopper, Blackwell or newer architecture).
|
|
543
|
+
**MXFP8 requires a device with compute capability 10.0 or higher** (Blackwell or newer architecture).
|
|
544
|
+
Please refer to the `compute capability table <https://developer.nvidia.com/cuda-gpus>`_
|
|
545
|
+
to check the compute capability of your device.
|
|
546
|
+
|
|
547
|
+
For FP8 operations:
|
|
548
|
+
|
|
549
|
+
* For each operand a scaling factor needs to be specified via ``quantization_scales`` argument.
|
|
550
|
+
* Maximum absolute value of the result (amax) can be requested via
|
|
551
|
+
:attr:`~nvmath.linalg.advanced.MatmulOptions.result_amax` option in ``options`` argument.
|
|
552
|
+
* Custom result type (both FP8 and non-FP8) can be requested via
|
|
553
|
+
:attr:`~nvmath.linalg.advanced.MatmulOptions.result_type` option in ``options`` argument.
|
|
554
|
+
|
|
555
|
+
For MXFP8 operations:
|
|
556
|
+
|
|
557
|
+
* To enable MXFP8 operations, :attr:`~nvmath.linalg.advanced.MatmulOptions.block_scaling` option
|
|
558
|
+
must be set to ``True``.
|
|
559
|
+
* Block scaling factors need to be specified via ``quantization_scales`` argument.
|
|
560
|
+
* Utilities in :mod:`nvmath.linalg.advanced.helpers.matmul` can be used to create and modify
|
|
561
|
+
block scaling factors.
|
|
562
|
+
* When MXFP8 is used and the result type is a narrow-precision data type, the auxiliary output
|
|
563
|
+
``"d_out_scale"`` will be returned in the auxiliary output tensor. It will contain the scales
|
|
564
|
+
that were used for the result quantization.
|
|
565
|
+
|
|
566
|
+
Please refer to the examples and narrow-precision operations tutorial for more details.
|
|
567
|
+
For more details on the FP8 and MXFP8 formats in cuBLAS,
|
|
568
|
+
see the `cublasLtMatmul documentation <https://docs.nvidia.com/cuda/cublas/#cublasltmatmul>`_.
|
|
569
|
+
""".strip(),
|
|
570
|
+
#
|
|
571
|
+
"semantics": """\
|
|
572
|
+
.. _semantics:
|
|
573
|
+
|
|
574
|
+
The semantics of the matrix multiplication follows :func:`numpy.matmul` semantics, with some restrictions on
|
|
575
|
+
broadcasting. In addition, the semantics for the fused matrix addition are described below:
|
|
576
|
+
|
|
577
|
+
* If arguments `a` and `b` are matrices, they are multiplied according to the rules of matrix multiplication.
|
|
578
|
+
* If argument `a` is 1-D, it is promoted to a matrix by prefixing ``1`` to its dimensions. After matrix
|
|
579
|
+
multiplication, the prefixed ``1`` is removed from the result's dimensions.
|
|
580
|
+
* If argument `b` is 1-D, it is promoted to a matrix by appending ``1`` to its dimensions. After matrix
|
|
581
|
+
multiplication, the appended ``1`` is removed from the result's dimensions.
|
|
582
|
+
* If `a` or `b` is N-D (N > 2), then the operand is treated as a batch of matrices. If both `a` and `b` are N-D,
|
|
583
|
+
their batch dimensions must match. If exactly one of `a` or `b` is N-D, the other operand is broadcast.
|
|
584
|
+
* The operand for the matrix addition `c` may be a matrix of shape (M, 1) or (M, N), or the batched versions
|
|
585
|
+
(..., M, 1) or (..., M, N). Here M and N are the dimensions of the result of the matrix multiplication. If N = 1, the
|
|
586
|
+
columns of `c` are broadcast for the addition; the rows of `c` are never broadcast. If batch dimensions are not
|
|
587
|
+
present, `c` is broadcast across batches as needed.
|
|
588
|
+
* Similarly, when operating on a batch, auxiliary outputs are 3-D for all epilogs. Therefore, epilogs that return 1-D
|
|
589
|
+
vectors of length N in non-batched mode return 3-D matrices of size (batch, N, 1) in batched mode.
|
|
590
|
+
""".strip(),
|
|
591
|
+
}
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
class InvalidMatmulState(Exception):
|
|
596
|
+
pass
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _check_extents(shape: tuple, name: str):
|
|
600
|
+
if any(e <= 0 for e in shape):
|
|
601
|
+
message = f"The specified extents {shape} for operand {name} are not valid. The extents must be strictly positive. "
|
|
602
|
+
raise ValueError(message)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
@utils.docstring_decorator(SHARED_MM_DOCUMENTATION, skip_missing=False)
|
|
606
|
+
class Matmul:
|
|
607
|
+
"""
|
|
608
|
+
Create a stateful object encapsulating the specified matrix multiplication computation
|
|
609
|
+
:math:`\\alpha a @ b + \\beta c` and the required resources to perform the operation. A
|
|
610
|
+
stateful object can be used to amortize the cost of preparation (planning in the case of
|
|
611
|
+
matrix multiplication) across multiple executions (also see the :ref:`Stateful APIs
|
|
612
|
+
<host api types>` section).
|
|
613
|
+
|
|
614
|
+
The function-form API :func:`matmul` is a convenient alternative to using stateful
|
|
615
|
+
objects for *single* use (the user needs to perform just one matrix multiplication, for
|
|
616
|
+
example), in which case there is no possibility of amortizing preparatory costs. The
|
|
617
|
+
function-form APIs are just convenience wrappers around the stateful object APIs.
|
|
618
|
+
|
|
619
|
+
Using the stateful object typically involves the following steps:
|
|
620
|
+
|
|
621
|
+
1. **Problem Specification**: Initialize the object with a defined operation and
|
|
622
|
+
options.
|
|
623
|
+
2. **Preparation**: Use :meth:`plan` to determine the best algorithmic implementation
|
|
624
|
+
for this specific matrix multiplication operation.
|
|
625
|
+
3. **Execution**: Perform the matrix multiplication computation with :meth:`execute`.
|
|
626
|
+
4. **Resource Management**: Ensure all resources are released either by explicitly
|
|
627
|
+
calling :meth:`free` or by managing the stateful object within a context manager.
|
|
628
|
+
|
|
629
|
+
Detailed information on what's happening in the various phases described above can be
|
|
630
|
+
obtained by passing in a :class:`logging.Logger` object to :class:`MatmulOptions` or by
|
|
631
|
+
setting the appropriate options in the root logger object, which is used by default:
|
|
632
|
+
|
|
633
|
+
>>> import logging
|
|
634
|
+
>>> logging.basicConfig(
|
|
635
|
+
... level=logging.INFO,
|
|
636
|
+
... format="%(asctime)s %(levelname)-8s %(message)s",
|
|
637
|
+
... datefmt="%m-%d %H:%M:%S",
|
|
638
|
+
... )
|
|
639
|
+
|
|
640
|
+
A user can select the desired logging level and, in general, take advantage of all of
|
|
641
|
+
the functionality offered by the Python `logging` module.
|
|
642
|
+
|
|
643
|
+
Args:
|
|
644
|
+
a: {a}
|
|
645
|
+
|
|
646
|
+
b: {b}
|
|
647
|
+
|
|
648
|
+
c: {c}
|
|
649
|
+
{c_admonitions}
|
|
650
|
+
|
|
651
|
+
alpha: {alpha}
|
|
652
|
+
|
|
653
|
+
beta: {beta}
|
|
654
|
+
|
|
655
|
+
qualifiers: {qualifiers}
|
|
656
|
+
|
|
657
|
+
options: {options}
|
|
658
|
+
|
|
659
|
+
stream: {stream}
|
|
660
|
+
|
|
661
|
+
quantization_scales: {quantization_scales}
|
|
662
|
+
|
|
663
|
+
Semantics:
|
|
664
|
+
{semantics}
|
|
665
|
+
|
|
666
|
+
Narrow-precision support:
|
|
667
|
+
{narrow_precision}
|
|
668
|
+
|
|
669
|
+
See Also:
|
|
670
|
+
:meth:`autotune`, :meth:`plan`, :meth:`reset_operands`, :meth:`execute`
|
|
671
|
+
|
|
672
|
+
Examples:
|
|
673
|
+
|
|
674
|
+
>>> import numpy as np
|
|
675
|
+
>>> import nvmath
|
|
676
|
+
|
|
677
|
+
Create two 2-D float64 ndarrays on the CPU:
|
|
678
|
+
|
|
679
|
+
>>> M, N, K = 1024, 1024, 1024
|
|
680
|
+
>>> a = np.random.rand(M, K)
|
|
681
|
+
>>> b = np.random.rand(K, N)
|
|
682
|
+
|
|
683
|
+
We will define a matrix multiplication operation followed by a RELU epilog function
|
|
684
|
+
using the specialized matrix multiplication interface.
|
|
685
|
+
|
|
686
|
+
Create a Matmul object encapsulating the problem specification above:
|
|
687
|
+
|
|
688
|
+
>>> mm = nvmath.linalg.advanced.Matmul(a, b)
|
|
689
|
+
|
|
690
|
+
Options can be provided above to control the behavior of the operation using the
|
|
691
|
+
`options` argument (see :class:`MatmulOptions`).
|
|
692
|
+
|
|
693
|
+
Next, plan the operation. The epilog is specified, and optionally, preferences can
|
|
694
|
+
be specified for planning:
|
|
695
|
+
|
|
696
|
+
>>> epilog = nvmath.linalg.advanced.MatmulEpilog.RELU
|
|
697
|
+
>>> algorithms = mm.plan(epilog=epilog)
|
|
698
|
+
|
|
699
|
+
Certain epilog choices (like :attr:`nvmath.linalg.advanced.MatmulEpilog.BIAS`)
|
|
700
|
+
require additional input provided using the `epilog_inputs` argument to
|
|
701
|
+
:meth:`plan`.
|
|
702
|
+
|
|
703
|
+
Now execute the matrix multiplication, and obtain the result `r1` as a NumPy
|
|
704
|
+
ndarray.
|
|
705
|
+
|
|
706
|
+
>>> r1 = mm.execute()
|
|
707
|
+
|
|
708
|
+
Finally, free the object's resources. To avoid having to explicitly making this
|
|
709
|
+
call, it's recommended to use the Matmul object as a context manager as shown below,
|
|
710
|
+
if possible.
|
|
711
|
+
|
|
712
|
+
>>> mm.free()
|
|
713
|
+
|
|
714
|
+
Note that all :class:`Matmul` methods execute on the current stream by default.
|
|
715
|
+
Alternatively, the `stream` argument can be used to run a method on a specified
|
|
716
|
+
stream.
|
|
717
|
+
|
|
718
|
+
Let's now look at the same problem with CuPy ndarrays on the GPU.
|
|
719
|
+
|
|
720
|
+
Create a 3-D complex128 CuPy ndarray on the GPU:
|
|
721
|
+
|
|
722
|
+
>>> import cupy as cp
|
|
723
|
+
>>> a = cp.random.rand(M, K)
|
|
724
|
+
>>> b = cp.random.rand(K, N)
|
|
725
|
+
|
|
726
|
+
Create an Matmul object encapsulating the problem specification described earlier
|
|
727
|
+
and use it as a context manager.
|
|
728
|
+
|
|
729
|
+
>>> with nvmath.linalg.advanced.Matmul(a, b) as mm:
|
|
730
|
+
... algorithms = mm.plan(epilog=epilog)
|
|
731
|
+
...
|
|
732
|
+
... # Execute the operation to get the first result.
|
|
733
|
+
... r1 = mm.execute()
|
|
734
|
+
...
|
|
735
|
+
... # Update operands A and B in-place (see reset_operands() for an
|
|
736
|
+
... # alternative).
|
|
737
|
+
... a[:] = cp.random.rand(M, K)
|
|
738
|
+
... b[:] = cp.random.rand(K, N)
|
|
739
|
+
...
|
|
740
|
+
... # Execute the operation to get the new result.
|
|
741
|
+
... r2 = mm.execute()
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
All the resources used by the object are released at the end of the block.
|
|
745
|
+
|
|
746
|
+
Further examples can be found in the `nvmath/examples/linalg/advanced/matmul
|
|
747
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/linalg/advanced/matmul>`_
|
|
748
|
+
directory.
|
|
749
|
+
"""
|
|
750
|
+
|
|
751
|
+
def __init__(
|
|
752
|
+
self,
|
|
753
|
+
a,
|
|
754
|
+
b,
|
|
755
|
+
/,
|
|
756
|
+
c=None,
|
|
757
|
+
*,
|
|
758
|
+
alpha=None,
|
|
759
|
+
beta=None,
|
|
760
|
+
qualifiers=None,
|
|
761
|
+
quantization_scales=None,
|
|
762
|
+
options=None,
|
|
763
|
+
stream: utils.AnyStream | int | None = None,
|
|
764
|
+
):
|
|
765
|
+
options = utils.check_or_create_options(_configuration.MatmulOptions, options, "Matrix multiplication options")
|
|
766
|
+
assert options is not None
|
|
767
|
+
self.options = options
|
|
768
|
+
|
|
769
|
+
self.logger = options.logger if options.logger is not None else logging.getLogger()
|
|
770
|
+
|
|
771
|
+
def check_dtype(dtype, operand_name):
|
|
772
|
+
if dtype not in SUPPORTED_TYPES:
|
|
773
|
+
raise ValueError(f"The dtype of operand {operand_name} ({dtype}) is not supported.")
|
|
774
|
+
|
|
775
|
+
# The matrix multiplication has two required operands 'a' and 'b', and one optional
|
|
776
|
+
# operand 'c'.
|
|
777
|
+
a = tensor_wrapper.wrap_operand(a)
|
|
778
|
+
b = tensor_wrapper.wrap_operand(b)
|
|
779
|
+
check_dtype(a.dtype, "A")
|
|
780
|
+
check_dtype(b.dtype, "B")
|
|
781
|
+
self.logger.info("= SPECIFICATION PHASE =")
|
|
782
|
+
self.logger.info(f"The data type of operand A is '{a.dtype}', and that of operand B is '{b.dtype}'.")
|
|
783
|
+
|
|
784
|
+
self.num_operands = 2
|
|
785
|
+
if c is not None:
|
|
786
|
+
self.num_operands = 3
|
|
787
|
+
c = tensor_wrapper.wrap_operand(c)
|
|
788
|
+
if len(c.shape) < 2:
|
|
789
|
+
raise ValueError(
|
|
790
|
+
"In order to avoid broadcasting behavior ambiguity, `c` must be at least 2-D. "
|
|
791
|
+
"Use a singleton dimension to convert your input array to 2-D."
|
|
792
|
+
)
|
|
793
|
+
check_dtype(c.dtype, "C")
|
|
794
|
+
self.logger.info(f"The data type of operand C is {c.dtype}.")
|
|
795
|
+
|
|
796
|
+
if c is not None and beta is None:
|
|
797
|
+
raise ValueError("A value for beta must be provided if operand C is provided.")
|
|
798
|
+
|
|
799
|
+
if (a.dtype, b.dtype) not in NAMES_TO_DEFAULT_SCALE_TYPE:
|
|
800
|
+
raise ValueError(f"Unsupported combination of dtypes for operands A {a.dtype} and B {b.dtype}.")
|
|
801
|
+
|
|
802
|
+
# Currently, a.dtype != b.dtype is only supported for FP8 (different FP8 kinds are
|
|
803
|
+
# allowed), so we assume that A and B have equal width.
|
|
804
|
+
self.input_type_width = typemaps.NAME_TO_DATA_WIDTH[a.dtype]
|
|
805
|
+
|
|
806
|
+
assert self.num_operands == 2 or self.num_operands == 3, "Internal Error."
|
|
807
|
+
|
|
808
|
+
_check_extents(a.shape, "a")
|
|
809
|
+
_check_extents(b.shape, "b")
|
|
810
|
+
if c is not None:
|
|
811
|
+
_check_extents(c.shape, "c")
|
|
812
|
+
|
|
813
|
+
# Infer the library package & device ID the operands belong to.
|
|
814
|
+
operands = [a, b]
|
|
815
|
+
if self.num_operands == 3:
|
|
816
|
+
operands.append(c)
|
|
817
|
+
self.operands: None | Sequence[utils.TensorHolder | None] = operands
|
|
818
|
+
|
|
819
|
+
self.package = utils.get_operands_package(operands)
|
|
820
|
+
self.memory_space = "cuda"
|
|
821
|
+
self.device_id = utils.get_operands_device_id(operands)
|
|
822
|
+
if self.device_id == "cpu":
|
|
823
|
+
if self.package == "numpy":
|
|
824
|
+
self.package = "cupy"
|
|
825
|
+
# TODO: remove this call after cupy is dropped
|
|
826
|
+
tensor_wrapper.maybe_register_package("cupy")
|
|
827
|
+
self.memory_space = "cpu"
|
|
828
|
+
self.device_id = options.device_id
|
|
829
|
+
self.logger.info(
|
|
830
|
+
f"The input operands' memory space is {self.memory_space}, and the execution space is on device {self.device_id}."
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
# Allocate device memory (in stream context) if needed.
|
|
834
|
+
stream_holder = utils.get_or_create_stream(self.device_id, stream, self.package)
|
|
835
|
+
self.logger.info(f"The specified stream for the Matmul ctor is {stream_holder.obj}.")
|
|
836
|
+
|
|
837
|
+
# Copy operands to device if needed.
|
|
838
|
+
if self.memory_space == "cpu":
|
|
839
|
+
self.operands = tensor_wrapper.to(self.operands, self.device_id, stream_holder)
|
|
840
|
+
|
|
841
|
+
# Set qualifiers.
|
|
842
|
+
self.qualifiers = qualifiers if qualifiers is not None else np.zeros((3,), dtype=_configuration.matrix_qualifiers_dtype)
|
|
843
|
+
if self.qualifiers.dtype != _configuration.matrix_qualifiers_dtype:
|
|
844
|
+
raise ValueError(
|
|
845
|
+
"The qualifiers must be specified as a NumPy array of length 3 corresponding to the operands A, B, and "
|
|
846
|
+
"C of type 'matrix_qualifiers_dtype'."
|
|
847
|
+
)
|
|
848
|
+
if self.qualifiers[2]["is_conjugate"]:
|
|
849
|
+
raise ValueError("The conjugate flag is currently not supported for operand C.")
|
|
850
|
+
# Set qualifiers based on torch lazy conjugation flag if not provided.
|
|
851
|
+
if self.package == "torch" and qualifiers is None:
|
|
852
|
+
self.qualifiers[0]["is_conjugate"] = self.operands[0].tensor.is_conj()
|
|
853
|
+
self.qualifiers[1]["is_conjugate"] = self.operands[1].tensor.is_conj()
|
|
854
|
+
if len(self.operands) > 2 and self.operands[2].tensor.is_conj():
|
|
855
|
+
raise ValueError("The conjugate flag is currently not supported for operand C.")
|
|
856
|
+
self.lazy_conjugation = True
|
|
857
|
+
else:
|
|
858
|
+
self.lazy_conjugation = False
|
|
859
|
+
|
|
860
|
+
# Set blocking or non-blocking behavior.
|
|
861
|
+
self.blocking = self.options.blocking is True or self.memory_space == "cpu"
|
|
862
|
+
if self.blocking:
|
|
863
|
+
self.call_prologue = "This call is blocking and will return only after the operation is complete."
|
|
864
|
+
else:
|
|
865
|
+
self.call_prologue = (
|
|
866
|
+
"This call is non-blocking and will return immediately after the operation is launched on the device."
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
# The result class is that of the first wrapped device operand.
|
|
870
|
+
self.result_class = self.operands[0].__class__
|
|
871
|
+
|
|
872
|
+
# Set memory allocator.
|
|
873
|
+
self.allocator = (
|
|
874
|
+
options.allocator
|
|
875
|
+
if options.allocator is not None
|
|
876
|
+
else memory._MEMORY_MANAGER[self.package](self.device_id, self.logger)
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
# Set memory limit.
|
|
880
|
+
self.memory_limit = utils.get_memory_limit_from_device_id(self.options.memory_limit, self.device_id)
|
|
881
|
+
self.logger.info(f"The memory limit is {formatters.MemoryStr(self.memory_limit)}.")
|
|
882
|
+
|
|
883
|
+
# Set handle. We don't destroy handles we create.
|
|
884
|
+
if options.handle is not None:
|
|
885
|
+
self.handle = options.handle
|
|
886
|
+
else:
|
|
887
|
+
self.handle = get_handle(self.device_id)
|
|
888
|
+
|
|
889
|
+
# Determine the data types for a and b.
|
|
890
|
+
self.a_dtype = typemaps.NAME_TO_DATA_TYPE[a.dtype]
|
|
891
|
+
self.b_dtype = typemaps.NAME_TO_DATA_TYPE[b.dtype]
|
|
892
|
+
self.a_dtype_name = a.dtype
|
|
893
|
+
self.b_dtype_name = b.dtype
|
|
894
|
+
|
|
895
|
+
self.is_complex = "complex" in self.a_dtype_name or "complex" in self.b_dtype_name
|
|
896
|
+
|
|
897
|
+
# Determine the data types for c and d.
|
|
898
|
+
self.d_dtype = options.result_type
|
|
899
|
+
if self.num_operands == 3:
|
|
900
|
+
self.c_dtype = typemaps.NAME_TO_DATA_TYPE[c.dtype]
|
|
901
|
+
if self.d_dtype is None:
|
|
902
|
+
self.d_dtype = self.c_dtype
|
|
903
|
+
elif self.num_operands == 2:
|
|
904
|
+
if self.d_dtype is None:
|
|
905
|
+
self.d_dtype = self.a_dtype
|
|
906
|
+
if self.d_dtype in (CudaDataType.CUDA_R_8F_E5M2, CudaDataType.CUDA_R_8F_E4M3):
|
|
907
|
+
self.c_dtype = CudaDataType.CUDA_R_16F
|
|
908
|
+
else:
|
|
909
|
+
self.c_dtype = self.d_dtype
|
|
910
|
+
self.c_dtype_name = typemaps.DATA_TYPE_TO_NAME[self.c_dtype]
|
|
911
|
+
self.d_dtype_name = typemaps.DATA_TYPE_TO_NAME[self.d_dtype]
|
|
912
|
+
self.c_dtype_width = typemaps.NAME_TO_DATA_WIDTH[self.c_dtype_name]
|
|
913
|
+
self.d_dtype_width = typemaps.NAME_TO_DATA_WIDTH[self.d_dtype_name]
|
|
914
|
+
|
|
915
|
+
self.logger.info(f"The data type for the result D is '{self.d_dtype_name}'.")
|
|
916
|
+
|
|
917
|
+
def assert_valid_compute_type(compute_type):
|
|
918
|
+
if compute_type not in COMPUTE_TYPE_TO_DEFAULT_SCALE_TYPE["real"]:
|
|
919
|
+
message = f"Unsupported compute type. The compute type '{repr(compute_type)}' is currently not supported."
|
|
920
|
+
raise ValueError(message)
|
|
921
|
+
|
|
922
|
+
# Determine the scale type.
|
|
923
|
+
if options.scale_type is None:
|
|
924
|
+
if options.compute_type is not None:
|
|
925
|
+
assert_valid_compute_type(options.compute_type)
|
|
926
|
+
if self.is_complex:
|
|
927
|
+
scale_type_map = COMPUTE_TYPE_TO_DEFAULT_SCALE_TYPE["complex"]
|
|
928
|
+
else:
|
|
929
|
+
scale_type_map = COMPUTE_TYPE_TO_DEFAULT_SCALE_TYPE["real"]
|
|
930
|
+
self.scale_type = scale_type_map[options.compute_type]
|
|
931
|
+
else:
|
|
932
|
+
self.scale_type = NAMES_TO_DEFAULT_SCALE_TYPE[(self.a_dtype_name, self.b_dtype_name)]
|
|
933
|
+
self.scale_type_name = typemaps.DATA_TYPE_TO_NAME[self.scale_type]
|
|
934
|
+
else:
|
|
935
|
+
self.scale_type = options.scale_type
|
|
936
|
+
if self.scale_type not in SCALE_TYPE_TO_DEFAULT_COMPUTE_TYPE:
|
|
937
|
+
message = f"Unsupported scale type. The data type '{repr(self.scale_type)}' is currently not supported."
|
|
938
|
+
raise ValueError(message)
|
|
939
|
+
self.scale_type_name = typemaps.DATA_TYPE_TO_NAME[self.scale_type]
|
|
940
|
+
self.logger.info(f"The scale type is '{self.scale_type_name}'.")
|
|
941
|
+
|
|
942
|
+
# Determine the compute type.
|
|
943
|
+
if options.compute_type is None:
|
|
944
|
+
if options.scale_type is not None:
|
|
945
|
+
self.compute_type = SCALE_TYPE_TO_DEFAULT_COMPUTE_TYPE[options.scale_type]
|
|
946
|
+
else:
|
|
947
|
+
self.compute_type = NAMES_TO_DEFAULT_COMPUTE_TYPE[(self.a_dtype_name, self.b_dtype_name)]
|
|
948
|
+
else:
|
|
949
|
+
self.compute_type = options.compute_type
|
|
950
|
+
assert_valid_compute_type(self.compute_type)
|
|
951
|
+
self.logger.info(f"The compute type is {self.compute_type.name}.")
|
|
952
|
+
|
|
953
|
+
def is_supported(atype, btype, compute_type, scale_type):
|
|
954
|
+
ct = cublas.ComputeType
|
|
955
|
+
st = CudaDataType
|
|
956
|
+
abtype = atype if atype == btype else (atype, btype)
|
|
957
|
+
if compute_type in (ct.COMPUTE_16F, ct.COMPUTE_16F_PEDANTIC):
|
|
958
|
+
return scale_type == st.CUDA_R_16F and abtype == "float16"
|
|
959
|
+
elif compute_type == ct.COMPUTE_32F_PEDANTIC:
|
|
960
|
+
if scale_type == st.CUDA_R_32F:
|
|
961
|
+
return abtype in ("float32", "bfloat16", "float16", "float8_e4m3fn", "float8_e5m2")
|
|
962
|
+
elif scale_type == st.CUDA_C_32F:
|
|
963
|
+
return abtype == "complex64"
|
|
964
|
+
elif compute_type == ct.COMPUTE_32F:
|
|
965
|
+
if scale_type == st.CUDA_R_32F:
|
|
966
|
+
return abtype in (
|
|
967
|
+
"float32",
|
|
968
|
+
"bfloat16",
|
|
969
|
+
"float16",
|
|
970
|
+
"float8_e4m3fn",
|
|
971
|
+
"float8_e5m2",
|
|
972
|
+
("float8_e4m3fn", "float8_e5m2"),
|
|
973
|
+
("float8_e5m2", "float8_e4m3fn"),
|
|
974
|
+
)
|
|
975
|
+
elif scale_type == st.CUDA_C_32F:
|
|
976
|
+
return abtype == "complex64"
|
|
977
|
+
elif compute_type in (ct.COMPUTE_32F_FAST_16F, ct.COMPUTE_32F_FAST_16BF, ct.COMPUTE_32F_FAST_TF32):
|
|
978
|
+
if scale_type == st.CUDA_R_32F:
|
|
979
|
+
return abtype == "float32"
|
|
980
|
+
if scale_type == st.CUDA_C_32F:
|
|
981
|
+
return abtype == "complex64"
|
|
982
|
+
elif compute_type in (ct.COMPUTE_64F, ct.COMPUTE_64F_PEDANTIC):
|
|
983
|
+
if scale_type == st.CUDA_R_64F:
|
|
984
|
+
return abtype == "float64"
|
|
985
|
+
if scale_type == st.CUDA_C_64F:
|
|
986
|
+
return abtype == "complex128"
|
|
987
|
+
return False
|
|
988
|
+
|
|
989
|
+
if not is_supported(self.a_dtype_name, self.b_dtype_name, self.compute_type, self.scale_type):
|
|
990
|
+
raise ValueError(
|
|
991
|
+
f"Selected scale_type={repr(self.scale_type)} compute_type={repr(self.compute_type)} "
|
|
992
|
+
+ f"are not supported for data types {self.a_dtype_name} (A) and {self.b_dtype_name} (B)."
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
# Set alpha and beta.
|
|
996
|
+
self.alpha = np.zeros((1,), dtype=self.scale_type_name)
|
|
997
|
+
try:
|
|
998
|
+
self.alpha[0] = alpha if alpha is not None else 1
|
|
999
|
+
except (ValueError, TypeError) as e:
|
|
1000
|
+
raise ValueError(f"The value provided for alpha {alpha} is not convertible to dtype '{self.alpha.dtype}'.") from e
|
|
1001
|
+
|
|
1002
|
+
self.beta = np.zeros((1,), dtype=self.scale_type_name)
|
|
1003
|
+
if beta is not None and self.num_operands == 2:
|
|
1004
|
+
self.logger.warning(f"Matmul: The provided beta value {beta} is ignored since operand C is not specified.")
|
|
1005
|
+
try:
|
|
1006
|
+
self.beta[0] = beta if beta is not None and self.num_operands == 3 else 0
|
|
1007
|
+
except (ValueError, TypeError) as e:
|
|
1008
|
+
raise ValueError(f"The value provided for beta {beta} is not convertible to dtype '{self.beta.dtype}'.") from e
|
|
1009
|
+
|
|
1010
|
+
# Set narrow-precision (FP8 and lower) quantization_scales.
|
|
1011
|
+
if self.input_type_width <= 8:
|
|
1012
|
+
self.quantization_scales = self._validate_operand_scales(quantization_scales, all_required=True)
|
|
1013
|
+
elif quantization_scales is not None:
|
|
1014
|
+
self.logger.warning(
|
|
1015
|
+
"Matmul: The provided scales are ignored, since they are only applicable to narrow-precision (FP8 and lower) "
|
|
1016
|
+
"operations."
|
|
1017
|
+
)
|
|
1018
|
+
|
|
1019
|
+
if self.options.result_amax and self.d_dtype_width > 8:
|
|
1020
|
+
raise ValueError("result_amax=True is allowed only for narrow-precision (FP8 and lower) results")
|
|
1021
|
+
|
|
1022
|
+
# Check operands alignment if needed
|
|
1023
|
+
if self.input_type_width <= 8:
|
|
1024
|
+
for operand, operand_name in zip(self.operands, "ABC", strict=False):
|
|
1025
|
+
if operand.data_ptr % 16 != 0:
|
|
1026
|
+
raise ValueError(
|
|
1027
|
+
f"For narrow-precision (FP8 and lower) multiplication, operand {operand_name} should be aligned to 16 "
|
|
1028
|
+
"bytes."
|
|
1029
|
+
)
|
|
1030
|
+
|
|
1031
|
+
# Capture operand extents and strides for consistency check when resetting operands.
|
|
1032
|
+
self.operand_extents = tuple(o.shape for o in self.operands)
|
|
1033
|
+
self.operand_strides = tuple(o.strides for o in self.operands)
|
|
1034
|
+
|
|
1035
|
+
# Create operand layouts.
|
|
1036
|
+
a_layout = MatrixLayout(self.operands[0].shape, self.operands[0].strides, self.qualifiers[0]["is_conjugate"])
|
|
1037
|
+
b_layout = MatrixLayout(self.operands[1].shape, self.operands[1].strides, self.qualifiers[1]["is_conjugate"])
|
|
1038
|
+
c_layout = MatrixLayout(self.operands[2].shape, self.operands[2].strides) if self.num_operands == 3 else None
|
|
1039
|
+
|
|
1040
|
+
# Enforce equal batch shape for A and B if block_scaling=True.
|
|
1041
|
+
if self.options.block_scaling and a_layout.shape[:-2] != b_layout.shape[:-2]:
|
|
1042
|
+
raise ValueError(
|
|
1043
|
+
"When block_scaling=True, the batch dimensions of A and B must match (broadcasting is not supported)."
|
|
1044
|
+
)
|
|
1045
|
+
|
|
1046
|
+
# Get the operation traits.
|
|
1047
|
+
self.mm_traits = get_mm_traits(a_layout, b_layout, c_layout, self.logger)
|
|
1048
|
+
self.result_traits = None # Wait till planning to determine this based on the epilog.
|
|
1049
|
+
self.logger.info(
|
|
1050
|
+
f"The matrix multiplication attributes are M = {self.mm_traits.M}, N = {self.mm_traits.N}, and "
|
|
1051
|
+
f"K = {self.mm_traits.K}."
|
|
1052
|
+
)
|
|
1053
|
+
self.logger.info(
|
|
1054
|
+
f"The batch count is {self.mm_traits.batch_count}, and the batch shape is {self.mm_traits.batch_shape} "
|
|
1055
|
+
f"with batch axis order {self.mm_traits.batch_axis_order}."
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1058
|
+
# Create and set the operation descriptor.
|
|
1059
|
+
self.mm_desc = cublaslt.matmul_desc_create(self.compute_type, self.scale_type)
|
|
1060
|
+
|
|
1061
|
+
self.mm_desc_ifc = matmul_desc_ifc.MatmulDescInterface(self.mm_desc)
|
|
1062
|
+
self.mm_desc_ifc.compute_type = self.compute_type
|
|
1063
|
+
self.mm_desc_ifc.scale_type = self.scale_type
|
|
1064
|
+
|
|
1065
|
+
# Guard SM count target and fast accumulation flag.
|
|
1066
|
+
version = cublaslt.get_version()
|
|
1067
|
+
if options.sm_count_target > 0:
|
|
1068
|
+
if version < 111103:
|
|
1069
|
+
raise ValueError(f"The 'sm_count_target' option is not supported in cuBLASLt version {version}.")
|
|
1070
|
+
self.mm_desc_ifc.sm_count_target = options.sm_count_target
|
|
1071
|
+
self.logger.info(f"The SM count target is {options.sm_count_target}.")
|
|
1072
|
+
|
|
1073
|
+
if options.fast_accumulation:
|
|
1074
|
+
if version < 111103:
|
|
1075
|
+
raise ValueError(f"The 'fast_accumulation' option is not supported in cuBLASLt version {version}.")
|
|
1076
|
+
self.mm_desc_ifc.fast_accum = options.fast_accumulation
|
|
1077
|
+
self.logger.info(f"The flag for fast accumulation mode is {options.fast_accumulation}.")
|
|
1078
|
+
|
|
1079
|
+
if self.input_type_width == 8 and version < 120800:
|
|
1080
|
+
raise ValueError(
|
|
1081
|
+
f"FP8 is not supported for cuBLASLt version {version}. cuBLASLt version 12.8 or higher is required."
|
|
1082
|
+
)
|
|
1083
|
+
|
|
1084
|
+
# Planning preferences
|
|
1085
|
+
self.preferences = None
|
|
1086
|
+
|
|
1087
|
+
# Epilog attributes.
|
|
1088
|
+
self.epilog = None
|
|
1089
|
+
|
|
1090
|
+
# Epilog attributes: name-to-operand.
|
|
1091
|
+
self.epilog_operands: dict[str, typing.Any] = {}
|
|
1092
|
+
|
|
1093
|
+
# Epilog attributes: epilog input name-to-handler.
|
|
1094
|
+
self.epilog_input_name_to_handler: dict[str, typing.Any] = {}
|
|
1095
|
+
|
|
1096
|
+
# Epilog attributes: name-to-output tensor.
|
|
1097
|
+
self.epilog_outputs: dict[str, typing.Any] = {}
|
|
1098
|
+
|
|
1099
|
+
# Keep track of epilog input traits for resetting operands.
|
|
1100
|
+
self.epilog_inputs_traits: dict[str, typing.Any] = {}
|
|
1101
|
+
|
|
1102
|
+
# Keep track of epilog output handlers to allocate output in execute().
|
|
1103
|
+
self.epilog_output_handlers: list[EpilogOutputHandler] = []
|
|
1104
|
+
|
|
1105
|
+
# Non-epilog aux outputs. Currently, only used for quantization outputs (amax etc.)
|
|
1106
|
+
self.aux_outputs = None
|
|
1107
|
+
|
|
1108
|
+
# Plan attributes.
|
|
1109
|
+
self.preference_ptr = None
|
|
1110
|
+
self.a_layout_ptr, self.b_layout_ptr, self.c_layout_ptr, self.d_layout_ptr = None, None, None, None
|
|
1111
|
+
self.flop_count = 0
|
|
1112
|
+
self.mm_planned = False
|
|
1113
|
+
|
|
1114
|
+
# Algorithm attributes.
|
|
1115
|
+
self.algorithms_buffer = None
|
|
1116
|
+
self.algorithm_objects = None
|
|
1117
|
+
self.cached_best_algorithm_struct = None
|
|
1118
|
+
|
|
1119
|
+
# Workspace attributes.
|
|
1120
|
+
self.workspace_ptr: None | memory.MemoryPointer = None
|
|
1121
|
+
self.workspace_size = 0
|
|
1122
|
+
self.workspace_allocated_size = 0
|
|
1123
|
+
self.workspace_allocated_here = False
|
|
1124
|
+
|
|
1125
|
+
# Attributes to establish stream ordering.
|
|
1126
|
+
self.workspace_stream = None
|
|
1127
|
+
self.last_compute_event = None
|
|
1128
|
+
|
|
1129
|
+
# Device-side array with the quantization_scales
|
|
1130
|
+
self.quantization_scales_device: dict[str, utils.TensorHolder] = {}
|
|
1131
|
+
|
|
1132
|
+
self.valid_state = True
|
|
1133
|
+
self.logger.info("The Matmul operation has been created.")
|
|
1134
|
+
|
|
1135
|
+
def __enter__(self):
|
|
1136
|
+
return self
|
|
1137
|
+
|
|
1138
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
1139
|
+
self.free()
|
|
1140
|
+
|
|
1141
|
+
def _check_valid_matmul(self, *args, **kwargs):
|
|
1142
|
+
"""
|
|
1143
|
+
Check if the Matmul object is alive and well.
|
|
1144
|
+
"""
|
|
1145
|
+
if not self.valid_state:
|
|
1146
|
+
raise InvalidMatmulState("The Matmul object cannot be used after resources are free'd")
|
|
1147
|
+
|
|
1148
|
+
def _check_valid_operands(self, *args, **kwargs):
|
|
1149
|
+
"""
|
|
1150
|
+
Check if the operands are available for the operation.
|
|
1151
|
+
"""
|
|
1152
|
+
what = kwargs["what"]
|
|
1153
|
+
if self.operands is None:
|
|
1154
|
+
raise RuntimeError(
|
|
1155
|
+
f"{what} cannot be performed if the operands have been set to None. Use reset_operands() to set the "
|
|
1156
|
+
f"desired input before using performing the {what.lower()}."
|
|
1157
|
+
)
|
|
1158
|
+
|
|
1159
|
+
def _free_plan_resources(self, exception: Exception | None = None) -> bool:
|
|
1160
|
+
"""
|
|
1161
|
+
Free resources allocated in planning.
|
|
1162
|
+
"""
|
|
1163
|
+
|
|
1164
|
+
# Destroy matrix layouts.
|
|
1165
|
+
if self.a_layout_ptr is not None:
|
|
1166
|
+
cublaslt.matrix_layout_destroy(self.a_layout_ptr)
|
|
1167
|
+
self.a_layout_ptr = None
|
|
1168
|
+
if self.b_layout_ptr is not None:
|
|
1169
|
+
cublaslt.matrix_layout_destroy(self.b_layout_ptr)
|
|
1170
|
+
self.b_layout_ptr = None
|
|
1171
|
+
if self.c_layout_ptr != self.d_layout_ptr and self.c_layout_ptr is not None:
|
|
1172
|
+
cublaslt.matrix_layout_destroy(self.c_layout_ptr)
|
|
1173
|
+
self.c_layout_ptr = None
|
|
1174
|
+
if self.d_layout_ptr is not None:
|
|
1175
|
+
cublaslt.matrix_layout_destroy(self.d_layout_ptr)
|
|
1176
|
+
self.d_layout_ptr = None
|
|
1177
|
+
|
|
1178
|
+
if self.preference_ptr is not None:
|
|
1179
|
+
cublaslt.matmul_preference_destroy(self.preference_ptr)
|
|
1180
|
+
self.preference_ptr = None
|
|
1181
|
+
|
|
1182
|
+
self.mm_planned = False
|
|
1183
|
+
return True
|
|
1184
|
+
|
|
1185
|
+
def _check_planned(self, *args, **kwargs):
|
|
1186
|
+
what = kwargs["what"]
|
|
1187
|
+
if not self.mm_planned:
|
|
1188
|
+
raise RuntimeError(f"{what} cannot be performed before plan() has been called.")
|
|
1189
|
+
|
|
1190
|
+
def _free_workspace_memory(self, exception: Exception | None = None) -> bool:
|
|
1191
|
+
"""
|
|
1192
|
+
Free workspace by releasing the MemoryPointer object.
|
|
1193
|
+
"""
|
|
1194
|
+
if self.workspace_ptr is None:
|
|
1195
|
+
return True
|
|
1196
|
+
|
|
1197
|
+
self.workspace_ptr = None
|
|
1198
|
+
self.workspace_allocated_size = 0
|
|
1199
|
+
self.logger.debug("[_free_workspace_memory] The workspace has been released.")
|
|
1200
|
+
|
|
1201
|
+
return True
|
|
1202
|
+
|
|
1203
|
+
def _reset_workspace_allocation_tracking(self):
|
|
1204
|
+
"""
|
|
1205
|
+
Reset workspace allocation tracking attributes to False at the end of the methods
|
|
1206
|
+
where workspace memory is potentially allocated. This is necessary to prevent any
|
|
1207
|
+
exceptions raised before method entry from using stale tracking values.
|
|
1208
|
+
"""
|
|
1209
|
+
self.workspace_allocated_here = False
|
|
1210
|
+
|
|
1211
|
+
@utils.precondition(_check_valid_matmul)
|
|
1212
|
+
def _release_workspace_memory_perhaps(self, release_workspace):
|
|
1213
|
+
"""
|
|
1214
|
+
Free workspace memory if it's larger than the specified limit.
|
|
1215
|
+
"""
|
|
1216
|
+
if not release_workspace:
|
|
1217
|
+
return True
|
|
1218
|
+
|
|
1219
|
+
# Establish ordering wrt the computation and free workspace if requested.
|
|
1220
|
+
if self.last_compute_event is not None:
|
|
1221
|
+
self.workspace_stream.wait(self.last_compute_event)
|
|
1222
|
+
self.logger.debug("Established ordering with respect to the computation before releasing the workspace.")
|
|
1223
|
+
self.last_compute_event = None
|
|
1224
|
+
|
|
1225
|
+
self.logger.debug("[_release_workspace_memory_perhaps] The workspace memory will be released.")
|
|
1226
|
+
return self._free_workspace_memory()
|
|
1227
|
+
|
|
1228
|
+
def _release_workspace_memory_perhaps_wrapper(self, exception: Exception | None = None) -> bool:
|
|
1229
|
+
"""
|
|
1230
|
+
This is used in @atomic.
|
|
1231
|
+
"""
|
|
1232
|
+
self._release_workspace_memory_perhaps(release_workspace=self.workspace_allocated_here)
|
|
1233
|
+
self._reset_workspace_allocation_tracking()
|
|
1234
|
+
return True
|
|
1235
|
+
|
|
1236
|
+
@utils.precondition(_check_valid_matmul)
|
|
1237
|
+
@utils.precondition(_check_planned, "Workspace memory allocation")
|
|
1238
|
+
@utils.atomic(_free_workspace_memory, method=True)
|
|
1239
|
+
def _allocate_workspace_memory(self, stream_holder: utils.StreamHolder):
|
|
1240
|
+
"""
|
|
1241
|
+
Allocate workspace memory using the specified allocator.
|
|
1242
|
+
"""
|
|
1243
|
+
|
|
1244
|
+
assert self.workspace_size is not None, "Internal Error."
|
|
1245
|
+
assert self.workspace_allocated_here is False, "Internal Error."
|
|
1246
|
+
|
|
1247
|
+
if self.workspace_size == 0: # For performance, bypass allocator for workspace size == 0.
|
|
1248
|
+
self.workspace_ptr = memory.MemoryPointer(0, 0, finalizer=None)
|
|
1249
|
+
else:
|
|
1250
|
+
self.logger.debug("Allocating workspace for performing the matrix multiplication...")
|
|
1251
|
+
with utils.device_ctx(self.device_id), stream_holder.ctx:
|
|
1252
|
+
try:
|
|
1253
|
+
if isinstance(self.allocator, memory.BaseCUDAMemoryManagerAsync):
|
|
1254
|
+
self.workspace_ptr = self.allocator.memalloc_async(self.workspace_size, stream_holder.obj)
|
|
1255
|
+
else:
|
|
1256
|
+
self.workspace_ptr = self.allocator.memalloc(self.workspace_size)
|
|
1257
|
+
self.workspace_allocated_here = True
|
|
1258
|
+
except TypeError as e:
|
|
1259
|
+
message = (
|
|
1260
|
+
"The method 'memalloc' in the allocator object must conform to the interface in the "
|
|
1261
|
+
"'BaseCUDAMemoryManager' protocol."
|
|
1262
|
+
)
|
|
1263
|
+
raise TypeError(message) from e
|
|
1264
|
+
|
|
1265
|
+
self.workspace_allocated_size = self.workspace_size
|
|
1266
|
+
self.workspace_stream = stream_holder.obj
|
|
1267
|
+
self.logger.debug(
|
|
1268
|
+
f"Finished allocating device workspace of size {formatters.MemoryStr(self.workspace_size)} in the context "
|
|
1269
|
+
f"of stream {self.workspace_stream}."
|
|
1270
|
+
)
|
|
1271
|
+
|
|
1272
|
+
def _allocate_workspace_memory_perhaps(self, stream_holder: utils.StreamHolder):
|
|
1273
|
+
"""
|
|
1274
|
+
Allocate workspace memory using the specified allocator, if it hasn't already been
|
|
1275
|
+
done.
|
|
1276
|
+
"""
|
|
1277
|
+
|
|
1278
|
+
if self.workspace_ptr is not None and self.workspace_allocated_size >= self.workspace_size:
|
|
1279
|
+
return
|
|
1280
|
+
|
|
1281
|
+
return self._allocate_workspace_memory(stream_holder)
|
|
1282
|
+
|
|
1283
|
+
@utils.precondition(_check_valid_matmul)
|
|
1284
|
+
def applicable_algorithm_ids(self, limit=8):
|
|
1285
|
+
"""Obtain the algorithm IDs that are applicable to this matrix multiplication.
|
|
1286
|
+
|
|
1287
|
+
Args:
|
|
1288
|
+
limit: The maximum number of applicable algorithm IDs that is desired
|
|
1289
|
+
|
|
1290
|
+
Returns:
|
|
1291
|
+
A sequence of algorithm IDs that are applicable to this matrix multiplication
|
|
1292
|
+
problem specification, in random order.
|
|
1293
|
+
"""
|
|
1294
|
+
...
|
|
1295
|
+
algo_ids = cublaslt.matmul_algo_get_ids(
|
|
1296
|
+
self.handle,
|
|
1297
|
+
self.compute_type,
|
|
1298
|
+
self.scale_type,
|
|
1299
|
+
self.a_dtype,
|
|
1300
|
+
self.b_dtype,
|
|
1301
|
+
self.c_dtype,
|
|
1302
|
+
self.d_dtype,
|
|
1303
|
+
limit,
|
|
1304
|
+
)
|
|
1305
|
+
return algo_ids
|
|
1306
|
+
|
|
1307
|
+
def _validate_operand_scales(self, quantization_scales, all_required):
|
|
1308
|
+
"""
|
|
1309
|
+
Validates the user-provided quantization scales and wraps them converts them to
|
|
1310
|
+
MatmulQuantizationScales if needed.
|
|
1311
|
+
"""
|
|
1312
|
+
if quantization_scales is None:
|
|
1313
|
+
raise ValueError(
|
|
1314
|
+
"Scales are required for narrow-precision (FP8 and lower) operations. Please set `quantization_scales` "
|
|
1315
|
+
"argument."
|
|
1316
|
+
)
|
|
1317
|
+
quantization_scales = utils.check_or_create_options(
|
|
1318
|
+
_configuration.MatmulQuantizationScales, quantization_scales, "Scale factors"
|
|
1319
|
+
)
|
|
1320
|
+
expected_scales = "AB"
|
|
1321
|
+
if self.d_dtype_width <= 8 and not self.options.block_scaling:
|
|
1322
|
+
expected_scales += "D"
|
|
1323
|
+
elif quantization_scales.d is not None:
|
|
1324
|
+
if self.options.block_scaling:
|
|
1325
|
+
raise ValueError("Quantization scaling is not supported for D when `block_scaling` option is enabled.")
|
|
1326
|
+
if self.d_dtype_width > 8:
|
|
1327
|
+
raise ValueError(
|
|
1328
|
+
"Quantization scaling is not supported for D when it is not a narrow-precision (FP8 and lower) type."
|
|
1329
|
+
)
|
|
1330
|
+
if self.num_operands == 3 and self.c_dtype_width <= 8:
|
|
1331
|
+
expected_scales += "C"
|
|
1332
|
+
elif quantization_scales.c is not None:
|
|
1333
|
+
raise ValueError(
|
|
1334
|
+
"Quantization scaling is not supported for C when it is not a narrow-precision (FP8 and lower) type."
|
|
1335
|
+
)
|
|
1336
|
+
if all_required:
|
|
1337
|
+
for operand in expected_scales:
|
|
1338
|
+
if getattr(quantization_scales, operand.lower()) is None:
|
|
1339
|
+
raise ValueError(f"Scale for {operand.upper()} is not specified")
|
|
1340
|
+
return quantization_scales
|
|
1341
|
+
|
|
1342
|
+
def _validate_epilog_aux_scale(self, aux_quantization_scale, *, required):
|
|
1343
|
+
is_fp8_aux = (
|
|
1344
|
+
self.preferences.epilog.aux_type is not None
|
|
1345
|
+
and typemaps.NAME_TO_DATA_WIDTH[typemaps.DATA_TYPE_TO_NAME[self.preferences.epilog.aux_type]] <= 8
|
|
1346
|
+
)
|
|
1347
|
+
if aux_quantization_scale is not None and not is_fp8_aux:
|
|
1348
|
+
raise ValueError(
|
|
1349
|
+
"Scales for epilog auxiliary output are not supported when `preferences.epilog.aux_type` is not set to a "
|
|
1350
|
+
"narrow-precision type."
|
|
1351
|
+
)
|
|
1352
|
+
elif aux_quantization_scale is None and is_fp8_aux and required:
|
|
1353
|
+
raise ValueError(
|
|
1354
|
+
'"aux_quantization_scale" epilog input is required when `preferences.epilog.aux_type` is not set to a '
|
|
1355
|
+
"narrow-precision type."
|
|
1356
|
+
)
|
|
1357
|
+
|
|
1358
|
+
def _prepare_quantization_scale(
|
|
1359
|
+
self, scale, operand: str, cublas_operand, stream_holder: utils.StreamHolder, operand_size=None
|
|
1360
|
+
):
|
|
1361
|
+
if scale is None:
|
|
1362
|
+
return
|
|
1363
|
+
elif isinstance(scale, int | float):
|
|
1364
|
+
if self.options.block_scaling:
|
|
1365
|
+
raise ValueError("A scalar tensor-wide scale factor is not allowed when block_scaling=True.")
|
|
1366
|
+
# If it's a scalar, copy to GPU. Float32 is the only type allowed by
|
|
1367
|
+
# cublasLtMatmulScale_t for tensor-wide scaling.
|
|
1368
|
+
self.logger.debug(f"Scale for {operand.upper()} will be copied to device {self.device_id}.")
|
|
1369
|
+
self.quantization_scales_device[operand] = tensor_wrapper.wrap_operand(cp.asarray([scale], dtype="float32"))
|
|
1370
|
+
else:
|
|
1371
|
+
if utils.infer_object_package(scale) != self.package:
|
|
1372
|
+
raise TypeError("The quantization scaling tensors must belong to the same package as the operands.")
|
|
1373
|
+
self.quantization_scales_device[operand] = tensor_wrapper.wrap_operand(scale)
|
|
1374
|
+
device_id = self.quantization_scales_device[operand].device_id
|
|
1375
|
+
if device_id != "cpu" and self.device_id != device_id:
|
|
1376
|
+
raise ValueError(f"The scales must be on the same device ({device_id}) as the operands ({self.device_id}).")
|
|
1377
|
+
if self.quantization_scales_device[operand].device in (None, "cpu"):
|
|
1378
|
+
# If it's on CPU, copy to GPU
|
|
1379
|
+
self.logger.debug(f"Scale for {operand.upper()} will be copied to device {self.device_id}.")
|
|
1380
|
+
self.quantization_scales_device[operand] = self.quantization_scales_device[operand].to(
|
|
1381
|
+
self.device_id, stream_holder
|
|
1382
|
+
)
|
|
1383
|
+
if not self.options.block_scaling:
|
|
1384
|
+
if self.quantization_scales_device[operand].shape not in ((1,), ()):
|
|
1385
|
+
raise ValueError(
|
|
1386
|
+
f"The provided {operand.upper()} scale tensor has to be of shape (1,) or (). "
|
|
1387
|
+
f"Got {self.quantization_scales_device[operand].shape} instead."
|
|
1388
|
+
)
|
|
1389
|
+
if self.quantization_scales_device[operand].dtype != "float32":
|
|
1390
|
+
raise ValueError(
|
|
1391
|
+
f"The provided {operand.upper()} scale tensor has to be float32 type. "
|
|
1392
|
+
f"Got {self.quantization_scales_device[operand].dtype} instead."
|
|
1393
|
+
)
|
|
1394
|
+
elif self.input_type_width == 8:
|
|
1395
|
+
if operand_size is None:
|
|
1396
|
+
raise ValueError(f"Block scaling is not supported for {operand.upper()} scale.")
|
|
1397
|
+
expected_shape = (operand_size // 32,)
|
|
1398
|
+
if self.quantization_scales_device[operand].shape != expected_shape:
|
|
1399
|
+
raise ValueError(
|
|
1400
|
+
f"Scales for {operand.upper()} should have shape {expected_shape}. "
|
|
1401
|
+
f"Got {self.quantization_scales_device[operand].shape}."
|
|
1402
|
+
)
|
|
1403
|
+
if self.quantization_scales_device[operand].dtype != "uint8":
|
|
1404
|
+
raise ValueError(f"Block scales for {operand.upper()} should be uint8 tensor.")
|
|
1405
|
+
else:
|
|
1406
|
+
raise ValueError("block_scaling == True is not supported for non-FP8 types.")
|
|
1407
|
+
setattr(self.mm_desc_ifc, f"{cublas_operand}_scale_pointer", self.quantization_scales_device[operand].data_ptr)
|
|
1408
|
+
if self.options.block_scaling:
|
|
1409
|
+
self.logger.debug(f"Using VEC32_UE8M0 scale mode for operand {operand.upper()}.")
|
|
1410
|
+
setattr(self.mm_desc_ifc, f"{cublas_operand}_scale_mode", cublaslt.MatmulMatrixScale.VEC32_UE8M0)
|
|
1411
|
+
else:
|
|
1412
|
+
self.logger.debug(f"Using SCALAR_32F scale mode for operand {operand.upper()}.")
|
|
1413
|
+
setattr(self.mm_desc_ifc, f"{cublas_operand}_scale_mode", cublaslt.MatmulMatrixScale.SCALAR_32F)
|
|
1414
|
+
|
|
1415
|
+
def _prepare_operand_quantization_scales(self, scales, stream_holder: utils.StreamHolder):
|
|
1416
|
+
"""
|
|
1417
|
+
Copies the scales to the GPU and updates the pointers in mm_desc_ifc.
|
|
1418
|
+
"""
|
|
1419
|
+
for operand in "abcd":
|
|
1420
|
+
scale = getattr(scales, operand)
|
|
1421
|
+
if self.options.block_scaling and operand == "a":
|
|
1422
|
+
operand_size = self.operands[0].size # type: ignore[union-attr,index]
|
|
1423
|
+
elif self.options.block_scaling and operand == "b":
|
|
1424
|
+
operand_size = self.operands[1].size # type: ignore[union-attr,index]
|
|
1425
|
+
else:
|
|
1426
|
+
operand_size = None
|
|
1427
|
+
self._prepare_quantization_scale(
|
|
1428
|
+
scale, operand, cublas_operand=operand, operand_size=operand_size, stream_holder=stream_holder
|
|
1429
|
+
)
|
|
1430
|
+
|
|
1431
|
+
@utils.precondition(_check_valid_matmul)
|
|
1432
|
+
@utils.atomic(_free_plan_resources, method=True)
|
|
1433
|
+
def plan(
|
|
1434
|
+
self, *, preferences=None, algorithms=None, epilog=None, epilog_inputs=None, stream: utils.AnyStream | int | None = None
|
|
1435
|
+
): # Epilog inputs require as many inputs (with specific shapes etc) as required by the epilogue. It's a dict.
|
|
1436
|
+
"""
|
|
1437
|
+
Plan the matrix multiplication operation, considering the epilog (if provided).
|
|
1438
|
+
|
|
1439
|
+
Args:
|
|
1440
|
+
preferences: {preferences}
|
|
1441
|
+
|
|
1442
|
+
algorithms: {algorithms}
|
|
1443
|
+
|
|
1444
|
+
epilog: {epilog}
|
|
1445
|
+
|
|
1446
|
+
epilog_inputs: {epilog_inputs}
|
|
1447
|
+
|
|
1448
|
+
stream: {stream}
|
|
1449
|
+
|
|
1450
|
+
Returns:
|
|
1451
|
+
A sequence of :class:`nvmath.linalg.advanced.Algorithm` objects that are
|
|
1452
|
+
applicable to this matrix multiplication problem specification, heuristically
|
|
1453
|
+
ordered from fastest to slowest.
|
|
1454
|
+
|
|
1455
|
+
Notes:
|
|
1456
|
+
Epilogs that have ``BIAS`` in their name need an epilog input with the key
|
|
1457
|
+
``'bias'``. Epilogs that have ``DRELU`` need an epilog input with the key
|
|
1458
|
+
``'relu_aux'``, which is produced in a "forward pass" epilog like ``RELU_AUX``
|
|
1459
|
+
or ``RELU_AUX_BIAS``. Similarly, epilogs with ``DGELU`` in their name require an
|
|
1460
|
+
epilog input with the key ``'gelu_aux'``, produced in the corresponding forward
|
|
1461
|
+
pass operation.
|
|
1462
|
+
|
|
1463
|
+
Examples:
|
|
1464
|
+
|
|
1465
|
+
>>> import numpy as np
|
|
1466
|
+
>>> import nvmath
|
|
1467
|
+
|
|
1468
|
+
Create two 3-D float64 ndarrays on the CPU representing batched matrices, along
|
|
1469
|
+
with a bias vector:
|
|
1470
|
+
|
|
1471
|
+
>>> batch = 32
|
|
1472
|
+
>>> M, N, K = 1024, 1024, 1024
|
|
1473
|
+
>>> a = np.random.rand(batch, M, K)
|
|
1474
|
+
>>> b = np.random.rand(batch, K, N)
|
|
1475
|
+
>>> # The bias vector will be broadcast along the columns, as well as along the
|
|
1476
|
+
>>> # batch dimension.
|
|
1477
|
+
>>> bias = np.random.rand(M)
|
|
1478
|
+
|
|
1479
|
+
We will define a matrix multiplication operation followed by a
|
|
1480
|
+
:attr:`nvmath.linalg.advanced.MatmulEpilog.RELU_BIAS` epilog function.
|
|
1481
|
+
|
|
1482
|
+
>>> with nvmath.linalg.advanced.Matmul(a, b) as mm:
|
|
1483
|
+
... # Plan the operation with RELU_BIAS epilog and corresponding epilog
|
|
1484
|
+
... # input.
|
|
1485
|
+
... p = nvmath.linalg.advanced.MatmulPlanPreferences(limit=8)
|
|
1486
|
+
... epilog = nvmath.linalg.advanced.MatmulEpilog.RELU_BIAS
|
|
1487
|
+
... epilog_inputs = {{"bias": bias}}
|
|
1488
|
+
... # The preferences can also be provided as a dict: {{'limit': 8}}
|
|
1489
|
+
... algorithms = mm.plan(
|
|
1490
|
+
... preferences=p,
|
|
1491
|
+
... epilog=epilog,
|
|
1492
|
+
... epilog_inputs=epilog_inputs,
|
|
1493
|
+
... )
|
|
1494
|
+
...
|
|
1495
|
+
... # Execute the matrix multiplication, and obtain the result `r` as a
|
|
1496
|
+
... # NumPy ndarray.
|
|
1497
|
+
... r = mm.execute()
|
|
1498
|
+
|
|
1499
|
+
Some epilogs like :attr:`nvmath.linalg.advanced.MatmulEpilog.RELU_AUX` produce
|
|
1500
|
+
auxiliary output.
|
|
1501
|
+
|
|
1502
|
+
>>> with nvmath.linalg.advanced.Matmul(a, b) as mm:
|
|
1503
|
+
... # Plan the operation with RELU_AUX epilog>
|
|
1504
|
+
... epilog = nvmath.linalg.advanced.MatmulEpilog.RELU_AUX
|
|
1505
|
+
... algorithms = mm.plan(epilog=epilog)
|
|
1506
|
+
...
|
|
1507
|
+
... # Execute the matrix multiplication, and obtain the result `r` along
|
|
1508
|
+
... # with the auxiliary output.
|
|
1509
|
+
... r, auxiliary = mm.execute()
|
|
1510
|
+
|
|
1511
|
+
The auxiliary output is a Python `dict` with the names of each auxiliary output
|
|
1512
|
+
as keys.
|
|
1513
|
+
|
|
1514
|
+
Further examples can be found in the `nvmath/examples/linalg/advanced/matmul
|
|
1515
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/linalg/advanced/matmul>`_
|
|
1516
|
+
directory.
|
|
1517
|
+
"""
|
|
1518
|
+
log_info = self.logger.isEnabledFor(logging.INFO)
|
|
1519
|
+
|
|
1520
|
+
self.logger.info("= PLANNING PHASE =")
|
|
1521
|
+
|
|
1522
|
+
# Clear epilog operands, since different epilogs can be provided in different calls.
|
|
1523
|
+
# We don't need to worry about ordering, since it's the user's responsibility to
|
|
1524
|
+
# order calls that accept a stream argument. This applies to CPU operands as well,
|
|
1525
|
+
# even though we move them to the GPU, since the execution is blocking.
|
|
1526
|
+
self.epilog_operands = {} # Clear operands in case of repeated planning.
|
|
1527
|
+
self.epilog_input_name_to_handler = {} # Clear input name to handler map as well,
|
|
1528
|
+
self.epilog_inputs_traits = {} # ... and the input traits as well.
|
|
1529
|
+
|
|
1530
|
+
preferences = utils.check_or_create_options(
|
|
1531
|
+
_configuration.MatmulPlanPreferences, preferences, "Matrix multiplication plan preferences"
|
|
1532
|
+
)
|
|
1533
|
+
self.preferences = preferences
|
|
1534
|
+
|
|
1535
|
+
mm_traits = self.mm_traits
|
|
1536
|
+
|
|
1537
|
+
stream_holder = utils.get_or_create_stream(self.device_id, stream, self.package)
|
|
1538
|
+
self.logger.info(f"The specified stream for the matrix multiplication plan is {stream_holder.obj}.")
|
|
1539
|
+
|
|
1540
|
+
# Base FLOP count.
|
|
1541
|
+
self.flop_count = 2 * mm_traits.M * mm_traits.N * mm_traits.K
|
|
1542
|
+
self.logger.info(f"The base matrix multiplication FLOP count is {formatters.FLOPSStr(self.flop_count, 'FLOP')}.")
|
|
1543
|
+
|
|
1544
|
+
if epilog is None and epilog_inputs is not None:
|
|
1545
|
+
self.logger.warning(
|
|
1546
|
+
f"Matmul: The provided epilog inputs {epilog_inputs.keys()} are ignored since an epilog is not specified."
|
|
1547
|
+
)
|
|
1548
|
+
|
|
1549
|
+
self.epilog = epilog
|
|
1550
|
+
epilog_ordering = None
|
|
1551
|
+
if epilog is not None:
|
|
1552
|
+
assert epilog in EPILOG_INPUT_HANDLERS_MAP, "Not supported."
|
|
1553
|
+
self.logger.info(f"The specified epilog is {epilog.name}.")
|
|
1554
|
+
|
|
1555
|
+
epilog_minimum_versions = EPILOG_MINIMUM_VERSIONS_MAP[epilog]
|
|
1556
|
+
batched_epilog_minimum_versions = BATCHED_EPILOG_MINIMUM_VERSIONS_MAP[epilog]
|
|
1557
|
+
version = cublaslt.get_version()
|
|
1558
|
+
if version < epilog_minimum_versions["cublaslt"]:
|
|
1559
|
+
message = (
|
|
1560
|
+
f"The epilog {epilog.name} requires cublaslt >= {epilog_minimum_versions['cublaslt']}; "
|
|
1561
|
+
f"you have version {version}. Update to CUDA Toolkit >= {epilog_minimum_versions['ctk']}."
|
|
1562
|
+
)
|
|
1563
|
+
raise ValueError(message)
|
|
1564
|
+
|
|
1565
|
+
if len(mm_traits.batch_shape) > 0 and version < batched_epilog_minimum_versions["cublaslt"]:
|
|
1566
|
+
message = (
|
|
1567
|
+
f"The epilog {epilog.name} supports batching in "
|
|
1568
|
+
f"cublaslt >= {batched_epilog_minimum_versions['cublaslt']}; "
|
|
1569
|
+
f"you have version {version}. Update to CUDA Toolkit >= {epilog_minimum_versions['ctk']}."
|
|
1570
|
+
)
|
|
1571
|
+
raise ValueError(message)
|
|
1572
|
+
if (
|
|
1573
|
+
self.mm_traits.c_layout_traits is not None
|
|
1574
|
+
and self.mm_traits.c_layout_traits.order == cublaslt.Order.ROW
|
|
1575
|
+
and epilog
|
|
1576
|
+
in [
|
|
1577
|
+
cublaslt.Epilogue.BGRADA,
|
|
1578
|
+
cublaslt.Epilogue.BGRADB,
|
|
1579
|
+
]
|
|
1580
|
+
):
|
|
1581
|
+
msg = f"The epilog {epilog.name} requires input matrix 'c' to be F-contiguous (column-major)."
|
|
1582
|
+
raise ValueError(msg)
|
|
1583
|
+
if (
|
|
1584
|
+
version < 120804
|
|
1585
|
+
# A has one row
|
|
1586
|
+
and self.mm_traits.M == 1
|
|
1587
|
+
# C is broadcast
|
|
1588
|
+
and self.mm_traits.c_layout_traits is not None
|
|
1589
|
+
and self.mm_traits.c_layout_traits.ld == 0
|
|
1590
|
+
# Using both an bias epilog and C
|
|
1591
|
+
and self.epilog & _configuration.MatmulEpilog.BIAS > 0
|
|
1592
|
+
):
|
|
1593
|
+
message = (
|
|
1594
|
+
"When matrix 'a' has one row, "
|
|
1595
|
+
"simultaneously broadcasting matrix 'c' and using a BIAS epilog requires cublaslt >= 120804; "
|
|
1596
|
+
f"You have version {version}. Update to CUDA Toolkit >= 12.8.1."
|
|
1597
|
+
)
|
|
1598
|
+
raise ValueError(message)
|
|
1599
|
+
|
|
1600
|
+
# Take a copy of the user-provided inputs.
|
|
1601
|
+
if epilog_inputs is not None:
|
|
1602
|
+
epilog_inputs = epilog_inputs.copy()
|
|
1603
|
+
else:
|
|
1604
|
+
epilog_inputs = {}
|
|
1605
|
+
|
|
1606
|
+
# Get the dtype of auxiliary buffer
|
|
1607
|
+
aux_dtype_name = (
|
|
1608
|
+
typemaps.DATA_TYPE_TO_NAME[self.preferences.epilog.aux_type] # type: ignore[attr-defined]
|
|
1609
|
+
if self.preferences.epilog.aux_type is not None # type: ignore[attr-defined]
|
|
1610
|
+
else None
|
|
1611
|
+
)
|
|
1612
|
+
|
|
1613
|
+
# Extract aux quantization scale from the inputs.
|
|
1614
|
+
aux_quantization_scale = (
|
|
1615
|
+
epilog_inputs.pop("aux_quantization_scale") if "aux_quantization_scale" in epilog_inputs else None
|
|
1616
|
+
)
|
|
1617
|
+
self._validate_epilog_aux_scale(aux_quantization_scale, required=True)
|
|
1618
|
+
self._prepare_quantization_scale(
|
|
1619
|
+
aux_quantization_scale, "epilog_aux", cublas_operand="epilogue_aux", stream_holder=stream_holder
|
|
1620
|
+
)
|
|
1621
|
+
|
|
1622
|
+
epilog_input_handler_types = EPILOG_INPUT_HANDLERS_MAP[epilog]
|
|
1623
|
+
if epilog_input_handler_types:
|
|
1624
|
+
epilog_input_handlers = [
|
|
1625
|
+
handler_type(self.logger, mm_traits, epilog, self.c_dtype_name, self.d_dtype_name, aux_dtype_name)
|
|
1626
|
+
for handler_type in epilog_input_handler_types
|
|
1627
|
+
]
|
|
1628
|
+
|
|
1629
|
+
# Check if the epilog requires a specific result layout, and if the
|
|
1630
|
+
# requirement is consistent for all the handlers.
|
|
1631
|
+
epilog_input_handlers_ordering = {h.order for h in epilog_input_handlers}
|
|
1632
|
+
assert len(epilog_input_handlers_ordering) == 1, "Internal error."
|
|
1633
|
+
epilog_ordering = epilog_input_handlers_ordering.pop()
|
|
1634
|
+
|
|
1635
|
+
required_epilog_input_names = {h.name for h in epilog_input_handlers}
|
|
1636
|
+
|
|
1637
|
+
self.logger.info(f"The epilog requires the following additional inputs: {required_epilog_input_names}.")
|
|
1638
|
+
if required_epilog_input_names != set(epilog_inputs.keys()):
|
|
1639
|
+
raise ValueError(
|
|
1640
|
+
f"The epilog {epilog.name} requires the following input tensors: "
|
|
1641
|
+
f"{required_epilog_input_names}. The provided tensor names are: {epilog_inputs.keys()}"
|
|
1642
|
+
)
|
|
1643
|
+
|
|
1644
|
+
# Wrap epilog inputs.
|
|
1645
|
+
for name in epilog_inputs:
|
|
1646
|
+
epilog_inputs[name] = tensor_wrapper.wrap_operand(epilog_inputs[name])
|
|
1647
|
+
|
|
1648
|
+
# Check if epilog inputs all belong to the same package, which is the same
|
|
1649
|
+
# as the package of the MM operands.
|
|
1650
|
+
epilog_package = utils.get_operands_package(list(epilog_inputs.values()))
|
|
1651
|
+
epilog_package = "cupy" if epilog_package == "numpy" else epilog_package # Handle the NumPy <=> CuPy asymmetry.
|
|
1652
|
+
if self.package != epilog_package:
|
|
1653
|
+
message = f"Library package mismatch for epilog: '{self.package}' => '{epilog_package}'"
|
|
1654
|
+
raise TypeError(message)
|
|
1655
|
+
|
|
1656
|
+
# Check if all epilog inputs all are on the same device, which is the device
|
|
1657
|
+
# of the operands.
|
|
1658
|
+
device_id = utils.get_operands_device_id(list(epilog_inputs.values()))
|
|
1659
|
+
if device_id != "cpu" and self.device_id != device_id:
|
|
1660
|
+
raise ValueError(
|
|
1661
|
+
f"The epilog inputs must be on the same device ({device_id}) as the operands ({self.device_id})."
|
|
1662
|
+
)
|
|
1663
|
+
|
|
1664
|
+
# Move epilog inputs to the GPU, if needed.
|
|
1665
|
+
if device_id == "cpu":
|
|
1666
|
+
for e in required_epilog_input_names:
|
|
1667
|
+
self.logger.debug(f"The epilog input {e} will be copied to device{self.device_id}.")
|
|
1668
|
+
self.epilog_operands[e] = epilog_inputs[e].to(self.device_id, stream_holder)
|
|
1669
|
+
else:
|
|
1670
|
+
for e in required_epilog_input_names:
|
|
1671
|
+
self.epilog_operands[e] = epilog_inputs[e]
|
|
1672
|
+
|
|
1673
|
+
# First validate all epilog inputs. Use the GPU tensors in case metadata has
|
|
1674
|
+
# changed.
|
|
1675
|
+
for handler in epilog_input_handlers:
|
|
1676
|
+
handler.validate(epilog_inputs[handler.name])
|
|
1677
|
+
|
|
1678
|
+
# Finally, update the MM descriptor. Note that we pass in
|
|
1679
|
+
# self.epilog_operands (which are on the GPU).
|
|
1680
|
+
for handler in epilog_input_handlers:
|
|
1681
|
+
handler.update(self.mm_desc_ifc, self.epilog_operands[handler.name])
|
|
1682
|
+
self.epilog_input_name_to_handler[handler.name] = handler
|
|
1683
|
+
|
|
1684
|
+
# Capture the epilog operands traits for consistency checks when resetting
|
|
1685
|
+
# operands.
|
|
1686
|
+
self.epilog_inputs_traits = {
|
|
1687
|
+
name: EpilogInputTraits(
|
|
1688
|
+
dtype=self.epilog_operands[name].dtype,
|
|
1689
|
+
extents=self.epilog_operands[name].shape,
|
|
1690
|
+
strides=self.epilog_operands[name].strides,
|
|
1691
|
+
)
|
|
1692
|
+
for name in self.epilog_operands
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
epilog_output_handler_types = EPILOG_OUTPUT_HANDLERS_MAP[epilog]
|
|
1696
|
+
if epilog_output_handler_types:
|
|
1697
|
+
self.epilog_output_handlers = epilog_output_handlers = [
|
|
1698
|
+
handler_type(self.logger, mm_traits, epilog, self.c_dtype_name, self.d_dtype_name, aux_dtype_name)
|
|
1699
|
+
for handler_type in epilog_output_handler_types
|
|
1700
|
+
]
|
|
1701
|
+
# Check if the epilog requires a specific result layout, and if the
|
|
1702
|
+
# requirement is consistent for all the handlers.
|
|
1703
|
+
epilog_output_handlers_ordering = {h.order for h in epilog_output_handlers}
|
|
1704
|
+
assert len(epilog_output_handlers_ordering) == 1, "Internal error."
|
|
1705
|
+
op_epilog_ordering = epilog_output_handlers_ordering.pop()
|
|
1706
|
+
if epilog_ordering is None:
|
|
1707
|
+
epilog_ordering = op_epilog_ordering
|
|
1708
|
+
else:
|
|
1709
|
+
assert epilog_ordering == op_epilog_ordering, "Internal error."
|
|
1710
|
+
|
|
1711
|
+
# Update the MM descriptor, except for the device pointer.
|
|
1712
|
+
for ohandler in epilog_output_handlers:
|
|
1713
|
+
ohandler.update(self.mm_desc_ifc)
|
|
1714
|
+
|
|
1715
|
+
# Set the epilog. At this point, we're sure that the epilog inputs, if any, are
|
|
1716
|
+
# valid and have been set.
|
|
1717
|
+
self.mm_desc_ifc.epilogue = epilog
|
|
1718
|
+
|
|
1719
|
+
# Fill the result traits, now that we know the epilog.
|
|
1720
|
+
self.result_traits = result_traits = get_result_traits(mm_traits, epilog_ordering, self.logger) # type: ignore[assignment]
|
|
1721
|
+
assert self.result_traits is not None, "Internal Error. self.result_traits should have been set by self.plan()."
|
|
1722
|
+
self.logger.info(
|
|
1723
|
+
f"The layout order for the result D is {self.result_traits.d_layout_traits.order.name}, with LD "
|
|
1724
|
+
f"{self.result_traits.d_layout_traits.ld}, and batch offset "
|
|
1725
|
+
f"{self.result_traits.d_layout_traits.batch_offset}."
|
|
1726
|
+
)
|
|
1727
|
+
|
|
1728
|
+
# Internally transpose operand A if required (conjugate flag) and create layout.
|
|
1729
|
+
transpose = False
|
|
1730
|
+
if mm_traits.a_layout_traits.is_conjugate and self.is_complex:
|
|
1731
|
+
self.mm_desc_ifc.transa = cublas.Operation.C
|
|
1732
|
+
transpose = True
|
|
1733
|
+
self.logger.debug(
|
|
1734
|
+
"To conjugate A, the operand A will be internally transposed and the matrix multiplication will be "
|
|
1735
|
+
"performed with OP_C for operand A."
|
|
1736
|
+
)
|
|
1737
|
+
if self.input_type_width <= 8:
|
|
1738
|
+
# narrow-precision (FP8 and lower) data types are only supported for transa=OP_T
|
|
1739
|
+
self.mm_desc_ifc.transa = cublas.Operation.T
|
|
1740
|
+
transpose = True
|
|
1741
|
+
self.logger.debug(
|
|
1742
|
+
"For narrow-precision (FP8 and lower) multiplication, the operand A will be internally transposed and the "
|
|
1743
|
+
"matrix multiplication will be performed with OP_T for operand A."
|
|
1744
|
+
)
|
|
1745
|
+
m, n, ld, a_order = mm_traits.a_layout_traits.get_mm_layout(transpose=transpose)
|
|
1746
|
+
self.a_layout_ptr = cublaslt.matrix_layout_create(self.a_dtype, rows=m, cols=n, ld=ld)
|
|
1747
|
+
self.logger.debug(f"Layout for A: rows = {m}, cols = {n}, ld = {ld}.")
|
|
1748
|
+
|
|
1749
|
+
# Internally transpose operand B if required (conjugate flag, or epilog is BGRADB)
|
|
1750
|
+
# and create layout.
|
|
1751
|
+
transpose = False
|
|
1752
|
+
if mm_traits.b_layout_traits.is_conjugate and self.is_complex:
|
|
1753
|
+
self.mm_desc_ifc.transb = cublas.Operation.C
|
|
1754
|
+
transpose = True
|
|
1755
|
+
self.logger.debug(
|
|
1756
|
+
"To conjugate B, the operand B will be internally transposed and the matrix multiplication will be "
|
|
1757
|
+
"performed with OP_C for operand B."
|
|
1758
|
+
)
|
|
1759
|
+
elif epilog == _configuration.MatmulEpilog.BGRADB:
|
|
1760
|
+
self.mm_desc_ifc.transb = cublas.Operation.T
|
|
1761
|
+
transpose = True
|
|
1762
|
+
self.logger.debug(
|
|
1763
|
+
"For BGRADB epilog, the operand B will be internally transposed and the matrix multiplication will be "
|
|
1764
|
+
"performed with OP_T for operand B."
|
|
1765
|
+
)
|
|
1766
|
+
m, n, ld, b_order = mm_traits.b_layout_traits.get_mm_layout(transpose=transpose)
|
|
1767
|
+
self.b_layout_ptr = cublaslt.matrix_layout_create(self.b_dtype, rows=m, cols=n, ld=ld)
|
|
1768
|
+
self.logger.debug(f"Layout for B: rows = {m}, cols = {n}, ld = {ld}.")
|
|
1769
|
+
|
|
1770
|
+
self.d_layout_ptr = cublaslt.matrix_layout_create(
|
|
1771
|
+
self.d_dtype, rows=mm_traits.M, cols=mm_traits.N, ld=result_traits.d_layout_traits.ld
|
|
1772
|
+
)
|
|
1773
|
+
|
|
1774
|
+
layout_a_ifc = matrix_layout_ifc.MatrixLayoutInterface(self.a_layout_ptr)
|
|
1775
|
+
layout_a_ifc.order = a_order
|
|
1776
|
+
layout_a_ifc.batch_count = mm_traits.batch_count
|
|
1777
|
+
layout_a_ifc.strided_batch_offset = mm_traits.a_layout_traits.batch_offset
|
|
1778
|
+
|
|
1779
|
+
layout_b_ifc = matrix_layout_ifc.MatrixLayoutInterface(self.b_layout_ptr)
|
|
1780
|
+
layout_b_ifc.order = b_order
|
|
1781
|
+
layout_b_ifc.batch_count = mm_traits.batch_count
|
|
1782
|
+
layout_b_ifc.strided_batch_offset = mm_traits.b_layout_traits.batch_offset
|
|
1783
|
+
|
|
1784
|
+
layout_d_ifc = matrix_layout_ifc.MatrixLayoutInterface(self.d_layout_ptr)
|
|
1785
|
+
layout_d_ifc.order = result_traits.d_layout_traits.order
|
|
1786
|
+
layout_d_ifc.batch_count = mm_traits.batch_count
|
|
1787
|
+
layout_d_ifc.strided_batch_offset = result_traits.d_layout_traits.batch_offset
|
|
1788
|
+
|
|
1789
|
+
if self.num_operands == 2:
|
|
1790
|
+
if self.c_dtype == self.d_dtype:
|
|
1791
|
+
# If C and D have equal types, reuse the layout.
|
|
1792
|
+
self.c_layout_ptr = self.d_layout_ptr
|
|
1793
|
+
else:
|
|
1794
|
+
# Otherwise, create a D-like layout, but with different type.
|
|
1795
|
+
self.c_layout_ptr = cublaslt.matrix_layout_create(
|
|
1796
|
+
self.c_dtype, rows=mm_traits.M, cols=mm_traits.N, ld=result_traits.d_layout_traits.ld
|
|
1797
|
+
)
|
|
1798
|
+
layout_c_ifc = matrix_layout_ifc.MatrixLayoutInterface(self.c_layout_ptr)
|
|
1799
|
+
layout_c_ifc.order = result_traits.d_layout_traits.order
|
|
1800
|
+
layout_c_ifc.batch_count = mm_traits.batch_count
|
|
1801
|
+
layout_c_ifc.strided_batch_offset = result_traits.d_layout_traits.batch_offset
|
|
1802
|
+
else:
|
|
1803
|
+
self.c_layout_ptr = cublaslt.matrix_layout_create(
|
|
1804
|
+
self.c_dtype, rows=mm_traits.M, cols=mm_traits.N, ld=mm_traits.c_layout_traits.ld
|
|
1805
|
+
)
|
|
1806
|
+
layout_c_ifc = matrix_layout_ifc.MatrixLayoutInterface(self.c_layout_ptr)
|
|
1807
|
+
layout_c_ifc.order = mm_traits.c_layout_traits.order
|
|
1808
|
+
layout_c_ifc.batch_count = mm_traits.batch_count
|
|
1809
|
+
layout_c_ifc.strided_batch_offset = mm_traits.c_layout_traits.batch_offset
|
|
1810
|
+
|
|
1811
|
+
if (
|
|
1812
|
+
self.input_type_width == 8
|
|
1813
|
+
and self.options.block_scaling
|
|
1814
|
+
and (mm_traits.M % 128 != 0 or mm_traits.N % 128 != 0 or mm_traits.K % 128 != 0)
|
|
1815
|
+
):
|
|
1816
|
+
raise ValueError(
|
|
1817
|
+
f"M={mm_traits.M} N={mm_traits.N} K={mm_traits.K} must be divisible by 128 when block_scaling=True."
|
|
1818
|
+
)
|
|
1819
|
+
|
|
1820
|
+
if self.input_type_width == 8 and (mm_traits.M % 16 != 0 or mm_traits.N % 16 != 0 or mm_traits.K % 16 != 0):
|
|
1821
|
+
raise ValueError(f"M={mm_traits.M} N={mm_traits.N} K={mm_traits.K} must be divisible by 16 for FP8 operations")
|
|
1822
|
+
|
|
1823
|
+
if self.options.block_scaling and self.d_dtype_width == 8:
|
|
1824
|
+
self.mm_desc_ifc.alpha_vector_batch_stride = 1 # Workaround for library caching issue
|
|
1825
|
+
|
|
1826
|
+
# cublasLtMatmulAlgoGetHeuristic requires the scale pointer to be set.
|
|
1827
|
+
self.aux_outputs = {
|
|
1828
|
+
"d_out_scale": utils.create_empty_tensor(
|
|
1829
|
+
self.result_class,
|
|
1830
|
+
((mm_traits.M * mm_traits.N) // 32 * self.mm_traits.batch_count),
|
|
1831
|
+
"uint8",
|
|
1832
|
+
self.device_id,
|
|
1833
|
+
stream_holder,
|
|
1834
|
+
verify_strides=False,
|
|
1835
|
+
)
|
|
1836
|
+
}
|
|
1837
|
+
self.mm_desc_ifc.d_out_scale_pointer = self.aux_outputs["d_out_scale"].data_ptr
|
|
1838
|
+
self.mm_desc_ifc.d_out_scale_mode = cublaslt.MatmulMatrixScale.VEC32_UE8M0
|
|
1839
|
+
|
|
1840
|
+
limit = preferences.limit
|
|
1841
|
+
if algorithms is None:
|
|
1842
|
+
num_algorithms = np.empty((1,), dtype=np.int32)
|
|
1843
|
+
self.algorithms_buffer = cublaslt.MatmulHeuristicResult(limit)
|
|
1844
|
+
else:
|
|
1845
|
+
assert all(isinstance(algo, _algorithmmod.Algorithm) for algo in algorithms), (
|
|
1846
|
+
"The algorithms passed to plan() are of wrong type."
|
|
1847
|
+
)
|
|
1848
|
+
num_algorithms = len(algorithms)
|
|
1849
|
+
|
|
1850
|
+
if self.preference_ptr is None:
|
|
1851
|
+
self.preference_ptr = cublaslt.matmul_preference_create()
|
|
1852
|
+
else:
|
|
1853
|
+
# We need to create a new preferences object to avoid preferences being set in a
|
|
1854
|
+
# cumulative manner if plan() is called multiple times.
|
|
1855
|
+
cublaslt.matmul_preference_destroy(self.preference_ptr)
|
|
1856
|
+
self.preference_ptr = cublaslt.matmul_preference_create()
|
|
1857
|
+
|
|
1858
|
+
if self.input_type_width <= 8:
|
|
1859
|
+
self._prepare_operand_quantization_scales(self.quantization_scales, stream_holder)
|
|
1860
|
+
|
|
1861
|
+
if algorithms is None:
|
|
1862
|
+
# Set preferences.
|
|
1863
|
+
preference_ifc = matmul_pref_ifc.MatmulPreferenceInterface(self.preference_ptr)
|
|
1864
|
+
preference_ifc.max_workspace_bytes = self.memory_limit
|
|
1865
|
+
preference_ifc.reduction_scheme_mask = preferences.reduction_scheme_mask
|
|
1866
|
+
preference_ifc.max_waves_count = preferences.max_waves_count
|
|
1867
|
+
preference_ifc.impl_mask = preferences.numerical_impl_mask
|
|
1868
|
+
|
|
1869
|
+
# Set minimum alignments.
|
|
1870
|
+
a_ptr, b_ptr = self.operands[0].data_ptr, self.operands[1].data_ptr
|
|
1871
|
+
preference_ifc.min_alignment_a_bytes = min(256, pointer_aligned_to(a_ptr))
|
|
1872
|
+
preference_ifc.min_alignment_b_bytes = min(256, pointer_aligned_to(b_ptr))
|
|
1873
|
+
self.logger.debug(f"The minimum alignment for operand A is {preference_ifc.min_alignment_a_bytes} bytes.")
|
|
1874
|
+
self.logger.debug(f"The minimum alignment for operand B is {preference_ifc.min_alignment_b_bytes} bytes.")
|
|
1875
|
+
if self.num_operands == 3:
|
|
1876
|
+
c_ptr = self.operands[2].data_ptr
|
|
1877
|
+
preference_ifc.min_alignment_c_bytes = min(256, pointer_aligned_to(c_ptr))
|
|
1878
|
+
self.logger.debug(f"The minimum alignment for operand C is {preference_ifc.min_alignment_c_bytes} bytes.")
|
|
1879
|
+
# The result alignment should be 256 bytes.
|
|
1880
|
+
self.logger.debug("The minimum alignment for the result D is the default 256 bytes.")
|
|
1881
|
+
|
|
1882
|
+
self.logger.info("Starting matrix multiplication planning...")
|
|
1883
|
+
assert isinstance(self.device_id, int), self.device_id
|
|
1884
|
+
assert stream_holder is not None
|
|
1885
|
+
with utils.cuda_call_ctx(stream_holder, blocking=True, timing=log_info) as (
|
|
1886
|
+
_,
|
|
1887
|
+
elapsed,
|
|
1888
|
+
):
|
|
1889
|
+
cublaslt.matmul_algo_get_heuristic(
|
|
1890
|
+
self.handle,
|
|
1891
|
+
self.mm_desc,
|
|
1892
|
+
self.a_layout_ptr,
|
|
1893
|
+
self.b_layout_ptr,
|
|
1894
|
+
self.c_layout_ptr,
|
|
1895
|
+
self.d_layout_ptr,
|
|
1896
|
+
self.preference_ptr,
|
|
1897
|
+
limit,
|
|
1898
|
+
self.algorithms_buffer.ptr,
|
|
1899
|
+
num_algorithms.ctypes.data,
|
|
1900
|
+
)
|
|
1901
|
+
|
|
1902
|
+
num_algorithms = num_algorithms[0]
|
|
1903
|
+
if num_algorithms == 0:
|
|
1904
|
+
raise RuntimeError("Planning failed to find any suitable algorithm.")
|
|
1905
|
+
assert self.algorithms_buffer is not None, (
|
|
1906
|
+
"Internal Error. self.algorithms_buffer should have been set by self.plan()."
|
|
1907
|
+
)
|
|
1908
|
+
self.algorithms_buffer = self.algorithms_buffer[:num_algorithms]
|
|
1909
|
+
|
|
1910
|
+
# Create algorithm objects.
|
|
1911
|
+
self.algorithm_objects = tuple(_algorithmmod.Algorithm(a) for a in self.algorithms_buffer)
|
|
1912
|
+
else:
|
|
1913
|
+
self.algorithm_objects = tuple(algorithms)
|
|
1914
|
+
self.algorithms_buffer = cublaslt.MatmulHeuristicResult(len(algorithms))
|
|
1915
|
+
for i, algo in enumerate(algorithms):
|
|
1916
|
+
# we wrap it too well that it's hard to copy-construct...
|
|
1917
|
+
self.algorithms_buffer[i] = algo.algorithm._data
|
|
1918
|
+
|
|
1919
|
+
# Cache the first (best) algorithm struct.
|
|
1920
|
+
self.cached_best_algorithm_struct = self.algorithms_buffer[0]["algo"]
|
|
1921
|
+
|
|
1922
|
+
# Create the map from object to buffer.
|
|
1923
|
+
self.algorithm_object_to_buffer = dict(zip(self.algorithm_objects, self.algorithms_buffer, strict=True))
|
|
1924
|
+
|
|
1925
|
+
self.workspace_size = int(np.max(self.algorithms_buffer["workspace_size"]))
|
|
1926
|
+
if self.workspace_size > 0 and self.epilog:
|
|
1927
|
+
self.workspace_size += 16 # Workaround for library issue
|
|
1928
|
+
|
|
1929
|
+
if algorithms is None:
|
|
1930
|
+
self.logger.info(
|
|
1931
|
+
f"The plan found {num_algorithms} suitable algorithms within the requested limit of {limit} "
|
|
1932
|
+
f"algorithms, with a workspace requirement of {formatters.MemoryStr(self.workspace_size)}."
|
|
1933
|
+
)
|
|
1934
|
+
else:
|
|
1935
|
+
self.logger.info(
|
|
1936
|
+
f"The plan is using {num_algorithms} algorithm passed through the algorithms argument, with a "
|
|
1937
|
+
f"workspace requirement of {formatters.MemoryStr(self.workspace_size)}."
|
|
1938
|
+
)
|
|
1939
|
+
|
|
1940
|
+
self.mm_planned = True
|
|
1941
|
+
if algorithms is None and elapsed.data is not None:
|
|
1942
|
+
self.logger.info(f"The matrix multiplication planning phase took {elapsed.data:.3f} ms to complete.")
|
|
1943
|
+
|
|
1944
|
+
return self.algorithm_objects
|
|
1945
|
+
|
|
1946
|
+
@property
|
|
1947
|
+
def algorithms(self):
|
|
1948
|
+
"""
|
|
1949
|
+
After planning using :meth:`plan()`, get the sequence of algorithm objects to
|
|
1950
|
+
inquire their capabilities, configure them, or serialize them for later use.
|
|
1951
|
+
|
|
1952
|
+
Returns:
|
|
1953
|
+
A sequence of :class:`nvmath.linalg.advanced.Algorithm` objects that are
|
|
1954
|
+
applicable to this matrix multiplication problem specification.
|
|
1955
|
+
"""
|
|
1956
|
+
return self.algorithm_objects
|
|
1957
|
+
|
|
1958
|
+
def _check_and_set_operand(
|
|
1959
|
+
self,
|
|
1960
|
+
operand,
|
|
1961
|
+
operand_name,
|
|
1962
|
+
mm_desc_ifc,
|
|
1963
|
+
stream_holder,
|
|
1964
|
+
*,
|
|
1965
|
+
operand_index=None,
|
|
1966
|
+
epilog_name=None,
|
|
1967
|
+
package=None,
|
|
1968
|
+
dtype=None,
|
|
1969
|
+
extents=None,
|
|
1970
|
+
strides=None,
|
|
1971
|
+
):
|
|
1972
|
+
"""
|
|
1973
|
+
Check to make sure that the provided operand is consistent with the one it's
|
|
1974
|
+
updating, and update it.
|
|
1975
|
+
"""
|
|
1976
|
+
assert (operand_index is None) ^ (epilog_name is None), "Internal Error."
|
|
1977
|
+
|
|
1978
|
+
# Make sure that the data type and extents match.
|
|
1979
|
+
utils.check_attribute_match(dtype, operand.dtype, "data type")
|
|
1980
|
+
utils.check_attribute_match(extents, operand.shape, "extents")
|
|
1981
|
+
|
|
1982
|
+
package = utils.infer_object_package(operand.tensor)
|
|
1983
|
+
|
|
1984
|
+
# Conjugate flag of the provided operands must match the original qualifiers
|
|
1985
|
+
if (
|
|
1986
|
+
operand_index is not None
|
|
1987
|
+
and package == "torch"
|
|
1988
|
+
and self.lazy_conjugation
|
|
1989
|
+
and self.qualifiers[operand_index]["is_conjugate"] != operand.tensor.is_conj()
|
|
1990
|
+
):
|
|
1991
|
+
raise ValueError(f"The provided operand {operand_name} has different conjugate flag than the original operand")
|
|
1992
|
+
|
|
1993
|
+
device_id = operand.device_id
|
|
1994
|
+
if device_id == "cpu":
|
|
1995
|
+
package = "cupy" if package == "numpy" else package # Handle the NumPy <=> CuPy asymmetry.
|
|
1996
|
+
if self.package != package:
|
|
1997
|
+
message = f"Library package mismatch: '{self.package}' => '{package}'"
|
|
1998
|
+
raise TypeError(message)
|
|
1999
|
+
|
|
2000
|
+
# Check if we have a GPU buffer to update into.
|
|
2001
|
+
if operand_index is not None:
|
|
2002
|
+
o = self.operands[operand_index]
|
|
2003
|
+
else:
|
|
2004
|
+
o = self.epilog_operands[epilog_name]
|
|
2005
|
+
if o is None: # No buffer, create one.
|
|
2006
|
+
# Copy operand across memory spaces (CPU to GPU).
|
|
2007
|
+
o = operand.to(self.device_id, stream_holder)
|
|
2008
|
+
if operand_index is not None:
|
|
2009
|
+
self.operands[operand_index] = o
|
|
2010
|
+
else:
|
|
2011
|
+
self.epilog_operands[epilog_name] = o
|
|
2012
|
+
# Update the epilog pointer, since we're starting afresh.
|
|
2013
|
+
self.epilog_input_name_to_handler[epilog_name].update(mm_desc_ifc, o)
|
|
2014
|
+
else:
|
|
2015
|
+
# In-place copy to existing device pointer because the new operand is on the
|
|
2016
|
+
# CPU.
|
|
2017
|
+
tensor_wrapper.copy_([operand], [o], stream_holder)
|
|
2018
|
+
else:
|
|
2019
|
+
if self.package != package:
|
|
2020
|
+
message = f"Library package mismatch: '{self.package}' => '{package}'"
|
|
2021
|
+
raise TypeError(message)
|
|
2022
|
+
|
|
2023
|
+
utils.check_attribute_match(strides, operand.strides, "strides")
|
|
2024
|
+
|
|
2025
|
+
if self.device_id != device_id:
|
|
2026
|
+
raise ValueError(
|
|
2027
|
+
f"The operand {operand_name} must be on the same device ({device_id}) as the original operand "
|
|
2028
|
+
f"({self.device_id})."
|
|
2029
|
+
)
|
|
2030
|
+
|
|
2031
|
+
# Finally, replace the original operand by the new one.
|
|
2032
|
+
if operand_index is not None:
|
|
2033
|
+
self.operands[operand_index] = operand
|
|
2034
|
+
else:
|
|
2035
|
+
self.epilog_operands[epilog_name] = operand
|
|
2036
|
+
# Update the epilog pointer, since we're starting afresh.
|
|
2037
|
+
self.epilog_input_name_to_handler[epilog_name].update(mm_desc_ifc, operand)
|
|
2038
|
+
|
|
2039
|
+
self.logger.info(f"Operand '{operand_name}' has been reset to the new value.")
|
|
2040
|
+
|
|
2041
|
+
return
|
|
2042
|
+
|
|
2043
|
+
@utils.precondition(_check_valid_matmul)
|
|
2044
|
+
def reset_operands(
|
|
2045
|
+
self,
|
|
2046
|
+
a=None,
|
|
2047
|
+
b=None,
|
|
2048
|
+
c=None,
|
|
2049
|
+
*,
|
|
2050
|
+
alpha=None,
|
|
2051
|
+
beta=None,
|
|
2052
|
+
quantization_scales=None,
|
|
2053
|
+
epilog_inputs=None,
|
|
2054
|
+
stream: utils.AnyStream | int | None = None,
|
|
2055
|
+
):
|
|
2056
|
+
"""
|
|
2057
|
+
Reset the operands held by this :class:`Matmul` instance.
|
|
2058
|
+
|
|
2059
|
+
This method has two use cases:
|
|
2060
|
+
(1) it can be used to provide new operands for execution when the original
|
|
2061
|
+
operands are on the CPU
|
|
2062
|
+
(2) it can be used to release the internal reference to the previous operands
|
|
2063
|
+
and make their memory available for other use by passing ``None`` for *all*
|
|
2064
|
+
arguments. In this case, this method must be called again to provide the
|
|
2065
|
+
desired operands before another call to execution APIs like :meth:`autotune`
|
|
2066
|
+
or :meth:`execute`.
|
|
2067
|
+
|
|
2068
|
+
This method is not needed when the operands reside on the GPU and in-place
|
|
2069
|
+
operations are used to update the operand values.
|
|
2070
|
+
|
|
2071
|
+
This method will perform various checks on the new operands to make sure:
|
|
2072
|
+
|
|
2073
|
+
- The shapes, strides, datatypes match those of the old ones.
|
|
2074
|
+
- The packages that the operands belong to match those of the old ones.
|
|
2075
|
+
- If input tensors are on GPU, the device must match.
|
|
2076
|
+
|
|
2077
|
+
Args:
|
|
2078
|
+
a: {a}
|
|
2079
|
+
|
|
2080
|
+
b: {b}
|
|
2081
|
+
|
|
2082
|
+
c: {c}
|
|
2083
|
+
{c_admonitions}
|
|
2084
|
+
|
|
2085
|
+
alpha: {alpha}
|
|
2086
|
+
|
|
2087
|
+
beta: {beta}
|
|
2088
|
+
|
|
2089
|
+
epilog_inputs: {epilog_inputs}
|
|
2090
|
+
|
|
2091
|
+
stream: {stream}
|
|
2092
|
+
|
|
2093
|
+
quantization_scales: {quantization_scales}
|
|
2094
|
+
|
|
2095
|
+
Examples:
|
|
2096
|
+
|
|
2097
|
+
>>> import cupy as cp
|
|
2098
|
+
>>> import nvmath
|
|
2099
|
+
|
|
2100
|
+
Create two 3-D float64 ndarrays on the GPU:
|
|
2101
|
+
|
|
2102
|
+
>>> M, N, K = 128, 128, 256
|
|
2103
|
+
>>> a = cp.random.rand(M, K)
|
|
2104
|
+
>>> b = cp.random.rand(K, N)
|
|
2105
|
+
|
|
2106
|
+
Create an matrix multiplication object as a context manager
|
|
2107
|
+
|
|
2108
|
+
>>> with nvmath.linalg.advanced.Matmul(a, b) as mm:
|
|
2109
|
+
... # Plan the operation.
|
|
2110
|
+
... algorithms = mm.plan()
|
|
2111
|
+
...
|
|
2112
|
+
... # Execute the MM to get the first result.
|
|
2113
|
+
... r1 = mm.execute()
|
|
2114
|
+
...
|
|
2115
|
+
... # Reset the operands to new CuPy ndarrays.
|
|
2116
|
+
... c = cp.random.rand(M, K)
|
|
2117
|
+
... d = cp.random.rand(K, N)
|
|
2118
|
+
... mm.reset_operands(c, d)
|
|
2119
|
+
...
|
|
2120
|
+
... # Execute to get the new result corresponding to the updated operands.
|
|
2121
|
+
... r2 = mm.execute()
|
|
2122
|
+
|
|
2123
|
+
Note that if only a subset of operands are reset, the operands that are not
|
|
2124
|
+
reset hold their original values.
|
|
2125
|
+
|
|
2126
|
+
With :meth:`reset_operands`, minimal overhead is achieved as problem
|
|
2127
|
+
specification and planning are only performed once.
|
|
2128
|
+
|
|
2129
|
+
For the particular example above, explicitly calling :meth:`reset_operands` is
|
|
2130
|
+
equivalent to updating the operands in-place, i.e, replacing
|
|
2131
|
+
``mm.reset_operand(c, d)`` with ``a[:]=c`` and ``b[:]=d``. Note that updating
|
|
2132
|
+
the operand in-place should be adopted with caution as it can only yield the
|
|
2133
|
+
expected result under the additional constraint below:
|
|
2134
|
+
|
|
2135
|
+
- The operand is on the GPU (more precisely, the operand memory space should
|
|
2136
|
+
be accessible from the execution space).
|
|
2137
|
+
|
|
2138
|
+
For more details, please refer to `inplace update example
|
|
2139
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/linalg/advanced/matmul/example05_stateful_inplace.py>`_.
|
|
2140
|
+
"""
|
|
2141
|
+
|
|
2142
|
+
if c is not None and self.num_operands == 2:
|
|
2143
|
+
raise ValueError(
|
|
2144
|
+
"The matrix multiplication problem specification does not include operand C, so it cannot be reset."
|
|
2145
|
+
)
|
|
2146
|
+
|
|
2147
|
+
if a is None and b is None and c is None and epilog_inputs is None and alpha is None and beta is None:
|
|
2148
|
+
self.operands = None
|
|
2149
|
+
self.epilog_operands = {}
|
|
2150
|
+
self.logger.info("The operands have been reset to None.")
|
|
2151
|
+
return
|
|
2152
|
+
|
|
2153
|
+
# If the operands have been reset to None, then all required operands (a, b, c, and
|
|
2154
|
+
# epilog_inputs need to be provided).
|
|
2155
|
+
if self.operands is None:
|
|
2156
|
+
if a is None or b is None or (c is None and self.num_operands == 3):
|
|
2157
|
+
op_names = "A, B"
|
|
2158
|
+
if c is None and self.num_operands == 3:
|
|
2159
|
+
op_names += ", C"
|
|
2160
|
+
raise ValueError(f"Operands {op_names} must be provided.")
|
|
2161
|
+
epilog_names = self.epilog_inputs_traits.keys()
|
|
2162
|
+
if epilog_inputs is None:
|
|
2163
|
+
if epilog_names:
|
|
2164
|
+
raise ValueError(f"The epilog inputs {epilog_names} must be provided.")
|
|
2165
|
+
else:
|
|
2166
|
+
# Check that all required epilog inputs names are provided.
|
|
2167
|
+
if epilog_names != epilog_inputs.keys():
|
|
2168
|
+
raise ValueError(
|
|
2169
|
+
f"The epilog inputs {epilog_names} are required. The provided epilog input names are "
|
|
2170
|
+
f"{epilog_inputs.keys()}."
|
|
2171
|
+
)
|
|
2172
|
+
self.operands = [None] * self.num_operands
|
|
2173
|
+
self.epilog_operands = dict.fromkeys(epilog_names)
|
|
2174
|
+
|
|
2175
|
+
# Future operations on the workspace stream should be ordered after the computation.
|
|
2176
|
+
if self.last_compute_event is not None:
|
|
2177
|
+
self.workspace_stream.wait(self.last_compute_event)
|
|
2178
|
+
self.last_compute_event = None
|
|
2179
|
+
|
|
2180
|
+
# Update alpha.
|
|
2181
|
+
if alpha is not None:
|
|
2182
|
+
try:
|
|
2183
|
+
self.alpha[0] = alpha
|
|
2184
|
+
except (ValueError, TypeError) as e:
|
|
2185
|
+
raise ValueError(
|
|
2186
|
+
f"The value provided for alpha {alpha} is not convertible to dtype '{self.alpha.dtype}'."
|
|
2187
|
+
) from e
|
|
2188
|
+
|
|
2189
|
+
# Update beta.
|
|
2190
|
+
if beta is not None:
|
|
2191
|
+
if self.num_operands == 2:
|
|
2192
|
+
self.logger.warning(f"Matmul: The provided beta value {beta} is ignored since operand C is not specified.")
|
|
2193
|
+
else:
|
|
2194
|
+
try:
|
|
2195
|
+
self.beta[0] = beta
|
|
2196
|
+
except (ValueError, TypeError) as e:
|
|
2197
|
+
raise ValueError(
|
|
2198
|
+
f"The value provided for beta {beta} is not convertible to dtype '{self.beta.dtype}'."
|
|
2199
|
+
) from e
|
|
2200
|
+
|
|
2201
|
+
stream_holder = utils.get_or_create_stream(self.device_id, stream, self.package)
|
|
2202
|
+
|
|
2203
|
+
# Update quantization_scales.
|
|
2204
|
+
if quantization_scales is not None:
|
|
2205
|
+
quantization_scales = self._validate_operand_scales(quantization_scales, all_required=False)
|
|
2206
|
+
if quantization_scales.a is not None:
|
|
2207
|
+
self.quantization_scales.a = quantization_scales.a
|
|
2208
|
+
if quantization_scales.b is not None:
|
|
2209
|
+
self.quantization_scales.b = quantization_scales.b
|
|
2210
|
+
if quantization_scales.c is not None:
|
|
2211
|
+
self.quantization_scales.c = quantization_scales.c
|
|
2212
|
+
if quantization_scales.d is not None:
|
|
2213
|
+
self.quantization_scales.d = quantization_scales.d
|
|
2214
|
+
self._prepare_operand_quantization_scales(self.quantization_scales, stream_holder)
|
|
2215
|
+
|
|
2216
|
+
if epilog_inputs is not None and "aux_quantization_scale" in epilog_inputs:
|
|
2217
|
+
epilog_inputs = epilog_inputs.copy()
|
|
2218
|
+
aux_quantization_scale = epilog_inputs.pop("aux_quantization_scale")
|
|
2219
|
+
self._validate_epilog_aux_scale(aux_quantization_scale, required=False)
|
|
2220
|
+
self._prepare_quantization_scale(
|
|
2221
|
+
aux_quantization_scale, "epilog_aux", cublas_operand="epilogue_aux", stream_holder=stream_holder
|
|
2222
|
+
)
|
|
2223
|
+
|
|
2224
|
+
# Reset the provided operands.
|
|
2225
|
+
if a is not None:
|
|
2226
|
+
a = tensor_wrapper.wrap_operand(a)
|
|
2227
|
+
index = 0
|
|
2228
|
+
self._check_and_set_operand(
|
|
2229
|
+
a,
|
|
2230
|
+
"A",
|
|
2231
|
+
self.mm_desc_ifc,
|
|
2232
|
+
stream_holder,
|
|
2233
|
+
operand_index=index,
|
|
2234
|
+
dtype=self.a_dtype_name,
|
|
2235
|
+
extents=self.operand_extents[index],
|
|
2236
|
+
strides=self.operand_strides[index],
|
|
2237
|
+
)
|
|
2238
|
+
|
|
2239
|
+
if b is not None:
|
|
2240
|
+
b = tensor_wrapper.wrap_operand(b)
|
|
2241
|
+
index = 1
|
|
2242
|
+
self._check_and_set_operand(
|
|
2243
|
+
b,
|
|
2244
|
+
"B",
|
|
2245
|
+
self.mm_desc_ifc,
|
|
2246
|
+
stream_holder,
|
|
2247
|
+
operand_index=index,
|
|
2248
|
+
dtype=self.b_dtype_name,
|
|
2249
|
+
extents=self.operand_extents[index],
|
|
2250
|
+
strides=self.operand_strides[index],
|
|
2251
|
+
)
|
|
2252
|
+
|
|
2253
|
+
if c is not None: # If we get here, we know that C is one of the operands in the problem specification.
|
|
2254
|
+
c = tensor_wrapper.wrap_operand(c)
|
|
2255
|
+
index = 2
|
|
2256
|
+
self._check_and_set_operand(
|
|
2257
|
+
c,
|
|
2258
|
+
"C",
|
|
2259
|
+
self.mm_desc_ifc,
|
|
2260
|
+
stream_holder,
|
|
2261
|
+
operand_index=index,
|
|
2262
|
+
dtype=self.c_dtype_name,
|
|
2263
|
+
extents=self.operand_extents[index],
|
|
2264
|
+
strides=self.operand_strides[index],
|
|
2265
|
+
)
|
|
2266
|
+
|
|
2267
|
+
# Reset the provided epilog inputs.
|
|
2268
|
+
if epilog_inputs is not None:
|
|
2269
|
+
for name in epilog_inputs:
|
|
2270
|
+
epilog_input = tensor_wrapper.wrap_operand(epilog_inputs[name])
|
|
2271
|
+
self._check_and_set_operand(
|
|
2272
|
+
epilog_input,
|
|
2273
|
+
name,
|
|
2274
|
+
self.mm_desc_ifc,
|
|
2275
|
+
stream_holder,
|
|
2276
|
+
epilog_name=name,
|
|
2277
|
+
dtype=self.epilog_inputs_traits[name].dtype,
|
|
2278
|
+
extents=self.epilog_inputs_traits[name].extents,
|
|
2279
|
+
strides=self.epilog_inputs_traits[name].strides,
|
|
2280
|
+
)
|
|
2281
|
+
|
|
2282
|
+
@utils.precondition(_check_valid_matmul)
|
|
2283
|
+
@utils.precondition(_check_planned, "Autotuning")
|
|
2284
|
+
@utils.precondition(_check_valid_operands, "Autotuning")
|
|
2285
|
+
@utils.atomic(_release_workspace_memory_perhaps_wrapper, method=True)
|
|
2286
|
+
def autotune(
|
|
2287
|
+
self, iterations=10, prune=None, release_workspace=False, stream: utils.AnyStream | int | None = None
|
|
2288
|
+
): # Prune means keep top N of the algorithms only.
|
|
2289
|
+
"""
|
|
2290
|
+
Autotune the matrix multiplication to order the algorithms from the fastest measured
|
|
2291
|
+
execution time to the slowest. Once autotuned, the optimally-ordered algorithm
|
|
2292
|
+
sequence can be accessed using :py:attr:`algorithms`.
|
|
2293
|
+
|
|
2294
|
+
.. note::
|
|
2295
|
+
This function will benchmark each of the algorithms and order the algorithms
|
|
2296
|
+
based on the benchmark results. The measurements can be impacted by factors
|
|
2297
|
+
such as GPU temperature, clock settings, or power consumption. Autotuning
|
|
2298
|
+
in an unstable environment can result in a suboptimal algorithm ordering.
|
|
2299
|
+
If you experience performance problems, consider omitting the autotuning.
|
|
2300
|
+
|
|
2301
|
+
Args:
|
|
2302
|
+
iterations: The number of autotuning iterations to perform.
|
|
2303
|
+
|
|
2304
|
+
prune: An integer N, specifying the top N fastest algorithms to retain after
|
|
2305
|
+
autotuning. The default is to retain all algorithms.
|
|
2306
|
+
|
|
2307
|
+
release_workspace: {release_workspace}
|
|
2308
|
+
|
|
2309
|
+
stream: {stream}
|
|
2310
|
+
"""
|
|
2311
|
+
self.logger.info("= AUTOTUNING PHASE =")
|
|
2312
|
+
# Measure time taken for autotuning.
|
|
2313
|
+
from timeit import default_timer as timer
|
|
2314
|
+
|
|
2315
|
+
self.logger.info("Starting autotuning...")
|
|
2316
|
+
start = timer()
|
|
2317
|
+
|
|
2318
|
+
assert self.algorithm_objects is not None, "Internal Error. self.algorithm_objects should have been set by self.plan()."
|
|
2319
|
+
num_algorithms = len(self.algorithm_objects)
|
|
2320
|
+
if num_algorithms == 1:
|
|
2321
|
+
self.logger.info("Skipping the autotuning, because only one algorithm has been returned in the planning phase.")
|
|
2322
|
+
return
|
|
2323
|
+
limit = min(prune, num_algorithms) if prune is not None else num_algorithms
|
|
2324
|
+
self.logger.info(
|
|
2325
|
+
f"The number of algorithms in the plan is {num_algorithms}, from which the top {limit} will be retained."
|
|
2326
|
+
)
|
|
2327
|
+
self.logger.info(f"The requested number of iterations is {iterations}.")
|
|
2328
|
+
|
|
2329
|
+
# Autotune setup.
|
|
2330
|
+
stream_holder = utils.get_or_create_stream(self.device_id, stream, self.package)
|
|
2331
|
+
|
|
2332
|
+
# Allocate workspace if needed.
|
|
2333
|
+
self._allocate_workspace_memory_perhaps(stream_holder)
|
|
2334
|
+
|
|
2335
|
+
# Create empty tensors for auxiliary output.
|
|
2336
|
+
epilog_outputs = {}
|
|
2337
|
+
for handler in self.epilog_output_handlers:
|
|
2338
|
+
name = handler.name
|
|
2339
|
+
shape, strides, dtype_name = handler.attributes()
|
|
2340
|
+
epilog_outputs[name] = aux = utils.create_empty_tensor(
|
|
2341
|
+
self.result_class,
|
|
2342
|
+
shape,
|
|
2343
|
+
dtype_name,
|
|
2344
|
+
self.device_id,
|
|
2345
|
+
stream_holder,
|
|
2346
|
+
verify_strides=False,
|
|
2347
|
+
strides=strides,
|
|
2348
|
+
)
|
|
2349
|
+
|
|
2350
|
+
# Update the data pointer in the MM descriptor.
|
|
2351
|
+
handler.update_ptr(self.mm_desc_ifc, aux.data_ptr)
|
|
2352
|
+
|
|
2353
|
+
# Create empty tensor for the result.
|
|
2354
|
+
assert self.result_traits is not None, "Internal Error. self.result_traits should have been set by self.plan()."
|
|
2355
|
+
result = utils.create_empty_tensor(
|
|
2356
|
+
self.result_class,
|
|
2357
|
+
self.result_traits.result_shape,
|
|
2358
|
+
self.d_dtype_name,
|
|
2359
|
+
self.device_id,
|
|
2360
|
+
stream_holder,
|
|
2361
|
+
verify_strides=False,
|
|
2362
|
+
strides=self.result_traits.result_strides,
|
|
2363
|
+
)
|
|
2364
|
+
result_ptr = result.data_ptr
|
|
2365
|
+
|
|
2366
|
+
c_ptr = self.operands[2].data_ptr if self.num_operands == 3 else result_ptr
|
|
2367
|
+
a, b = self.operands[0], self.operands[1]
|
|
2368
|
+
raw_workspace_ptr = utils.get_ptr_from_memory_pointer(self.workspace_ptr)
|
|
2369
|
+
alpha_ptr, a_ptr, b_ptr, beta_ptr = self.alpha.ctypes.data, a.data_ptr, b.data_ptr, self.beta.ctypes.data
|
|
2370
|
+
|
|
2371
|
+
def execute_matmul(algorithm_ptr):
|
|
2372
|
+
cublaslt.matmul(
|
|
2373
|
+
self.handle,
|
|
2374
|
+
self.mm_desc,
|
|
2375
|
+
alpha_ptr,
|
|
2376
|
+
a_ptr,
|
|
2377
|
+
self.a_layout_ptr,
|
|
2378
|
+
b_ptr,
|
|
2379
|
+
self.b_layout_ptr,
|
|
2380
|
+
beta_ptr,
|
|
2381
|
+
c_ptr,
|
|
2382
|
+
self.c_layout_ptr,
|
|
2383
|
+
result_ptr,
|
|
2384
|
+
self.d_layout_ptr,
|
|
2385
|
+
algorithm_ptr,
|
|
2386
|
+
raw_workspace_ptr,
|
|
2387
|
+
self.workspace_size,
|
|
2388
|
+
stream_holder.ptr,
|
|
2389
|
+
)
|
|
2390
|
+
|
|
2391
|
+
# Tune.
|
|
2392
|
+
with utils.cuda_call_ctx(stream_holder, blocking=False, timing=False) as (
|
|
2393
|
+
self.last_compute_event,
|
|
2394
|
+
_,
|
|
2395
|
+
):
|
|
2396
|
+
gpu_times = np.empty(shape=(len(self.algorithms_buffer), iterations), dtype=float)
|
|
2397
|
+
algorithm_idxs = list(range(len(self.algorithms_buffer)))
|
|
2398
|
+
timing_enabled_options = ccx.EventOptions(enable_timing=True)
|
|
2399
|
+
start0 = stream_holder.obj.device.create_event(options=timing_enabled_options)
|
|
2400
|
+
end0 = stream_holder.obj.device.create_event(options=timing_enabled_options)
|
|
2401
|
+
for i in range(iterations):
|
|
2402
|
+
random.shuffle(algorithm_idxs)
|
|
2403
|
+
for algorithm_idx in algorithm_idxs:
|
|
2404
|
+
algorithm_ptr = self.algorithms_buffer[algorithm_idx]["algo"].ctypes.data
|
|
2405
|
+
stream_holder.obj.record(start0)
|
|
2406
|
+
execute_matmul(algorithm_ptr=algorithm_ptr)
|
|
2407
|
+
stream_holder.obj.record(end0)
|
|
2408
|
+
# FIXME: @dching Calling sync() here could slow down tuning by forcing
|
|
2409
|
+
# the device to wait for the host to record the elapsed time. It may be
|
|
2410
|
+
# faster to store references to 2 * len(iterations) *
|
|
2411
|
+
# len(algorithms_buffer) Events and compute the elapsed time at the end.
|
|
2412
|
+
end0.sync()
|
|
2413
|
+
gpu_times[algorithm_idx, i] = end0 - start0
|
|
2414
|
+
|
|
2415
|
+
gpu_times = np.median(gpu_times, axis=1)
|
|
2416
|
+
|
|
2417
|
+
# Establish ordering wrt the computation and free workspace if requested.
|
|
2418
|
+
self._release_workspace_memory_perhaps(release_workspace=release_workspace)
|
|
2419
|
+
self._reset_workspace_allocation_tracking()
|
|
2420
|
+
|
|
2421
|
+
# Get the sort order based on the GPU times.
|
|
2422
|
+
sorted_gpu_times, sort_order = zip(*sorted(zip(gpu_times, range(num_algorithms), strict=True)), strict=True)
|
|
2423
|
+
|
|
2424
|
+
# Reorder the algorithms buffer and algorithm objects according to the sort order,
|
|
2425
|
+
# and prune it.
|
|
2426
|
+
sort_order = sort_order[:limit]
|
|
2427
|
+
self.algorithms_buffer = self.algorithms_buffer[list(sort_order)]
|
|
2428
|
+
self.algorithm_objects = tuple(self.algorithm_objects[i] for i in sort_order)
|
|
2429
|
+
|
|
2430
|
+
# Update cached first (best) algorithm struct after tuning.
|
|
2431
|
+
self.cached_best_algorithm_struct = self.algorithms_buffer[0]["algo"]
|
|
2432
|
+
|
|
2433
|
+
# Create the map from object to buffer.
|
|
2434
|
+
self.algorithm_object_to_buffer = dict(zip(self.algorithm_objects, self.algorithms_buffer, strict=True))
|
|
2435
|
+
|
|
2436
|
+
gpu_times_str = ", ".join(f"{t:0.3f}" for t in gpu_times)
|
|
2437
|
+
self.logger.info(f"The autotuned GPU times (in milliseconds) are: {gpu_times_str}.")
|
|
2438
|
+
self.logger.info(f"The corresponding sort order is: {sort_order}.")
|
|
2439
|
+
orig_flop_rate = self.flop_count / gpu_times[0] * 1000
|
|
2440
|
+
if sort_order[0] != 0:
|
|
2441
|
+
self.logger.info(
|
|
2442
|
+
f"Autotuning found that the algorithm originally ranked {sort_order[0]} is the best out of the "
|
|
2443
|
+
f"{num_algorithms} in the plan, and moved it to rank 0."
|
|
2444
|
+
)
|
|
2445
|
+
new_flop_rate = self.flop_count / sorted_gpu_times[0] * 1000
|
|
2446
|
+
self.logger.info(
|
|
2447
|
+
f"Autotuning has improved performance from {formatters.FLOPSStr(orig_flop_rate, 'FLOP/s')} to "
|
|
2448
|
+
f"{formatters.FLOPSStr(new_flop_rate, 'FLOP/s')}."
|
|
2449
|
+
)
|
|
2450
|
+
else:
|
|
2451
|
+
self.logger.info(
|
|
2452
|
+
f"Autotuning found that the algorithm ranked best by the plan heuristics remains the best out of the "
|
|
2453
|
+
f"{num_algorithms} algorithms in the plan."
|
|
2454
|
+
)
|
|
2455
|
+
self.logger.info(f"The best performance remains at {formatters.FLOPSStr(orig_flop_rate, 'FLOP/s')}.")
|
|
2456
|
+
|
|
2457
|
+
end = timer()
|
|
2458
|
+
self.logger.info(f"The autotuning took {(end - start) * 1000.0:.3f} ms to complete.")
|
|
2459
|
+
|
|
2460
|
+
@utils.precondition(_check_valid_matmul)
|
|
2461
|
+
@utils.precondition(_check_planned, "Execution")
|
|
2462
|
+
@utils.precondition(_check_valid_operands, "Execution")
|
|
2463
|
+
@utils.atomic(_release_workspace_memory_perhaps_wrapper, method=True)
|
|
2464
|
+
def execute(self, *, algorithm=None, release_workspace=False, stream: utils.AnyStream | int | None = None):
|
|
2465
|
+
"""
|
|
2466
|
+
Execute a prepared (planned and possibly autotuned) matrix multiplication.
|
|
2467
|
+
|
|
2468
|
+
Args:
|
|
2469
|
+
algorithm: (Experimental) An algorithm chosen from the sequence returned by
|
|
2470
|
+
:meth:`plan` or :py:attr:`algorithms`. By default, the first algorithm in
|
|
2471
|
+
the sequence is used.
|
|
2472
|
+
|
|
2473
|
+
release_workspace: {release_workspace}
|
|
2474
|
+
|
|
2475
|
+
stream: {stream}
|
|
2476
|
+
|
|
2477
|
+
Returns:
|
|
2478
|
+
{result}
|
|
2479
|
+
"""
|
|
2480
|
+
log_info = self.logger.isEnabledFor(logging.INFO)
|
|
2481
|
+
log_debug = self.logger.isEnabledFor(logging.DEBUG)
|
|
2482
|
+
|
|
2483
|
+
if log_info:
|
|
2484
|
+
self.logger.info("= EXECUTION PHASE =")
|
|
2485
|
+
stream_holder = utils.get_or_create_stream(self.device_id, stream, self.package)
|
|
2486
|
+
if log_info:
|
|
2487
|
+
self.logger.info(f"The specified stream for execute() is {stream_holder.obj}.")
|
|
2488
|
+
|
|
2489
|
+
# Allocate workspace if needed.
|
|
2490
|
+
self._allocate_workspace_memory_perhaps(stream_holder)
|
|
2491
|
+
|
|
2492
|
+
# Create empty tensors for auxiliary output.
|
|
2493
|
+
for handler in self.epilog_output_handlers:
|
|
2494
|
+
name = handler.name
|
|
2495
|
+
shape, strides, dtype_name = handler.attributes()
|
|
2496
|
+
if log_debug:
|
|
2497
|
+
self.logger.debug(f"Beginning auxiliary output tensor '{name}' creation...")
|
|
2498
|
+
self.logger.debug(f"The '{name}' tensor shape = {shape} with strides = {strides} and data type '{dtype_name}'.")
|
|
2499
|
+
self.epilog_outputs[name] = aux = utils.create_empty_tensor(
|
|
2500
|
+
self.result_class,
|
|
2501
|
+
shape,
|
|
2502
|
+
dtype_name,
|
|
2503
|
+
self.device_id,
|
|
2504
|
+
stream_holder,
|
|
2505
|
+
verify_strides=False,
|
|
2506
|
+
strides=strides,
|
|
2507
|
+
)
|
|
2508
|
+
if log_debug:
|
|
2509
|
+
self.logger.debug(f"The auxiliary output tensor '{name}' has been created.")
|
|
2510
|
+
if self.preferences.epilog.aux_amax: # type: ignore[attr-defined]
|
|
2511
|
+
if "float8" not in dtype_name:
|
|
2512
|
+
raise ValueError("epilog.aux_amax=True is not supported when epilog output type is not FP8.")
|
|
2513
|
+
self.epilog_outputs[f"{name}_amax"] = utils.create_empty_tensor(
|
|
2514
|
+
self.result_class,
|
|
2515
|
+
(1,),
|
|
2516
|
+
"float32", # This is the only type allowed by cuBLAS for AMAX.
|
|
2517
|
+
self.device_id,
|
|
2518
|
+
stream_holder,
|
|
2519
|
+
verify_strides=False,
|
|
2520
|
+
)
|
|
2521
|
+
self.mm_desc_ifc.epilogue_aux_amax_pointer = self.epilog_outputs[f"{name}_amax"].data_ptr
|
|
2522
|
+
|
|
2523
|
+
# Update the data pointer in the MM descriptor.
|
|
2524
|
+
handler.update_ptr(self.mm_desc_ifc, aux.data_ptr)
|
|
2525
|
+
|
|
2526
|
+
# Create empty tensor for the result.
|
|
2527
|
+
assert self.result_traits is not None, "Internal Error. self.result_traits should have been set by self.plan()"
|
|
2528
|
+
if log_debug:
|
|
2529
|
+
self.logger.debug("Beginning output (empty) tensor creation...")
|
|
2530
|
+
self.logger.debug(
|
|
2531
|
+
f"The output tensor shape = {self.result_traits.result_shape} with strides = "
|
|
2532
|
+
f"{self.result_traits.result_strides} and data type '{self.d_dtype_name}'."
|
|
2533
|
+
)
|
|
2534
|
+
self.result = utils.create_empty_tensor(
|
|
2535
|
+
self.result_class,
|
|
2536
|
+
self.result_traits.result_shape,
|
|
2537
|
+
self.d_dtype_name,
|
|
2538
|
+
self.device_id,
|
|
2539
|
+
stream_holder,
|
|
2540
|
+
verify_strides=False,
|
|
2541
|
+
strides=self.result_traits.result_strides,
|
|
2542
|
+
)
|
|
2543
|
+
if log_debug:
|
|
2544
|
+
self.logger.debug("The output (empty) tensor has been created.")
|
|
2545
|
+
|
|
2546
|
+
self.aux_outputs = {}
|
|
2547
|
+
|
|
2548
|
+
if self.options.result_amax:
|
|
2549
|
+
self.aux_outputs["result_amax"] = utils.create_empty_tensor(
|
|
2550
|
+
self.result_class,
|
|
2551
|
+
(1,),
|
|
2552
|
+
"float32", # This is the only type allowed by cuBLAS for AMAX.
|
|
2553
|
+
self.device_id,
|
|
2554
|
+
stream_holder,
|
|
2555
|
+
verify_strides=False,
|
|
2556
|
+
)
|
|
2557
|
+
self.mm_desc_ifc.amax_d_pointer = self.aux_outputs["result_amax"].data_ptr
|
|
2558
|
+
|
|
2559
|
+
if self.options.block_scaling and self.d_dtype_width == 8:
|
|
2560
|
+
self.aux_outputs["d_out_scale"] = utils.create_empty_tensor(
|
|
2561
|
+
self.result_class,
|
|
2562
|
+
(self.mm_traits.batch_count * self.result_traits.result_shape[-1] * self.result_traits.result_shape[-2] // 32),
|
|
2563
|
+
"uint8",
|
|
2564
|
+
self.device_id,
|
|
2565
|
+
stream_holder,
|
|
2566
|
+
verify_strides=False,
|
|
2567
|
+
)
|
|
2568
|
+
self.mm_desc_ifc.d_out_scale_pointer = self.aux_outputs["d_out_scale"].data_ptr
|
|
2569
|
+
|
|
2570
|
+
# Select the first (best) algorithm if one is not provided.
|
|
2571
|
+
if algorithm is None:
|
|
2572
|
+
algorithm_struct = self.cached_best_algorithm_struct
|
|
2573
|
+
if log_info:
|
|
2574
|
+
self.logger.info(
|
|
2575
|
+
"The highest ranked algorithm in the plan (algorithm id = "
|
|
2576
|
+
f"{self.algorithm_objects[0].algorithm_id}) will be used."
|
|
2577
|
+
)
|
|
2578
|
+
else:
|
|
2579
|
+
if algorithm not in self.algorithm_objects:
|
|
2580
|
+
raise ValueError("Algorithm passed to execute() has to be included in the plan() algorithms")
|
|
2581
|
+
algorithm_struct = self.algorithm_object_to_buffer[algorithm]["algo"]
|
|
2582
|
+
if log_info:
|
|
2583
|
+
self.logger.info(f"The specified algorithm (algorithm id = {algorithm.algorithm_id}) will be used.")
|
|
2584
|
+
|
|
2585
|
+
c_ptr = self.operands[2].data_ptr if self.num_operands == 3 else self.result.data_ptr
|
|
2586
|
+
a, b = self.operands[0], self.operands[1]
|
|
2587
|
+
raw_workspace_ptr = utils.get_ptr_from_memory_pointer(self.workspace_ptr)
|
|
2588
|
+
if log_info:
|
|
2589
|
+
self.logger.info("Starting matrix multiplication...")
|
|
2590
|
+
self.logger.info(f"{self.call_prologue}")
|
|
2591
|
+
with utils.cuda_call_ctx(stream_holder, self.blocking, timing=log_info) as (
|
|
2592
|
+
self.last_compute_event,
|
|
2593
|
+
elapsed,
|
|
2594
|
+
):
|
|
2595
|
+
cublaslt.matmul(
|
|
2596
|
+
self.handle,
|
|
2597
|
+
self.mm_desc,
|
|
2598
|
+
self.alpha.ctypes.data,
|
|
2599
|
+
a.data_ptr,
|
|
2600
|
+
self.a_layout_ptr,
|
|
2601
|
+
b.data_ptr,
|
|
2602
|
+
self.b_layout_ptr,
|
|
2603
|
+
self.beta.ctypes.data,
|
|
2604
|
+
c_ptr,
|
|
2605
|
+
self.c_layout_ptr,
|
|
2606
|
+
self.result.data_ptr,
|
|
2607
|
+
self.d_layout_ptr,
|
|
2608
|
+
algorithm_struct.ctypes.data,
|
|
2609
|
+
raw_workspace_ptr,
|
|
2610
|
+
self.workspace_size,
|
|
2611
|
+
stream_holder.ptr,
|
|
2612
|
+
)
|
|
2613
|
+
|
|
2614
|
+
if log_info and elapsed.data is not None:
|
|
2615
|
+
self.logger.info(f"The matrix multiplication calculation took {elapsed.data:.3f} ms to complete.")
|
|
2616
|
+
|
|
2617
|
+
# Establish ordering wrt the computation and free workspace if requested.
|
|
2618
|
+
if release_workspace:
|
|
2619
|
+
self._release_workspace_memory_perhaps(True)
|
|
2620
|
+
|
|
2621
|
+
# Return the result and auxiliary outputs, if present.
|
|
2622
|
+
all_outputs = self.epilog_outputs | self.aux_outputs
|
|
2623
|
+
if self.memory_space == "cpu":
|
|
2624
|
+
out = self.result.to("cpu", stream_holder=stream_holder).tensor
|
|
2625
|
+
# Copy auxiliary output to CPU.
|
|
2626
|
+
aux = {name: all_outputs[name].to("cpu", stream_holder=stream_holder).tensor for name in all_outputs}
|
|
2627
|
+
else:
|
|
2628
|
+
out = self.result.tensor
|
|
2629
|
+
# Return the unwrapped epilog output tensor(s).
|
|
2630
|
+
aux = {name: all_outputs[name].tensor for name in all_outputs}
|
|
2631
|
+
|
|
2632
|
+
# Release internal reference to the result to permit recycling of memory.
|
|
2633
|
+
self.result = None
|
|
2634
|
+
self.aux_outputs = {}
|
|
2635
|
+
self.epilog_outputs = {}
|
|
2636
|
+
self._reset_workspace_allocation_tracking()
|
|
2637
|
+
|
|
2638
|
+
if aux:
|
|
2639
|
+
return out, aux
|
|
2640
|
+
|
|
2641
|
+
return out
|
|
2642
|
+
|
|
2643
|
+
def free(self):
|
|
2644
|
+
"""Free Matmul resources.
|
|
2645
|
+
|
|
2646
|
+
It is recommended that the :class:`Matmul` object be used within a context, but if
|
|
2647
|
+
it is not possible then this method must be called explicitly to ensure that the
|
|
2648
|
+
matrix multiplication resources (especially internal library objects) are properly
|
|
2649
|
+
cleaned up.
|
|
2650
|
+
"""
|
|
2651
|
+
|
|
2652
|
+
if not self.valid_state:
|
|
2653
|
+
return
|
|
2654
|
+
|
|
2655
|
+
try:
|
|
2656
|
+
# Future operations on the workspace stream should be ordered after the
|
|
2657
|
+
# computation.
|
|
2658
|
+
if self.last_compute_event is not None:
|
|
2659
|
+
self.workspace_stream.wait(self.last_compute_event)
|
|
2660
|
+
self.last_compute_event = None
|
|
2661
|
+
|
|
2662
|
+
self._free_workspace_memory()
|
|
2663
|
+
|
|
2664
|
+
self._free_plan_resources()
|
|
2665
|
+
|
|
2666
|
+
# We won't destroy the handle.
|
|
2667
|
+
|
|
2668
|
+
except Exception as e:
|
|
2669
|
+
self.logger.critical("Internal error: only part of the Matmul object's resources have been released.")
|
|
2670
|
+
self.logger.critical(str(e))
|
|
2671
|
+
raise e
|
|
2672
|
+
finally:
|
|
2673
|
+
self.valid_state = False
|
|
2674
|
+
|
|
2675
|
+
self.logger.info("The Matmul object's resources have been released.")
|
|
2676
|
+
|
|
2677
|
+
|
|
2678
|
+
@utils.docstring_decorator(SHARED_MM_DOCUMENTATION, skip_missing=False)
|
|
2679
|
+
def matmul(
|
|
2680
|
+
a,
|
|
2681
|
+
b,
|
|
2682
|
+
/,
|
|
2683
|
+
c=None,
|
|
2684
|
+
*,
|
|
2685
|
+
alpha=None,
|
|
2686
|
+
beta=None,
|
|
2687
|
+
epilog=None,
|
|
2688
|
+
epilog_inputs=None,
|
|
2689
|
+
qualifiers=None,
|
|
2690
|
+
quantization_scales=None,
|
|
2691
|
+
options=None,
|
|
2692
|
+
preferences=None,
|
|
2693
|
+
algorithm=None,
|
|
2694
|
+
stream: utils.AnyStream | int | None = None,
|
|
2695
|
+
):
|
|
2696
|
+
"""
|
|
2697
|
+
Perform the specified matrix multiplication computation :math:`F(\\alpha a @ b + \\beta
|
|
2698
|
+
c)`, where :math:`F` is the epilog. This function-form is a wrapper around the stateful
|
|
2699
|
+
:class:`Matmul` object APIs and is meant for *single* use (the user needs to perform
|
|
2700
|
+
just one matrix multiplication, for example), in which case there is no possibility of
|
|
2701
|
+
amortizing preparatory costs.
|
|
2702
|
+
|
|
2703
|
+
Detailed information on what's happening within this function can be obtained by passing
|
|
2704
|
+
in a :class:`logging.Logger` object to :class:`MatmulOptions` or by setting the
|
|
2705
|
+
appropriate options in the root logger object, which is used by default:
|
|
2706
|
+
|
|
2707
|
+
>>> import logging
|
|
2708
|
+
>>> logging.basicConfig(
|
|
2709
|
+
... level=logging.INFO,
|
|
2710
|
+
... format="%(asctime)s %(levelname)-8s %(message)s",
|
|
2711
|
+
... datefmt="%m-%d %H:%M:%S",
|
|
2712
|
+
... )
|
|
2713
|
+
|
|
2714
|
+
A user can select the desired logging level and, in general, take advantage of all of
|
|
2715
|
+
the functionality offered by the Python `logging` module.
|
|
2716
|
+
|
|
2717
|
+
Args:
|
|
2718
|
+
a: {a}
|
|
2719
|
+
|
|
2720
|
+
b: {b}
|
|
2721
|
+
|
|
2722
|
+
c: {c}
|
|
2723
|
+
{c_admonitions}
|
|
2724
|
+
|
|
2725
|
+
alpha: {alpha}
|
|
2726
|
+
|
|
2727
|
+
beta: {beta} from a previously planned and autotuned matrix multiplication.
|
|
2728
|
+
|
|
2729
|
+
epilog: {epilog}
|
|
2730
|
+
|
|
2731
|
+
epilog_inputs: {epilog_inputs}
|
|
2732
|
+
|
|
2733
|
+
qualifiers: {qualifiers}
|
|
2734
|
+
|
|
2735
|
+
options: {options}
|
|
2736
|
+
|
|
2737
|
+
preferences: {preferences}
|
|
2738
|
+
|
|
2739
|
+
algorithm: An object of type :class:`Algorithm` objects can be directly provided to
|
|
2740
|
+
bypass planning, if desired. The algorithm object must be compatible with the
|
|
2741
|
+
matrix multiplication. A typical use for this option is to provide an algorithm
|
|
2742
|
+
that has been serialized (pickled) from a previously planned and autotuned
|
|
2743
|
+
matrix multiplication.
|
|
2744
|
+
|
|
2745
|
+
stream: {stream}
|
|
2746
|
+
|
|
2747
|
+
quantization_scales: {quantization_scales}
|
|
2748
|
+
|
|
2749
|
+
Returns:
|
|
2750
|
+
{result}
|
|
2751
|
+
|
|
2752
|
+
Semantics:
|
|
2753
|
+
{semantics}
|
|
2754
|
+
|
|
2755
|
+
Narrow-precision support:
|
|
2756
|
+
{narrow_precision}
|
|
2757
|
+
|
|
2758
|
+
See Also:
|
|
2759
|
+
:class:`Matmul`, :class:`MatmulOptions`, :class:`MatmulEpilog`,
|
|
2760
|
+
:class:`MatmulPlanPreferences`
|
|
2761
|
+
|
|
2762
|
+
Examples:
|
|
2763
|
+
|
|
2764
|
+
>>> import cupy as cp
|
|
2765
|
+
>>> import nvmath
|
|
2766
|
+
|
|
2767
|
+
Create three float32 ndarrays on the GPU:
|
|
2768
|
+
|
|
2769
|
+
>>> M, N, K = 128, 64, 256
|
|
2770
|
+
>>> a = cp.random.rand(M, K, dtype=cp.float32)
|
|
2771
|
+
>>> b = cp.random.rand(K, N, dtype=cp.float32)
|
|
2772
|
+
>>> c = cp.random.rand(M, N, dtype=cp.float32)
|
|
2773
|
+
|
|
2774
|
+
Perform the operation :math:`\\alpha A @ B + \\beta C` using :func:`matmul`. The
|
|
2775
|
+
result `r` is also a CuPy float64 ndarray:
|
|
2776
|
+
|
|
2777
|
+
>>> r = nvmath.linalg.advanced.matmul(a, b, c, alpha=1.23, beta=0.74)
|
|
2778
|
+
|
|
2779
|
+
An epilog can be used as well. Here we perform
|
|
2780
|
+
:math:`RELU(\\alpha A @ B + \\beta C)`:
|
|
2781
|
+
|
|
2782
|
+
>>> epilog = nvmath.linalg.advanced.MatmulEpilog.RELU
|
|
2783
|
+
>>> r = nvmath.linalg.advanced.matmul(a, b, c, alpha=1.23, beta=0.74, epilog=epilog)
|
|
2784
|
+
|
|
2785
|
+
Options can be provided to customize the operation:
|
|
2786
|
+
|
|
2787
|
+
>>> compute_type = nvmath.linalg.advanced.MatmulComputeType.COMPUTE_32F_FAST_TF32
|
|
2788
|
+
>>> o = nvmath.linalg.advanced.MatmulOptions(compute_type=compute_type)
|
|
2789
|
+
>>> r = nvmath.linalg.advanced.matmul(a, b, options=o)
|
|
2790
|
+
|
|
2791
|
+
See `MatmulOptions` for the complete list of available options.
|
|
2792
|
+
|
|
2793
|
+
The package current stream is used by default, but a stream can be explicitly
|
|
2794
|
+
provided to the Matmul operation. This can be done if the operands are computed on a
|
|
2795
|
+
different stream, for example:
|
|
2796
|
+
|
|
2797
|
+
>>> s = cp.cuda.Stream()
|
|
2798
|
+
>>> with s:
|
|
2799
|
+
... a = cp.random.rand(M, K)
|
|
2800
|
+
... b = cp.random.rand(K, N)
|
|
2801
|
+
>>> r = nvmath.linalg.advanced.matmul(a, b, stream=s)
|
|
2802
|
+
|
|
2803
|
+
The operation above runs on stream `s` and is ordered with respect to the input
|
|
2804
|
+
computation.
|
|
2805
|
+
|
|
2806
|
+
Create NumPy ndarrays on the CPU.
|
|
2807
|
+
|
|
2808
|
+
>>> import numpy as np
|
|
2809
|
+
>>> a = np.random.rand(M, K)
|
|
2810
|
+
>>> b = np.random.rand(K, N)
|
|
2811
|
+
|
|
2812
|
+
Provide the NumPy ndarrays to :func:`matmul`, with the result also being a NumPy
|
|
2813
|
+
ndarray:
|
|
2814
|
+
|
|
2815
|
+
>>> r = nvmath.linalg.advanced.matmul(a, b)
|
|
2816
|
+
|
|
2817
|
+
Notes:
|
|
2818
|
+
- This function is a convenience wrapper around :class:`Matmul` and and is
|
|
2819
|
+
specifically meant for *single* use.
|
|
2820
|
+
|
|
2821
|
+
Further examples can be found in the `nvmath/examples/linalg/advanced/matmul
|
|
2822
|
+
<https://github.com/NVIDIA/nvmath-python/tree/main/examples/linalg/advanced/matmul>`_
|
|
2823
|
+
directory.
|
|
2824
|
+
"""
|
|
2825
|
+
|
|
2826
|
+
# Set algorithm limit to 1, but take a copy first if needed.
|
|
2827
|
+
if isinstance(preferences, _configuration.MatmulPlanPreferences):
|
|
2828
|
+
preferences = copy.copy(preferences)
|
|
2829
|
+
|
|
2830
|
+
preferences = utils.check_or_create_options(
|
|
2831
|
+
_configuration.MatmulPlanPreferences, preferences, "Matrix multiplication plan preferences"
|
|
2832
|
+
)
|
|
2833
|
+
preferences.limit = 1
|
|
2834
|
+
|
|
2835
|
+
if algorithm is None:
|
|
2836
|
+
algorithms = None
|
|
2837
|
+
else:
|
|
2838
|
+
algorithms = [algorithm] # The type of algorithm should be algorithm.Algorithm and will be checked in plan()
|
|
2839
|
+
|
|
2840
|
+
with Matmul(
|
|
2841
|
+
a,
|
|
2842
|
+
b,
|
|
2843
|
+
c=c,
|
|
2844
|
+
alpha=alpha,
|
|
2845
|
+
beta=beta,
|
|
2846
|
+
qualifiers=qualifiers,
|
|
2847
|
+
options=options,
|
|
2848
|
+
stream=stream,
|
|
2849
|
+
quantization_scales=quantization_scales,
|
|
2850
|
+
) as mm:
|
|
2851
|
+
mm.plan(preferences=preferences, epilog=epilog, epilog_inputs=epilog_inputs, stream=stream, algorithms=algorithms)
|
|
2852
|
+
|
|
2853
|
+
r = mm.execute(stream=stream)
|
|
2854
|
+
|
|
2855
|
+
return r
|