nvmath-python 0.9.0__cp314-cp314-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.
Files changed (309) hide show
  1. nvmath/__init__.pxd +0 -0
  2. nvmath/__init__.py +45 -0
  3. nvmath/_internal/__init__.py +0 -0
  4. nvmath/_internal/attribute_ifc_factory.py +330 -0
  5. nvmath/_internal/layout.py +70 -0
  6. nvmath/_internal/templates.py +383 -0
  7. nvmath/_utils.py +150 -0
  8. nvmath/bindings/__init__.py +51 -0
  9. nvmath/bindings/_internal/__init__.pxd +0 -0
  10. nvmath/bindings/_internal/__init__.py +0 -0
  11. nvmath/bindings/_internal/cublas.cp314-win_amd64.pyd +0 -0
  12. nvmath/bindings/_internal/cublas.pxd +530 -0
  13. nvmath/bindings/_internal/cublasLt.cp314-win_amd64.pyd +0 -0
  14. nvmath/bindings/_internal/cublasLt.pxd +59 -0
  15. nvmath/bindings/_internal/cublasMp.pxd +52 -0
  16. nvmath/bindings/_internal/cudss.cp314-win_amd64.pyd +0 -0
  17. nvmath/bindings/_internal/cudss.pxd +47 -0
  18. nvmath/bindings/_internal/cufft.cp314-win_amd64.pyd +0 -0
  19. nvmath/bindings/_internal/cufft.pxd +70 -0
  20. nvmath/bindings/_internal/cufftMp.pxd +77 -0
  21. nvmath/bindings/_internal/curand.cp314-win_amd64.pyd +0 -0
  22. nvmath/bindings/_internal/curand.pxd +42 -0
  23. nvmath/bindings/_internal/cusolver.cp314-win_amd64.pyd +0 -0
  24. nvmath/bindings/_internal/cusolver.pxd +15 -0
  25. nvmath/bindings/_internal/cusolverDn.cp314-win_amd64.pyd +0 -0
  26. nvmath/bindings/_internal/cusolverDn.pxd +406 -0
  27. nvmath/bindings/_internal/cusparse.cp314-win_amd64.pyd +0 -0
  28. nvmath/bindings/_internal/cusparse.pxd +277 -0
  29. nvmath/bindings/_internal/cusparseLt.cp314-win_amd64.pyd +0 -0
  30. nvmath/bindings/_internal/cusparseLt.pxd +48 -0
  31. nvmath/bindings/_internal/cutensor.cp314-win_amd64.pyd +0 -0
  32. nvmath/bindings/_internal/cutensor.pxd +58 -0
  33. nvmath/bindings/_internal/mathdx.cp314-win_amd64.pyd +0 -0
  34. nvmath/bindings/_internal/mathdx.pxd +116 -0
  35. nvmath/bindings/_internal/nvshmem.pxd +29 -0
  36. nvmath/bindings/_internal/utils.cp314-win_amd64.pyd +0 -0
  37. nvmath/bindings/_internal/utils.pxd +179 -0
  38. nvmath/bindings/_internal/utils.pyi +10 -0
  39. nvmath/bindings/cublas.cp314-win_amd64.pyd +0 -0
  40. nvmath/bindings/cublas.pxd +558 -0
  41. nvmath/bindings/cublas.pyi +746 -0
  42. nvmath/bindings/cublasLt.cp314-win_amd64.pyd +0 -0
  43. nvmath/bindings/cublasLt.pxd +109 -0
  44. nvmath/bindings/cublasLt.pyi +1337 -0
  45. nvmath/bindings/cublasMp.pxd +85 -0
  46. nvmath/bindings/cublasMp.pyi +219 -0
  47. nvmath/bindings/cudss.cp314-win_amd64.pyd +0 -0
  48. nvmath/bindings/cudss.pxd +86 -0
  49. nvmath/bindings/cudss.pyi +282 -0
  50. nvmath/bindings/cufft.cp314-win_amd64.pyd +0 -0
  51. nvmath/bindings/cufft.pxd +118 -0
  52. nvmath/bindings/cufft.pyi +241 -0
  53. nvmath/bindings/cufftMp.pxd +124 -0
  54. nvmath/bindings/cufftMp.pyi +260 -0
  55. nvmath/bindings/curand.cp314-win_amd64.pyd +0 -0
  56. nvmath/bindings/curand.pxd +71 -0
  57. nvmath/bindings/curand.pyi +159 -0
  58. nvmath/bindings/cusolver.cp314-win_amd64.pyd +0 -0
  59. nvmath/bindings/cusolver.pxd +62 -0
  60. nvmath/bindings/cusolver.pyi +242 -0
  61. nvmath/bindings/cusolverDn.cp314-win_amd64.pyd +0 -0
  62. nvmath/bindings/cusolverDn.pxd +430 -0
  63. nvmath/bindings/cusolverDn.pyi +416 -0
  64. nvmath/bindings/cusparse.cp314-win_amd64.pyd +0 -0
  65. nvmath/bindings/cusparse.pxd +338 -0
  66. nvmath/bindings/cusparse.pyi +659 -0
  67. nvmath/bindings/cusparseLt.cp314-win_amd64.pyd +0 -0
  68. nvmath/bindings/cusparseLt.pxd +99 -0
  69. nvmath/bindings/cusparseLt.pyi +198 -0
  70. nvmath/bindings/cutensor.cp314-win_amd64.pyd +0 -0
  71. nvmath/bindings/cutensor.pxd +98 -0
  72. nvmath/bindings/cutensor.pyi +294 -0
  73. nvmath/bindings/cycublas.cp314-win_amd64.pyd +0 -0
  74. nvmath/bindings/cycublas.pxd +684 -0
  75. nvmath/bindings/cycublasLt.cp314-win_amd64.pyd +0 -0
  76. nvmath/bindings/cycublasLt.pxd +1055 -0
  77. nvmath/bindings/cycublasMp.pxd +183 -0
  78. nvmath/bindings/cycudss.cp314-win_amd64.pyd +0 -0
  79. nvmath/bindings/cycudss.pxd +224 -0
  80. nvmath/bindings/cycufft.cp314-win_amd64.pyd +0 -0
  81. nvmath/bindings/cycufft.pxd +326 -0
  82. nvmath/bindings/cycufftMp.pxd +334 -0
  83. nvmath/bindings/cycurand.cp314-win_amd64.pyd +0 -0
  84. nvmath/bindings/cycurand.pxd +146 -0
  85. nvmath/bindings/cycusolver.cp314-win_amd64.pyd +0 -0
  86. nvmath/bindings/cycusolver.pxd +154 -0
  87. nvmath/bindings/cycusolverDn.cp314-win_amd64.pyd +0 -0
  88. nvmath/bindings/cycusolverDn.pxd +446 -0
  89. nvmath/bindings/cycusparse.cp314-win_amd64.pyd +0 -0
  90. nvmath/bindings/cycusparse.pxd +470 -0
  91. nvmath/bindings/cycusparseLt.cp314-win_amd64.pyd +0 -0
  92. nvmath/bindings/cycusparseLt.pxd +150 -0
  93. nvmath/bindings/cycutensor.cp314-win_amd64.pyd +0 -0
  94. nvmath/bindings/cycutensor.pxd +192 -0
  95. nvmath/bindings/cymathdx.cp314-win_amd64.pyd +0 -0
  96. nvmath/bindings/cymathdx.pxd +552 -0
  97. nvmath/bindings/cynvshmem.pxd +126 -0
  98. nvmath/bindings/mathdx.cp314-win_amd64.pyd +0 -0
  99. nvmath/bindings/mathdx.pxd +182 -0
  100. nvmath/bindings/mathdx.pyi +1244 -0
  101. nvmath/bindings/nvpl/__init__.pxd +0 -0
  102. nvmath/bindings/nvpl/__init__.py +13 -0
  103. nvmath/bindings/nvpl/_internal/__init__.pxd +0 -0
  104. nvmath/bindings/nvpl/_internal/__init__.py +0 -0
  105. nvmath/bindings/nvpl/_internal/blas.cp314-win_amd64.pyd +0 -0
  106. nvmath/bindings/nvpl/_internal/blas.pxd +125 -0
  107. nvmath/bindings/nvpl/_internal/fft.cp314-win_amd64.pyd +0 -0
  108. nvmath/bindings/nvpl/_internal/fft.pxd +36 -0
  109. nvmath/bindings/nvpl/blas.cp314-win_amd64.pyd +0 -0
  110. nvmath/bindings/nvpl/blas.pxd +85 -0
  111. nvmath/bindings/nvpl/blas.pyi +122 -0
  112. nvmath/bindings/nvpl/cyblas.cp314-win_amd64.pyd +0 -0
  113. nvmath/bindings/nvpl/cyblas.pxd +166 -0
  114. nvmath/bindings/nvpl/cyfft.cp314-win_amd64.pyd +0 -0
  115. nvmath/bindings/nvpl/cyfft.pxd +92 -0
  116. nvmath/bindings/nvpl/fft.cp314-win_amd64.pyd +0 -0
  117. nvmath/bindings/nvpl/fft.pxd +100 -0
  118. nvmath/bindings/nvpl/fft.pyi +100 -0
  119. nvmath/bindings/nvshmem.pxd +54 -0
  120. nvmath/bindings/nvshmem.pyi +179 -0
  121. nvmath/device/__init__.py +21 -0
  122. nvmath/device/_deprecated.py +33 -0
  123. nvmath/device/common.py +313 -0
  124. nvmath/device/common_backend.py +131 -0
  125. nvmath/device/common_cuda.py +201 -0
  126. nvmath/device/common_numba.py +310 -0
  127. nvmath/device/common_opaque_tensor.py +201 -0
  128. nvmath/device/cublasdx.py +1805 -0
  129. nvmath/device/cublasdx_backend.py +807 -0
  130. nvmath/device/cublasdx_numba.py +1612 -0
  131. nvmath/device/cufftdx.py +510 -0
  132. nvmath/device/cufftdx_backend.py +196 -0
  133. nvmath/device/cufftdx_numba.py +196 -0
  134. nvmath/device/curand_kernel.py +9147 -0
  135. nvmath/device/cusolverdx.py +2690 -0
  136. nvmath/device/cusolverdx_backend.py +440 -0
  137. nvmath/device/cusolverdx_numba.py +1624 -0
  138. nvmath/device/llvm_array.py +29 -0
  139. nvmath/device/random.py +445 -0
  140. nvmath/device/random_helpers.py +23 -0
  141. nvmath/device/random_states.py +187 -0
  142. nvmath/device/types.py +147 -0
  143. nvmath/device/vector_types_numba.py +203 -0
  144. nvmath/distributed/__init__.py +205 -0
  145. nvmath/distributed/_internal/__init__.py +0 -0
  146. nvmath/distributed/_internal/nvshmem.py +302 -0
  147. nvmath/distributed/_internal/tensor_ifc.py +67 -0
  148. nvmath/distributed/_internal/tensor_ifc_cupy.py +62 -0
  149. nvmath/distributed/_internal/tensor_ifc_host_device.py +165 -0
  150. nvmath/distributed/_internal/tensor_ifc_numpy.py +41 -0
  151. nvmath/distributed/_internal/tensor_ifc_torch.py +141 -0
  152. nvmath/distributed/_internal/tensor_wrapper.py +78 -0
  153. nvmath/distributed/_utils.py +167 -0
  154. nvmath/distributed/distribution.py +770 -0
  155. nvmath/distributed/fft/__init__.py +7 -0
  156. nvmath/distributed/fft/_configuration.py +79 -0
  157. nvmath/distributed/fft/fft.py +2801 -0
  158. nvmath/distributed/linalg/__init__.py +12 -0
  159. nvmath/distributed/linalg/_internal/__init__.py +3 -0
  160. nvmath/distributed/linalg/_internal/epilog_protocol.py +496 -0
  161. nvmath/distributed/linalg/_internal/matmul_desc_ifc.py +28 -0
  162. nvmath/distributed/linalg/advanced/__init__.py +8 -0
  163. nvmath/distributed/linalg/advanced/_configuration.py +166 -0
  164. nvmath/distributed/linalg/advanced/matmulmod.py +2908 -0
  165. nvmath/distributed/process_group.py +408 -0
  166. nvmath/distributed/reshape/__init__.py +6 -0
  167. nvmath/distributed/reshape/_configuration.py +39 -0
  168. nvmath/distributed/reshape/reshape.py +1256 -0
  169. nvmath/fft/__init__.py +7 -0
  170. nvmath/fft/_configuration.py +208 -0
  171. nvmath/fft/_exec_utils.py +109 -0
  172. nvmath/fft/_helpers.py +237 -0
  173. nvmath/fft/fft.py +2903 -0
  174. nvmath/internal/__init__.pxd +3 -0
  175. nvmath/internal/__init__.py +10 -0
  176. nvmath/internal/_bindings.cp314-win_amd64.pyd +0 -0
  177. nvmath/internal/_bindings.pxd +12 -0
  178. nvmath/internal/_device_utils.py +54 -0
  179. nvmath/internal/enum_utils.py +142 -0
  180. nvmath/internal/formatters.py +87 -0
  181. nvmath/internal/mem_limit.py +51 -0
  182. nvmath/internal/memory.cp314-win_amd64.pyd +0 -0
  183. nvmath/internal/memory.pxd +10 -0
  184. nvmath/internal/memory.pyi +46 -0
  185. nvmath/internal/ndbuffer/__init__.pxd +3 -0
  186. nvmath/internal/ndbuffer/__init__.py +7 -0
  187. nvmath/internal/ndbuffer/copy_kernel/args.h +34 -0
  188. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/array_view.h +52 -0
  189. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/elementwise.h +68 -0
  190. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/grid_indexer.h +69 -0
  191. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/transposed.h +240 -0
  192. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/type_utils.h +39 -0
  193. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/utils.h +132 -0
  194. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/vec.h +159 -0
  195. nvmath/internal/ndbuffer/copy_kernel/elementwise.h +53 -0
  196. nvmath/internal/ndbuffer/copy_kernel/transposed.h +58 -0
  197. nvmath/internal/ndbuffer/copy_kernel.cp314-win_amd64.pyd +0 -0
  198. nvmath/internal/ndbuffer/copy_kernel.pxd +9 -0
  199. nvmath/internal/ndbuffer/copy_kernel.pyi +9 -0
  200. nvmath/internal/ndbuffer/data_layout.cp314-win_amd64.pyd +0 -0
  201. nvmath/internal/ndbuffer/data_layout.pxd +72 -0
  202. nvmath/internal/ndbuffer/data_layout.pyi +23 -0
  203. nvmath/internal/ndbuffer/jit.cp314-win_amd64.pyd +0 -0
  204. nvmath/internal/ndbuffer/jit.pxd +7 -0
  205. nvmath/internal/ndbuffer/jit.pyi +6 -0
  206. nvmath/internal/ndbuffer/ndbuffer.cp314-win_amd64.pyd +0 -0
  207. nvmath/internal/ndbuffer/ndbuffer.pxd +36 -0
  208. nvmath/internal/ndbuffer/ndbuffer.pyi +41 -0
  209. nvmath/internal/ndbuffer/package_utils.cp314-win_amd64.pyd +0 -0
  210. nvmath/internal/ndbuffer/package_utils.pxd +11 -0
  211. nvmath/internal/ndbuffer/package_utils.pyi +12 -0
  212. nvmath/internal/package_ifc.py +147 -0
  213. nvmath/internal/package_ifc_cuda.py +49 -0
  214. nvmath/internal/package_ifc_cupy.py +49 -0
  215. nvmath/internal/package_ifc_torch.py +31 -0
  216. nvmath/internal/package_wrapper.py +14 -0
  217. nvmath/internal/tensor_ifc.py +194 -0
  218. nvmath/internal/tensor_ifc_cupy.py +241 -0
  219. nvmath/internal/tensor_ifc_ndbuffer.py +153 -0
  220. nvmath/internal/tensor_ifc_numpy.py +183 -0
  221. nvmath/internal/tensor_ifc_torch.py +176 -0
  222. nvmath/internal/tensor_wrapper.py +157 -0
  223. nvmath/internal/typemaps.py +162 -0
  224. nvmath/internal/utils.py +771 -0
  225. nvmath/linalg/__init__.py +46 -0
  226. nvmath/linalg/_internal/__init__.py +3 -0
  227. nvmath/linalg/_internal/algo_cap_ifc.py +82 -0
  228. nvmath/linalg/_internal/algo_config_ifc.py +43 -0
  229. nvmath/linalg/_internal/batch.py +217 -0
  230. nvmath/linalg/_internal/enum_to_tuples.py +64 -0
  231. nvmath/linalg/_internal/epilog_protocol.py +766 -0
  232. nvmath/linalg/_internal/layout.py +624 -0
  233. nvmath/linalg/_internal/matmul_desc_ifc.py +28 -0
  234. nvmath/linalg/_internal/matmul_pref_ifc.py +27 -0
  235. nvmath/linalg/_internal/matrix_layout_ifc.py +26 -0
  236. nvmath/linalg/_internal/typemaps.py +144 -0
  237. nvmath/linalg/_internal/utils.py +134 -0
  238. nvmath/linalg/advanced/__init__.py +8 -0
  239. nvmath/linalg/advanced/_algorithmmod.py +149 -0
  240. nvmath/linalg/advanced/_configuration.py +349 -0
  241. nvmath/linalg/advanced/helpers/__init__.py +5 -0
  242. nvmath/linalg/advanced/helpers/matmul.py +1348 -0
  243. nvmath/linalg/advanced/matmulmod.py +3937 -0
  244. nvmath/linalg/generic/__init__.py +43 -0
  245. nvmath/linalg/generic/_configuration/__init__.py +37 -0
  246. nvmath/linalg/generic/_configuration/layout.py +263 -0
  247. nvmath/linalg/generic/_configuration/match.py +609 -0
  248. nvmath/linalg/generic/_configuration/qualifiers.py +494 -0
  249. nvmath/linalg/generic/_configuration/wrap.py +217 -0
  250. nvmath/linalg/generic/_dtype.py +15 -0
  251. nvmath/linalg/generic/matmulmod.py +1060 -0
  252. nvmath/memory.py +282 -0
  253. nvmath/sparse/__init__.py +38 -0
  254. nvmath/sparse/_internal/__init__.py +21 -0
  255. nvmath/sparse/_internal/common_utils.py +144 -0
  256. nvmath/sparse/_internal/cudss_config_ifc.py +741 -0
  257. nvmath/sparse/_internal/cudss_data_ifc.py +399 -0
  258. nvmath/sparse/_internal/cudss_utils.py +445 -0
  259. nvmath/sparse/_internal/cusparse_utils.py +382 -0
  260. nvmath/sparse/_internal/sparse_bsc_ifc.py +300 -0
  261. nvmath/sparse/_internal/sparse_bsr_ifc.py +303 -0
  262. nvmath/sparse/_internal/sparse_coo_ifc.py +254 -0
  263. nvmath/sparse/_internal/sparse_csc_ifc.py +266 -0
  264. nvmath/sparse/_internal/sparse_csr_ifc.py +266 -0
  265. nvmath/sparse/_internal/sparse_dia_ifc.py +239 -0
  266. nvmath/sparse/_internal/sparse_format_helpers.py +601 -0
  267. nvmath/sparse/_internal/sparse_tensor_ifc.py +121 -0
  268. nvmath/sparse/_internal/sparse_ust_ifc.py +146 -0
  269. nvmath/sparse/_internal/utils.py +56 -0
  270. nvmath/sparse/advanced/__init__.py +7 -0
  271. nvmath/sparse/advanced/_configuration.py +230 -0
  272. nvmath/sparse/advanced/direct_solver.py +1884 -0
  273. nvmath/sparse/generic/__init__.py +7 -0
  274. nvmath/sparse/generic/_configuration.py +129 -0
  275. nvmath/sparse/generic/_helpers.py +137 -0
  276. nvmath/sparse/generic/_thunks.py +21 -0
  277. nvmath/sparse/generic/matmulmod.py +2426 -0
  278. nvmath/sparse/ust/__init__.py +7 -0
  279. nvmath/sparse/ust/_converters.py +378 -0
  280. nvmath/sparse/ust/_drawer.py +552 -0
  281. nvmath/sparse/ust/_emitter.py +553 -0
  282. nvmath/sparse/ust/_jit.py +212 -0
  283. nvmath/sparse/ust/_kernel.py +346 -0
  284. nvmath/sparse/ust/_semiring.py +71 -0
  285. nvmath/sparse/ust/_utils.py +102 -0
  286. nvmath/sparse/ust/interfaces/__init__.py +0 -0
  287. nvmath/sparse/ust/interfaces/torch_interface.py +476 -0
  288. nvmath/sparse/ust/semiring_ops.py +71 -0
  289. nvmath/sparse/ust/tensor.py +1027 -0
  290. nvmath/sparse/ust/tensor_drawer.py +552 -0
  291. nvmath/sparse/ust/tensor_emitter.py +558 -0
  292. nvmath/sparse/ust/tensor_format.py +935 -0
  293. nvmath/sparse/ust/tensor_jit.py +212 -0
  294. nvmath/sparse/ust/tensor_kernel.py +348 -0
  295. nvmath/sparse/ust/tensor_utils.py +380 -0
  296. nvmath/tensor/__init__.py +6 -0
  297. nvmath/tensor/_configuration.py +95 -0
  298. nvmath/tensor/_internal/__init__.py +3 -0
  299. nvmath/tensor/_internal/cutensor_config_ifc.py +247 -0
  300. nvmath/tensor/_internal/cutensor_utils.py +162 -0
  301. nvmath/tensor/_internal/data.py +43 -0
  302. nvmath/tensor/_internal/einsum_parser.py +444 -0
  303. nvmath/tensor/_internal/typemaps.py +96 -0
  304. nvmath/tensor/contract.py +1861 -0
  305. nvmath_python-0.9.0.dist-info/METADATA +127 -0
  306. nvmath_python-0.9.0.dist-info/RECORD +309 -0
  307. nvmath_python-0.9.0.dist-info/WHEEL +5 -0
  308. nvmath_python-0.9.0.dist-info/licenses/LICENSE +177 -0
  309. nvmath_python-0.9.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,3937 @@
1
+ # Copyright (c) 2024-2026, 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
+ import dataclasses
9
+ import functools
10
+ import logging
11
+ import operator
12
+ import random
13
+ import typing
14
+ from collections import namedtuple
15
+ from collections.abc import MutableSequence, Sequence
16
+ from dataclasses import dataclass
17
+
18
+ try:
19
+ from cuda.core import Device, EventOptions
20
+ except ImportError:
21
+ from cuda.core.experimental import Device, EventOptions
22
+ import numpy as np
23
+
24
+ from nvmath import memory
25
+ from nvmath._utils import CudaDataType
26
+ from nvmath.bindings import cublas
27
+ from nvmath.bindings import cublasLt as cublaslt # type: ignore
28
+ from nvmath.internal import formatters, tensor_wrapper, typemaps, utils
29
+ from nvmath.linalg._internal import matmul_desc_ifc, matmul_pref_ifc, matrix_layout_ifc
30
+ from nvmath.linalg._internal.epilog_protocol import (
31
+ BATCHED_EPILOG_MINIMUM_VERSIONS_MAP,
32
+ EPILOG_INPUT_HANDLERS_MAP,
33
+ EPILOG_MINIMUM_VERSIONS_MAP,
34
+ EPILOG_OUTPUT_HANDLERS_MAP,
35
+ EpilogOutputHandler,
36
+ )
37
+ from nvmath.linalg._internal.typemaps import (
38
+ COMPUTE_TYPE_TO_DEFAULT_SCALE_TYPE,
39
+ NAMES_TO_DEFAULT_COMPUTE_TYPE,
40
+ NAMES_TO_DEFAULT_SCALE_TYPE,
41
+ SCALE_TYPE_TO_DEFAULT_COMPUTE_TYPE,
42
+ SUPPORTED_TYPES,
43
+ )
44
+ from nvmath.linalg._internal.utils import (
45
+ axis_order_in_memory,
46
+ calculate_strides,
47
+ check_batch_tileable,
48
+ get_handle,
49
+ pointer_aligned_to,
50
+ )
51
+ from nvmath.linalg.advanced import _algorithmmod, _configuration
52
+
53
+ MatmulComputeType = cublas.ComputeType
54
+
55
+ EpilogInputTraits = namedtuple("EpilogInputTraits", ["dtype", "extents", "strides"])
56
+
57
+
58
+ @dataclass
59
+ class MatrixLayout:
60
+ """An internal data class for capturing the tensor layout."""
61
+
62
+ shape: Sequence[int]
63
+ strides: Sequence[int]
64
+ is_conjugate: bool = False # Used to support is_conjugate via conjugate_transpose.
65
+
66
+
67
+ @dataclass
68
+ class LayoutTraits:
69
+ """An internal data class for capturing the matrix multiplication traits."""
70
+
71
+ order: cublaslt.Order
72
+ ld: int
73
+ batch_offset: int # Based on strides
74
+ is_conjugate: bool # Used to support is_conjugate via conjugate_transpose.
75
+ mm_shape: Sequence[int] | None = None
76
+ mm_strides: Sequence[int] | None = None
77
+
78
+ def get_mm_layout(self, transpose=False):
79
+ if self.is_conjugate:
80
+ transpose = True
81
+ if not transpose:
82
+ return *self.mm_shape, self.ld, self.order
83
+
84
+ # Use of transpose is supported only for A and B for two specific use cases till the
85
+ # C library directly supports these use cases:
86
+ #
87
+ # 1. When A or B has the conjugate qualifier, we transpose it internally and then
88
+ # use conjugate transpose in the MM (A @ B.conj() == A @ B.T.H).
89
+ #
90
+ # 2. When the epilog is BGRADB, we transpose B internally and use transpose in the
91
+ # MM since this epilog requires B to be transposed (A @ B == A @ B.T.T).
92
+ #
93
+ # This requires that the layout order be ROW or COL (no special layouts such as
94
+ # structured or hierarchical).
95
+ assert self.mm_shape is not None and self.mm_strides is not None, "Internal Error."
96
+ assert self.ld != 0, "Internal Error."
97
+
98
+ mm_shape = self.mm_shape[1], self.mm_shape[0]
99
+ if self.order == cublaslt.Order.ROW:
100
+ order = cublaslt.Order.COL
101
+ ld = max(self.mm_shape[1], self.mm_strides[0])
102
+ elif self.order == cublaslt.Order.COL:
103
+ order = cublaslt.Order.ROW
104
+ ld = max(self.mm_shape[0], self.mm_strides[1])
105
+ else:
106
+ raise AssertionError("Internal Error. Invalid layout order.")
107
+
108
+ return *mm_shape, ld, order
109
+
110
+
111
+ @dataclass
112
+ class MMTraits:
113
+ """An internal data class for capturing the matrix multiplication traits. The
114
+ result traits are captured separately, because we need to wait for the
115
+ epilog to be provided.
116
+ """
117
+
118
+ M: int
119
+ N: int
120
+ K: int
121
+ d_mm_shape: Sequence[int]
122
+ inplace: bool
123
+ c_layout: MatrixLayout
124
+ a_layout_traits: LayoutTraits
125
+ b_layout_traits: LayoutTraits
126
+ c_layout_traits: LayoutTraits
127
+ batch_count: int
128
+ batch_shape: Sequence[int]
129
+ batch_axis_order: Sequence[int]
130
+
131
+
132
+ @dataclass
133
+ class ResultTraits:
134
+ """An internal data class for capturing the result matrix's traits."""
135
+
136
+ d_layout_traits: LayoutTraits
137
+ result_shape: Sequence[int]
138
+ result_strides: Sequence[int]
139
+
140
+
141
+ def get_matrix_layout_traits(
142
+ mm_shape: Sequence[int],
143
+ mm_strides: Sequence[int],
144
+ batch_strides: Sequence[int],
145
+ col_bcast: bool,
146
+ ordering: cublaslt.Order | None = None,
147
+ orientation: cublaslt.Order | None = None,
148
+ ) -> tuple[cublaslt.Order, int, int]:
149
+ """
150
+ The 'ordering' option specifies the layout order, if it's not None, as in the case of
151
+ the D matrix whose layout is determined by the other operands' layout. It is required if
152
+ the matrix is degenerate (a vector or scalar: len(mm_shape) < 2).
153
+
154
+ The 'orientation' option (ROW or COL) is required to infer the correct leading dimension
155
+ for degenerate matrices (a vector or scalar: len(mm_shape) < 2).
156
+ """
157
+ if len(mm_shape) < 2: # The result D can be a scalar or vector.
158
+ assert ordering is not None, "Internal Error: 'ordering' must be specified for degenerate matrices."
159
+ assert orientation is not None, "Internal Error: 'orientation' must be specified for degenerate matrices."
160
+ batch_offset = min(batch_strides) if batch_strides else 0
161
+ order = ordering
162
+ if len(mm_shape) < 1:
163
+ ld = 1
164
+ elif order != orientation:
165
+ # For a ROW vector in COL order, the LD should be 1 since we promote the row
166
+ # vector to a matrix as (1, M). Similarly, for a COL vector in ROW order, the LD
167
+ # should be 1 as well since we promote the column vector to a matrix as (M, 1).
168
+ ld = 1
169
+ else:
170
+ ld = max(mm_strides[0], mm_shape[0])
171
+ return order, ld, batch_offset
172
+
173
+ M, N = mm_shape
174
+
175
+ if ordering is not None:
176
+ order = ordering
177
+ message = f"Internal Error: incompatible ordering '{ordering}' and strides {mm_strides}"
178
+ if order == cublaslt.Order.ROW:
179
+ assert mm_strides[0] >= mm_strides[1] and mm_strides[1] == 1, message
180
+ else:
181
+ assert mm_strides[1] >= mm_strides[0] and mm_strides[0] == 1, message
182
+ else:
183
+ # Important: start with the first dimension so that cases like (M, 1) : (1, 1) or
184
+ # (1, M) : (1, 1) in CuTe notation map to COL.
185
+ if mm_strides[0] == 1:
186
+ order = cublaslt.Order.COL
187
+ elif mm_strides[1] == 1:
188
+ order = cublaslt.Order.ROW
189
+ else:
190
+ if M == 1:
191
+ order = cublaslt.Order.COL
192
+ elif N == 1:
193
+ order = cublaslt.Order.ROW
194
+ else:
195
+ raise ValueError("Unsupported layout.")
196
+
197
+ # We need to handle broadcast dimensions with zero-stride for the c matrix.
198
+ if col_bcast and N == 1:
199
+ ld = 0
200
+ else:
201
+ ld = max(M, mm_strides[1]) if order == cublaslt.Order.COL else max(N, mm_strides[0])
202
+
203
+ # Batch dimensions should be contiguous in memory, which we have already checked. The
204
+ # batch_offset should be based on the lowest stride in the batch dimension to account
205
+ # for embedded matrices.
206
+ batch_offset = min(batch_strides) if batch_strides else 0
207
+
208
+ return order, ld, batch_offset
209
+
210
+
211
+ def get_mm_traits(a_layout, b_layout, c_layout, inplace, logger):
212
+ """
213
+ First check A and B compatibility:
214
+
215
+ 1. Check MM compatibility (K):
216
+ a. First pad A and/or B MM dimensions if 1-D according to NumPy convention.
217
+ b. The padding is used to determine M, N, and K but should not appear in the output
218
+ dimensions.
219
+ c. If both A and B are N-D, the dimensions must match.
220
+ 2. Check batch dimensions:
221
+ a. One of A or B can have missing batch extents, in which case it is broadcast,
222
+ otherwise
223
+ b. A and B must have the same batch ordering.
224
+ c. In addition, the batch dimensions must be tileable (contiguous in memory).
225
+
226
+ Then check C:
227
+
228
+ C can be None. If C is passed in, it must be a matrix. The batching rule is the
229
+ same as above.
230
+
231
+ For the inplace option (result == C), C cannot be broadcast and so must have batch
232
+ dimensions (if batched).
233
+ """
234
+ inplace_addendum = ""
235
+ if inplace:
236
+ inplace_addendum = "This is required for inplace operations."
237
+
238
+ a_shape, a_strides = list(a_layout.shape), list(a_layout.strides)
239
+ b_shape, b_strides = list(b_layout.shape), list(b_layout.strides)
240
+
241
+ a_batch_shape, a_mm_shape = a_shape[:-2], a_shape[-2:]
242
+ b_batch_shape, b_mm_shape = b_shape[:-2], b_shape[-2:]
243
+
244
+ a_batch_strides, a_mm_strides = a_strides[:-2], a_strides[-2:]
245
+ b_batch_strides, b_mm_strides = b_strides[:-2], b_strides[-2:]
246
+ d_mm_shape = []
247
+ if len(a_mm_shape) == 1:
248
+ s, d = a_mm_shape[0], a_mm_strides[0]
249
+ a_mm_shape = [1] + a_mm_shape
250
+ a_mm_strides = [s * d] + a_mm_strides
251
+ else:
252
+ d_mm_shape.append(a_mm_shape[0]) # The first mode for d applies only when a is not a vector.
253
+
254
+ if len(b_mm_shape) == 1:
255
+ s, d = b_mm_shape[0], b_mm_strides[0]
256
+ b_mm_shape = b_mm_shape + [1]
257
+ b_mm_strides = b_mm_strides + [s * d]
258
+ else:
259
+ d_mm_shape.append(b_mm_shape[1]) # The second mode for d applies only when b is not a vector.
260
+
261
+ logger.debug(f"The MM shape for operand A is {a_mm_shape} with strides {a_mm_strides}.")
262
+ logger.debug(f"The MM shape for operand B is {b_mm_shape} with strides {b_mm_strides}.")
263
+ logger.debug(f"The MM shape for operand D is {d_mm_shape}.")
264
+
265
+ M0, K0 = a_mm_shape
266
+ K1, N0 = b_mm_shape
267
+ if K0 != K1:
268
+ raise ValueError(
269
+ f"The 'K' extent must match for the operands: K={K0} in operand A is not equal to K={K1} in operand B."
270
+ )
271
+
272
+ # Check if batch dimensions of A and B are tileable as well as compatible.
273
+ batch_shape, batch_axis_order = [], ()
274
+ if len(a_batch_shape) > 0:
275
+ if not check_batch_tileable(a_batch_shape, a_batch_strides):
276
+ message = (
277
+ f"The batch layout for A corresponding to shape = {a_batch_shape} and strides = {a_batch_strides} "
278
+ "is currently not supported because it is not tileable."
279
+ )
280
+ raise ValueError(message)
281
+ logger.debug(
282
+ f"The batch layout for A corresponding to shape = {a_batch_shape} and strides = {a_batch_strides} IS tileable."
283
+ )
284
+ batch_shape = a_batch_shape
285
+ batch_axis_order = a_batch_axis_order = axis_order_in_memory(a_batch_strides)
286
+
287
+ if len(b_batch_shape) > 0:
288
+ if not check_batch_tileable(b_batch_shape, b_batch_strides):
289
+ message = (
290
+ f"The batch layout for B corresponding to shape = {b_batch_shape} and strides = {b_batch_strides} "
291
+ "is currently not supported because it is not tileable."
292
+ )
293
+ raise ValueError(message)
294
+ logger.debug(
295
+ f"The batch layout for B corresponding to shape = {b_batch_shape} and strides = {b_batch_strides} IS tileable."
296
+ )
297
+ batch_shape = b_batch_shape
298
+ batch_axis_order = b_batch_axis_order = axis_order_in_memory(b_batch_strides)
299
+
300
+ if len(a_batch_shape) > 0 and len(b_batch_shape) > 0:
301
+ if a_batch_shape != b_batch_shape:
302
+ raise ValueError(f"The batch dimensions of operands A {a_batch_shape} and B {b_batch_shape} must match.")
303
+ if a_batch_axis_order != b_batch_axis_order:
304
+ raise ValueError(f"The batch order of operands A {a_batch_axis_order} and B {b_batch_axis_order} must match.")
305
+
306
+ logger.debug(f"The batch shape is {batch_shape} with batch axis order {batch_axis_order}.")
307
+
308
+ batch_count = functools.reduce(operator.mul, batch_shape, 1)
309
+
310
+ # Create matrix layout traits.
311
+ a_order, a_ld, a_batch_offset = get_matrix_layout_traits(a_mm_shape, a_mm_strides, a_batch_strides, col_bcast=False)
312
+ a_layout_traits = LayoutTraits(
313
+ order=a_order,
314
+ ld=a_ld,
315
+ batch_offset=a_batch_offset,
316
+ is_conjugate=a_layout.is_conjugate,
317
+ mm_shape=a_mm_shape,
318
+ mm_strides=a_mm_strides,
319
+ )
320
+ logger.debug(f"The layout order for operand A is {a_order.name}, with LD {a_ld}, and batch offset {a_batch_offset}.")
321
+
322
+ b_order, b_ld, b_batch_offset = get_matrix_layout_traits(b_mm_shape, b_mm_strides, b_batch_strides, col_bcast=False)
323
+ b_layout_traits = LayoutTraits(
324
+ order=b_order,
325
+ ld=b_ld,
326
+ batch_offset=b_batch_offset,
327
+ is_conjugate=b_layout.is_conjugate,
328
+ mm_shape=b_mm_shape,
329
+ mm_strides=b_mm_strides,
330
+ )
331
+ logger.debug(f"The layout order for operand B is {b_order.name}, with LD {b_ld}, and batch offset {b_batch_offset}.")
332
+
333
+ # Process matrix c, if provided.
334
+ c_layout_traits = None
335
+ if c_layout is not None:
336
+ # 1. C cannot be a vector.
337
+ # 2. C can be a matrix of dimension (M, N) or (M, 1), broadcast in the latter case
338
+ # and has to have contiguous strides.
339
+ # 3. C can be batched matrices of dimension (..., M, N) or (..., M, 1), broadcast in
340
+ # the latter case and has to have contiguous strides.
341
+ c_shape, c_strides = list(c_layout.shape), list(c_layout.strides)
342
+
343
+ c_batch_shape, c_mm_shape = c_shape[:-2], c_shape[-2:]
344
+ c_batch_strides, c_mm_strides = c_strides[:-2], c_strides[-2:]
345
+ if len(c_mm_shape) == 1:
346
+ raise ValueError(f"C cannot be a vector. C shape: {c_mm_shape}")
347
+ logger.debug(f"The MM shape for operand C is {c_mm_shape} with strides {c_mm_strides}.")
348
+
349
+ Mc, Nc = c_mm_shape
350
+ if Mc != M0:
351
+ raise ValueError(f"The M dimension of the C matrix ({Mc}) must match the M dimension of A.")
352
+
353
+ if (inplace and Nc != N0) or (Nc != 1 and Nc != N0):
354
+ raise ValueError(f"The N dimension of the C matrix ({Nc}) must match the N dimension of B. {inplace_addendum}")
355
+
356
+ # For the inplace option, C must be batched if an operand is batched.
357
+ if inplace or len(c_batch_shape) > 0:
358
+ if c_batch_shape != batch_shape:
359
+ raise ValueError(
360
+ f"The batch dimension of operand C {c_batch_shape} must match with that of the other operands "
361
+ f"{batch_shape}. {inplace_addendum}"
362
+ )
363
+
364
+ if batch_shape and (c_batch_axis_order := axis_order_in_memory(c_batch_strides)) != batch_axis_order:
365
+ raise ValueError(
366
+ f"The batch axis order of operand C {c_batch_axis_order} must match with that of the other "
367
+ f"operands {batch_axis_order}."
368
+ )
369
+
370
+ if batch_shape and not check_batch_tileable(c_batch_shape, c_batch_strides):
371
+ message = (
372
+ f"The batch layout for C corresponding to shape = {c_batch_shape} and strides = "
373
+ f"{c_batch_strides} is currently not supported because it is not tileable."
374
+ )
375
+ raise ValueError(message)
376
+
377
+ c_order, c_ld, c_batch_offset = get_matrix_layout_traits(
378
+ c_mm_shape, c_mm_strides, c_batch_strides, col_bcast=not inplace
379
+ )
380
+ c_layout_traits = LayoutTraits(order=c_order, ld=c_ld, batch_offset=c_batch_offset, is_conjugate=c_layout.is_conjugate)
381
+ logger.debug(f"The layout order for operand C is {c_order.name}, with LD {c_ld}, and batch offset {c_batch_offset}.")
382
+
383
+ return MMTraits(
384
+ M=M0,
385
+ N=N0,
386
+ K=K0,
387
+ d_mm_shape=d_mm_shape,
388
+ inplace=inplace,
389
+ c_layout=c_layout,
390
+ batch_count=batch_count,
391
+ batch_shape=batch_shape,
392
+ batch_axis_order=batch_axis_order,
393
+ a_layout_traits=a_layout_traits,
394
+ b_layout_traits=b_layout_traits,
395
+ c_layout_traits=c_layout_traits,
396
+ )
397
+
398
+
399
+ def get_result_traits(mm_traits: MMTraits, epilog_ordering: cublaslt.Order | None, logger: logging.Logger) -> ResultTraits:
400
+ """
401
+ epilog_ordering = value of type cublaslt.Order or None.
402
+
403
+ The result layout is determined from:
404
+ - the ordering of operand c, if it is provided, or
405
+ - the epilog requirement, if it exists, or
406
+ - the ordering of operand a.
407
+
408
+ The result batch dimensions must have the same extents and axis order as the inputs. The
409
+ MM layout can be C or F.
410
+ """
411
+
412
+ # For inplace=True, the result traits are essentially the same as for operand C except
413
+ # for conjugation.
414
+ if mm_traits.inplace:
415
+ result_shape, result_strides = mm_traits.c_layout.shape, mm_traits.c_layout.strides
416
+ d_layout_traits = dataclasses.replace(mm_traits.c_layout_traits, is_conjugate=False)
417
+ return ResultTraits(result_shape=result_shape, result_strides=result_strides, d_layout_traits=d_layout_traits)
418
+
419
+ # The result shape is the batch shape + d_mm_shape.
420
+ result_shape = (*mm_traits.batch_shape, *mm_traits.d_mm_shape)
421
+
422
+ if mm_traits.c_layout_traits is not None:
423
+ result_ordering = mm_traits.c_layout_traits.order
424
+ elif epilog_ordering is not None:
425
+ result_ordering = epilog_ordering
426
+ else:
427
+ result_ordering = mm_traits.a_layout_traits.order
428
+
429
+ if result_ordering == cublaslt.Order.ROW:
430
+ d_order = list(range(len(mm_traits.d_mm_shape) - 1, -1, -1))
431
+ elif result_ordering == cublaslt.Order.COL:
432
+ d_order = list(range(len(mm_traits.d_mm_shape)))
433
+ else:
434
+ raise AssertionError("Internal Error.")
435
+
436
+ result_axis_order = [len(mm_traits.batch_axis_order) + a for a in d_order] + list(mm_traits.batch_axis_order)
437
+
438
+ # Calculate the result strides.
439
+ result_strides = calculate_strides(result_shape, result_axis_order)
440
+
441
+ # For degenerate matrices, we need to specify the result orientation.
442
+ result_orientation = None
443
+ if len(mm_traits.d_mm_shape) < 2:
444
+ if mm_traits.M == 1:
445
+ result_orientation = cublaslt.Order.ROW
446
+ elif mm_traits.N == 1:
447
+ result_orientation = cublaslt.Order.COL
448
+
449
+ # The result's traits.
450
+ d_batch_strides, d_mm_strides = (
451
+ result_strides[: len(mm_traits.batch_shape)],
452
+ result_strides[len(mm_traits.batch_shape) :],
453
+ )
454
+ order, d_ld, d_batch_offset = get_matrix_layout_traits(
455
+ mm_traits.d_mm_shape,
456
+ d_mm_strides,
457
+ d_batch_strides,
458
+ col_bcast=False,
459
+ ordering=result_ordering,
460
+ orientation=result_orientation,
461
+ )
462
+ assert order == result_ordering, (
463
+ f"Internal Error: d_order = {order.name}, result_ordering = {result_ordering.name}, mm_traits = {mm_traits}."
464
+ )
465
+ d_layout_traits = LayoutTraits(order=order, ld=d_ld, batch_offset=d_batch_offset, is_conjugate=False)
466
+ logger.debug(f"The layout order for operand D is {order.name}, with LD {d_ld}, and batch offset {d_batch_offset}.")
467
+
468
+ return ResultTraits(result_shape=result_shape, result_strides=result_strides, d_layout_traits=d_layout_traits)
469
+
470
+
471
+ SHARED_MM_DOCUMENTATION = utils.COMMON_SHARED_DOC_MAP.copy()
472
+ SHARED_MM_DOCUMENTATION.update(
473
+ {
474
+ "a": """\
475
+ A tensor representing the first operand to the matrix multiplication (see `Semantics`_). The currently supported types
476
+ are :class:`numpy.ndarray`, :class:`cupy.ndarray`, and :class:`torch.Tensor`.""".replace("\n", " "),
477
+ #
478
+ "b": """\
479
+ A tensor representing the second operand to the matrix multiplication (see `Semantics`_). The currently supported types
480
+ are :class:`numpy.ndarray`, :class:`cupy.ndarray`, and :class:`torch.Tensor`.""".replace("\n", " "),
481
+ #
482
+ "c": """\
483
+ (Optional) A tensor representing the operand to add to the matrix multiplication result (see `Semantics`_). The currently
484
+ supported types are :class:`numpy.ndarray`, :class:`cupy.ndarray`, and :class:`torch.Tensor`.""".replace("\n", " "),
485
+ #
486
+ "c_admonitions": """
487
+ .. versionchanged:: 0.3.0
488
+ In order to avoid broadcasting behavior ambiguity, nvmath-python no longer
489
+ accepts a 1-D (vector) `c`. Use a singleton dimension to convert your input
490
+ array to 2-D.
491
+ """,
492
+ #
493
+ "alpha": """\
494
+ The scale factor for the matrix multiplication term as a real or complex number. The default is
495
+ :math:`1.0`.""".replace("\n", " "),
496
+ #
497
+ "beta": """\
498
+ The scale factor for the matrix addition term as a real or complex number. A value for `beta` must be provided if
499
+ operand `c` is specified.""".replace("\n", " "),
500
+ #
501
+ "quantization_scales": """\
502
+ Specify scale factors for the matrix multiplication as a :class:`~nvmath.linalg.advanced.MatmulQuantizationScales`
503
+ object. Alternatively, a `dict` containing the parameters for the
504
+ :class:`~nvmath.linalg.advanced.MatmulQuantizationScales`
505
+ constructor can also be provided. The scale factors can be provided as scalars or tensors.
506
+ If a scale factor is provided as a tensor, it must be from the same package and on the same
507
+ memory space (CPU or GPU device) as the operands of the matmul.
508
+ If a scale factor is provided as a scalar, and the execution space is GPU,
509
+ a CPU->GPU copy is inevitable. To avoid this copy, provide
510
+ the quantization scale as one-element array on the GPU.
511
+ Allowed and required only for narrow-precision (FP8 and lower) operations.""".replace("\n", " "),
512
+ #
513
+ "algorithms": """\
514
+ A sequence of :class:`Algorithm` objects that can be directly provided to bypass planning. The algorithm objects must be
515
+ compatible with the matrix multiplication. A typical use for this option is to provide algorithms serialized (pickled)
516
+ from a previously planned and autotuned matrix multiplication.""".replace("\n", " "),
517
+ #
518
+ "epilog": """\
519
+ Specify an epilog :math:`F` as an object of type :class:`MatmulEpilog` to apply to the result of the matrix
520
+ multiplication: :math:`F(\\alpha A @ B + \\beta C`). The default is no epilog. See `cuBLASLt documentation
521
+ <https://docs.nvidia.com/cuda/cublas/#cublasltepilogue-t>`_ for the list of available epilogs.""".replace("\n", " "),
522
+ #
523
+ "epilog_inputs": """\
524
+ Specify the additional inputs needed for the selected epilog as a dictionary, where the key is the epilog input name and
525
+ the value is the epilog input. The epilog input must be a tensor with the same package and in the same memory space as
526
+ the operands (see the constructor for more information on the operands). If the required epilog inputs are not provided,
527
+ an exception is raised that lists the required epilog inputs. Some epilog inputs are generated by other epilogs. For
528
+ example, the epilog input for :class:`MatmulEpilog.DRELU` is generated by matrix multiplication with the same operands
529
+ using :class:`MatmulEpilog.RELU_AUX`. """.replace("\n", " "),
530
+ #
531
+ "qualifiers": """\
532
+ If desired, specify the matrix qualifiers as a :class:`numpy.ndarray` of
533
+ :class:`~nvmath.linalg.advanced.matrix_qualifiers_dtype` objects of length 3 corresponding to the operands `a`, `b`, and
534
+ `c`. See :ref:`matrix-tensor-qualifiers` for the motivation behind
535
+ qualifiers.""".replace("\n", " "),
536
+ #
537
+ "options": """\
538
+ Specify options for the matrix multiplication as a :class:`~nvmath.linalg.advanced.MatmulOptions` object. Alternatively,
539
+ a `dict` containing the parameters for the ``MatmulOptions`` constructor can also be provided. If not specified, the
540
+ value will be set to the default-constructed ``MatmulOptions`` object.""".replace("\n", " "),
541
+ #
542
+ "preferences": """\
543
+ This parameter specifies the preferences for planning as a :class:`MatmulPlanPreferences` object. Alternatively, a
544
+ dictionary containing the parameters for the :class:`MatmulPlanPreferences` constructor can also be provided. If not
545
+ specified, the value will be set to the default-constructed :class:`MatmulPlanPreferences` object.
546
+ """.replace("\n", " "),
547
+ #
548
+ "result": """\
549
+ The result of the specified matrix multiplication (epilog applied), which remains on the same device and belongs to the
550
+ same package as the input operands. If an epilog (like :attr:`nvmath.linalg.advanced.MatmulEpilog.RELU_AUX`) that
551
+ results in extra output is used, or an extra output is requested (for example by setting
552
+ :attr:`~nvmath.linalg.advanced.MatmulOptions.result_amax` option in ``options`` argument),
553
+ a tuple is returned with the first element being the matrix multiplication result (epilog applied) and the second element
554
+ being the auxiliary output provided as a `dict`. """.replace("\n", " "),
555
+ #
556
+ "narrow_precision": """\
557
+
558
+ .. _narrow_precision:
559
+
560
+ Matrix multiplication with narrow-precision operands is supported, in FP8, MXFP8, and NVFP4 formats.
561
+
562
+ **FP8 and MXFP8**
563
+
564
+ FP8 and MXFP8 use ``float8_e4m3fn`` or ``float8_e5m2`` data types. The difference is the scaling mode:
565
+ FP8 (``block_scaling=False``) uses per-tensor scaling where a single scalar scale is applied to each operand;
566
+ MXFP8 (``block_scaling=True``) uses microscaling with 32-element blocks arranged in 128x128 tiles.
567
+
568
+ .. note::
569
+
570
+ FP8 and MXFP8 matrix multiplication requires **CUDA Toolkit 12.8 or newer**.
571
+ **FP8 requires a device with compute capability 8.9 or higher** (Ada, Hopper, Blackwell or newer architecture).
572
+ **MXFP8 requires a device with compute capability 10.0 or higher** (Blackwell or newer architecture).
573
+ Please refer to the `compute capability table <https://developer.nvidia.com/cuda-gpus>`_
574
+ to check the compute capability of your device.
575
+
576
+ For FP8 operations:
577
+
578
+ * For each operand a scaling factor needs to be specified via ``quantization_scales`` argument.
579
+ * Maximum absolute value of the result (amax) can be requested via
580
+ :attr:`~nvmath.linalg.advanced.MatmulOptions.result_amax` option in ``options`` argument.
581
+ * Custom result type (both FP8 and non-FP8) can be requested via
582
+ :attr:`~nvmath.linalg.advanced.MatmulOptions.result_type` option in ``options`` argument.
583
+
584
+ For MXFP8 operations:
585
+
586
+ * 1-D (vector) operands are not supported. Both ``a`` and ``b`` must be at least 2-D matrices.
587
+ * Broadcasting of batch dimensions is not supported. The batch shapes of ``a`` and ``b`` must match exactly.
588
+ * All operand dimensions (M, N, K) must be multiples of 128.
589
+ * :attr:`~nvmath.linalg.advanced.MatmulOptions.block_scaling` option must be set to ``True`` and
590
+ block scaling factors need to be specified via ``quantization_scales`` argument.
591
+ Utilities in :mod:`nvmath.linalg.advanced.helpers.matmul` can be used to create and modify
592
+ block scaling factors, see e.g. :func:`~nvmath.linalg.advanced.helpers.matmul.create_mxfp8_scale`.
593
+ * When the result type is a narrow-precision data type, the auxiliary output ``"d_out_scale"``
594
+ will be returned containing the scales used for result quantization.
595
+
596
+ *Layout Requirements*
597
+
598
+ Due to the requirements of narrow-precision GEMM kernels, the contracting dimension
599
+ K must be contiguous (stride-1) for both operands. The following layout constraints
600
+ apply to both FP8 and MXFP8:
601
+
602
+ * Operand ``a`` must be ``(..., M, K)`` with ``stride[-1] == 1`` and
603
+ ``stride[-2] >= K`` (row-major). The leading dimension (``stride[-2]``) can be
604
+ larger than ``K`` to support sliced or padded views.
605
+
606
+ * Operand ``b`` must be ``(..., K, N)`` with ``stride[-2] == 1`` and
607
+ ``stride[-1] >= K`` (column-major). The leading dimension (``stride[-1]``) can
608
+ be larger than ``K`` to support sliced or padded views.
609
+
610
+ .. attention::
611
+
612
+ Epilog support for MXFP8 is still evolving in the underlying cuBLASLt library,
613
+ so not every combination of epilog, data type, and layout is guaranteed to work.
614
+ If running into unsupported combinations, a cuBLASLt error will be raised
615
+ either at planning time or at execution time that will reveal the
616
+ root cause. These gaps are expected to be filled in future cuBLASLt releases.
617
+
618
+ For more details on the FP8 and MXFP8 formats in cuBLAS,
619
+ see the `cublasLtMatmul documentation <https://docs.nvidia.com/cuda/cublas/#cublasltmatmul>`_.
620
+
621
+ **NVFP4**
622
+
623
+ .. versionadded:: 1.0
624
+ NVFP4 support.
625
+
626
+ NVFP4 uses ``float4_e2m1fn_x2`` data type with block scaling (16-element blocks arranged in 128x64 tiles).
627
+
628
+ .. note::
629
+
630
+ NVFP4 matrix multiplication currently requires **CUDA Toolkit 12.8 or newer**,
631
+ **a device with compute capability 10.0 or higher** (Blackwell or newer architecture),
632
+ and **PyTorch 2.9 or newer** for ``float4_e2m1fn_x2`` dtype support.
633
+ Please refer to the `compute capability table <https://developer.nvidia.com/cuda-gpus>`_
634
+ to check the compute capability of your device.
635
+
636
+ For NVFP4 operations:
637
+
638
+ * 1-D (vector) operands are not supported. Both ``a`` and ``b`` must be at least 2-D matrices.
639
+ * Broadcasting of batch dimensions is not supported. The batch shapes of ``a`` and ``b`` must match exactly.
640
+ * The outer dimensions of ``a`` and ``b`` (M and N) must be multiples of 128,
641
+ and the contracting dimension K must be a multiple of 64.
642
+ * :attr:`~nvmath.linalg.advanced.MatmulOptions.block_scaling` option must be set to ``True`` and
643
+ block scaling factors need to be specified via ``quantization_scales`` argument.
644
+ * When the result type is a narrow-precision data type, the auxiliary output ``"d_out_scale"``
645
+ will be returned containing the scales used for result quantization.
646
+
647
+ *Layout and Packing Requirements*
648
+
649
+ FP4 data is per-byte packed: ``float4_e2m1fn_x2`` stores 2 FP4 values per byte.
650
+ The block scaling (VEC16_UE4M3) assigns one scale factor per 16 consecutive elements
651
+ along the innermost (stride-1) dimension of each operand. The layout requirements
652
+ below ensure that this innermost dimension corresponds to the contracting dimension K
653
+ for both operands.
654
+
655
+ * Operand ``a`` must be ``(..., M, K//2)`` with ``stride[-1] == 1`` and
656
+ ``stride[-2] >= K//2``, i.e., row-wise packed along K. Note that the
657
+ leading dimension (``stride[-2]``) can be larger than ``K//2`` to support
658
+ sliced views, as long as the stride remains 16-byte aligned.
659
+
660
+ * Operand ``b`` must be ``(..., K//2, N)`` with ``stride[-2] == 1`` and
661
+ ``stride[-1] >= K//2``, i.e., column-wise packed along K. Note that the
662
+ leading dimension (``stride[-1]``) can be larger than ``K//2`` to support
663
+ sliced views, as long as the stride remains 16-byte aligned.
664
+
665
+ If your data has the stride-1 axis along a dimension other than K,
666
+ you must repack it before calling :func:`matmul`.
667
+
668
+ When the result type is also FP4, the output is packed along a dimension that
669
+ depends on the result layout order:
670
+
671
+ * **Row-major result**: packed along N — shape ``(..., M, N//2)``, strides ``(..., N//2, 1)``.
672
+ * **Column-major result**: packed along M — shape ``(..., M//2, N)``, strides ``(..., 1, M//2)``.
673
+
674
+ The result layout order is determined by the following priority:
675
+
676
+ 1. If ``c`` is provided, the result inherits ``c``'s layout order.
677
+ 2. Otherwise, if the epilog requests a specific layout, that layout is used.
678
+ 3. Otherwise, the result inherits ``a``'s layout order as a fallback.
679
+
680
+ *Epilog Support*
681
+
682
+ NVFP4 matmul supports epilogs. The following have been verified:
683
+
684
+ * ``RELU``, ``GELU`` -- with both row-major and column-major output.
685
+ * ``BIAS``, ``RELU_BIAS``, ``GELU_BIAS`` -- with column-major output only
686
+ (``BIAS`` with ``float16`` C/D requires cuBLASLt >= 13.0).
687
+
688
+ .. attention::
689
+
690
+ Epilog support for NVFP4 is still evolving in the underlying cuBLASLt library,
691
+ so not every combination of epilog, data type, and layout is guaranteed to work.
692
+ If running into unsupported combinations, a cuBLASLt error will be raised
693
+ either at planning time or at execution time that will reveal the
694
+ root cause. These gaps are expected to be filled in future cuBLASLt releases.
695
+
696
+ *Helper Functions*
697
+
698
+ The :mod:`nvmath.linalg.advanced.helpers.matmul` module provides helpers for working with
699
+ FP4 encoding/decoding and NVFP4 block scales, see e.g.
700
+ :func:`~nvmath.linalg.advanced.helpers.matmul.quantize_to_fp4`,
701
+ :func:`~nvmath.linalg.advanced.helpers.matmul.unpack_fp4`,
702
+ :func:`~nvmath.linalg.advanced.helpers.matmul.get_block_scale_offset`,
703
+ :func:`~nvmath.linalg.advanced.helpers.matmul.to_block_scale`,
704
+ :func:`~nvmath.linalg.advanced.helpers.matmul.expand_block_scale`.
705
+
706
+ For more details on the NVFP4 format in cuBLAS,
707
+ see the `cublasLtMatmul documentation <https://docs.nvidia.com/cuda/cublas/#cublasltmatmul>`_.
708
+ For usage examples, see the relevant files in the
709
+ `examples/linalg/advanced/matmul
710
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/linalg/advanced/matmul>`_
711
+ directory.
712
+ """.strip(),
713
+ #
714
+ "semantics": """\
715
+ .. _semantics:
716
+
717
+ The semantics of the matrix multiplication follows :external:py:data:`numpy.matmul` semantics, with some restrictions on
718
+ broadcasting. In addition, the semantics for the fused matrix addition are described below.
719
+
720
+ .. note::
721
+
722
+ For narrow-precision formats (FP8, MXFP8, NVFP4), some of the rules below are
723
+ restricted — see the :ref:`narrow-precision <narrow_precision>` section for details.
724
+
725
+ * For in-place matrix multiplication (where the result is written into `c`) the result has the same shape as `c`.
726
+ * If arguments `a` and `b` are matrices, they are multiplied according to the rules of matrix multiplication.
727
+ * If argument `a` is 1-D, it is promoted to a matrix by prefixing ``1`` to its dimensions. After matrix
728
+ multiplication, the prefixed ``1`` is removed from the result's dimensions if the operation is not in-place.
729
+ * If argument `b` is 1-D, it is promoted to a matrix by appending ``1`` to its dimensions. After matrix
730
+ multiplication, the appended ``1`` is removed from the result's dimensions if the operation is not in-place.
731
+ * 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,
732
+ their batch dimensions must match. If exactly one of `a` or `b` is N-D, the other operand is broadcast.
733
+ * The operand for the matrix addition `c` may be a matrix of shape (M, 1) or (M, N), or the batched versions
734
+ (..., M, 1) or (..., M, N). Here M and N are the dimensions of the result of the matrix multiplication. If N = 1, the
735
+ columns of `c` are broadcast for the addition; the rows of `c` are never broadcast. If batch dimensions are not
736
+ present, `c` is broadcast across batches as needed. If the operation is in-place, `c` cannot be broadcast since
737
+ it must be large enough to hold the result.
738
+ * Similarly, when operating on a batch, auxiliary outputs are 3-D for all epilogs. Therefore, epilogs that return 1-D
739
+ vectors of length N in non-batched mode return 3-D matrices of size (batch, N, 1) in batched mode.
740
+
741
+ For narrow-precision operations (FP8 and lower), further restrictions apply;
742
+ see the narrow-precision support section below.
743
+ """.strip(),
744
+ }
745
+ )
746
+
747
+
748
+ class InvalidMatmulState(Exception):
749
+ pass
750
+
751
+
752
+ def _check_extents(shape: tuple, name: str):
753
+ if any(e <= 0 for e in shape):
754
+ message = f"The specified extents {shape} for operand {name} are not valid. The extents must be strictly positive. "
755
+ raise ValueError(message)
756
+
757
+
758
+ def _detect_and_validate_ab_operands_for_fp4(a, b, logger) -> bool:
759
+ """
760
+ Detect whether A and B are FP4 operands, and if so validate their format.
761
+
762
+ Returns True if both operands are FP4 (float4_e2m1fn_x2), False if neither
763
+ is FP4, and raises ValueError if exactly one is FP4 (mixed operands are not
764
+ supported).
765
+
766
+ When both operands are FP4, the function validates layout and dimensions.
767
+ FP4 data is always packed: float4_e2m1fn_x2 stores 2 FP4 values per byte.
768
+ A and B must be in the following format:
769
+
770
+ - A must be (M, K//2) with stride[-1]==1 and stride[-2] >= K//2, i.e. row-wise
771
+ packed along K. The leading dimension can be larger than K//2, namely
772
+ padded/sliced views are supported.
773
+ - B must be (K//2, N) with stride[-2]==1 and stride[-1] >= K//2, i.e. column-wise
774
+ packed along K. The leading dimension can be larger than K//2, namely
775
+ padded/sliced views are supported.
776
+
777
+ Why packing must be along K:
778
+
779
+ cuBLASLt hardcodes transa=OP_T for MXP4, and expects both A and B to be provided
780
+ in column-major layout. In column-major,
781
+ the stride-1 (innermost) dimension corresponds to the rows of the stored matrix.
782
+ Combined with OP_T, these rows map to the contracting dimension K from the user's
783
+ logical perspective. This means:
784
+
785
+ - User's row-major A (M, K//2) with stride[-1]==1: cuBLASLt interprets this
786
+ as a column-major (K//2, M) matrix (K//2 rows along the stride-1 axis,
787
+ M columns). With transa=OP_T, the logical GEMM operand becomes (M, K),
788
+ with the physical stride-1 axis aligned to the contraction dimension K.
789
+ - User's column-major B (K//2, N) with stride[-2]==1: cuBLASLt interprets
790
+ this as a column-major (K//2, N) matrix directly. With transb=OP_N
791
+ (the default), the logical GEMM operand is (K, N), again with the
792
+ physical stride-1 axis aligned to K.
793
+
794
+ NVFP4 block scaling (VEC16_UE4M3) assigns one scale per 16 elements along the
795
+ innermost (stride-1) dimension. Because of the layout above, this innermost
796
+ dimension is K, so the scales are naturally aligned with the contraction axis.
797
+
798
+ Dimension requirements:
799
+ - M (outer dim of A): must be a multiple of 128
800
+ - N (outer dim of B): must be a multiple of 128
801
+ - K (contraction dim): must be a multiple of 64
802
+ """
803
+ a_is_fp4 = a.dtype == "float4_e2m1fn_x2"
804
+ b_is_fp4 = b.dtype == "float4_e2m1fn_x2"
805
+
806
+ # Neither is FP4 means this is not an FP4 operation
807
+ if not a_is_fp4 and not b_is_fp4:
808
+ return False
809
+
810
+ # One is FP4 and the other is not: unsupported
811
+ if a_is_fp4 != b_is_fp4:
812
+ raise ValueError(
813
+ f"Mixed FP4/non-FP4 A/B operands are not supported. "
814
+ f"Got A dtype={a.dtype}, B dtype={b.dtype}. "
815
+ f"Both A and B operands must be float4_e2m1fn_x2 or neither."
816
+ )
817
+
818
+ logger.info(f"FP4 validation: A shape={a.shape}, strides={a.strides}")
819
+ logger.info(f"FP4 validation: B shape={b.shape}, strides={b.strides}")
820
+
821
+ if len(a.shape) < 2:
822
+ raise ValueError(f"FP4 operand A must be at least 2-D, got shape {a.shape}.")
823
+ if len(b.shape) < 2:
824
+ raise ValueError(f"FP4 operand B must be at least 2-D, got shape {b.shape}.")
825
+
826
+ # Validate A: Must be row-major (..., M, K//2) with strides (..., K//2, 1)
827
+ if a.strides[-1] != 1:
828
+ raise ValueError(f"FP4 operand A must be row-major with stride[-1]=1. Got shape {a.shape} with strides {a.strides}.")
829
+
830
+ # Validate B: Must be column-major (..., K//2, N) with strides (..., 1, K//2)
831
+ if b.strides[-2] != 1:
832
+ raise ValueError(
833
+ f"FP4 operand B must be column-major with stride[-2]=1. "
834
+ f"Got shape {b.shape} with strides {b.strides}. "
835
+ f"Use torch.as_strided() to create column-major view."
836
+ )
837
+
838
+ # Extract dimensions from packed tensors:
839
+ # A is (M, K//2) -> M = a.shape[-2], K = a.shape[-1] * 2
840
+ # B is (K//2, N) -> N = b.shape[-1], K = b.shape[-2] * 2
841
+ M = a.shape[-2]
842
+ K_from_a = a.shape[-1] * 2 # Unpack: K//2 -> K
843
+ K_from_b = b.shape[-2] * 2 # Unpack: K//2 -> K
844
+ N = b.shape[-1]
845
+
846
+ # K should match between A and B
847
+ if K_from_a != K_from_b:
848
+ raise ValueError(
849
+ f"Dimension mismatch: K from A ({K_from_a}) != K from B ({K_from_b}). "
850
+ f"A shape is {a.shape} (M, K//2), B shape is {b.shape} (K//2, N)."
851
+ )
852
+ K = K_from_a
853
+
854
+ logger.info(f"FP4 dimensions: M={M}, N={N}, K={K}")
855
+
856
+ # Validate block scaling dimension requirements
857
+ errors = []
858
+ if M % 128 != 0:
859
+ errors.append(f"M={M} must be divisible by 128")
860
+ if N % 128 != 0:
861
+ errors.append(f"N={N} must be divisible by 128")
862
+ if K % 64 != 0:
863
+ errors.append(f"K={K} must be divisible by 64")
864
+
865
+ if errors:
866
+ raise ValueError(
867
+ f"FP4 block scaling dimension requirements not met: {', '.join(errors)}. "
868
+ f"NVFP4 requires M and N divisible by 128, K divisible by 64."
869
+ )
870
+
871
+ return True
872
+
873
+
874
+ def _create_fp4_ab_layouts(a_shape, a_strides, a_is_conjugate, b_shape, b_strides, b_is_conjugate):
875
+ """
876
+ Create MatrixLayout objects for packed FP4 operands A and B.
877
+
878
+ cuBLASLt always computes A^T @ B (transa=OP_T is hardcoded). The user provides
879
+ A as (M, K//2) row-wise packed and B as (K//2, N) column-wise packed. We convert
880
+ these to logical shapes/strides that cuBLASLt expects:
881
+
882
+ - A is reinterpreted as (K//2, M) internally (swap last two dims), so that
883
+ cuBLASLt computes (A^T)^T @ B = A @ B.
884
+ - B is kept as is, but strides are converted from packed to logical.
885
+
886
+ Since float4_e2m1fn_x2 packs 2 values per byte, logical strides are 2x the
887
+ packed strides along the K dimension.
888
+
889
+ Args:
890
+ a_shape: Physical shape of A, e.g. (..., M, K//2).
891
+ a_strides: Physical strides of A.
892
+ a_is_conjugate: Whether A is conjugated.
893
+ b_shape: Physical shape of B, e.g. (..., K//2, N).
894
+ b_strides: Physical strides of B.
895
+ b_is_conjugate: Whether B is conjugated.
896
+
897
+ Returns:
898
+ (a_layout, b_layout) — MatrixLayout objects with logical shapes/strides.
899
+ """
900
+ M, K_packed = a_shape[-2], a_shape[-1]
901
+ K_logical = K_packed * 2
902
+
903
+ # A: swap last two stride dimensions (transpose for cuBLASLt), then convert to logical
904
+ a_transposed_strides = (*a_strides[:-2], a_strides[-1], a_strides[-2])
905
+ a_logical_shape = (*a_shape[:-2], M, K_logical)
906
+ a_batch_strides = tuple(s * 2 for s in a_transposed_strides[:-2])
907
+ a_logical_strides = (*a_batch_strides, a_transposed_strides[-1] * 2, a_transposed_strides[-2])
908
+
909
+ # B: keep dimension order, convert packed strides to logical
910
+ N = b_shape[-1]
911
+ b_logical_shape = (*b_shape[:-2], K_logical, N)
912
+ b_batch_strides = tuple(s * 2 for s in b_strides[:-2])
913
+ b_logical_strides = (*b_batch_strides, b_strides[-2], b_strides[-1] * 2)
914
+
915
+ a_layout = MatrixLayout(a_logical_shape, a_logical_strides, a_is_conjugate)
916
+ b_layout = MatrixLayout(b_logical_shape, b_logical_strides, b_is_conjugate)
917
+ return a_layout, b_layout
918
+
919
+
920
+ def _create_d_out_scale_and_scale_mode(result_class, num_output_elements, using_fp4_ab, device_id, stream_holder):
921
+ """
922
+ Create the d_out_scale tensor and determine the scale mode for block-scaled output.
923
+ Block-scaled narrow-precision output (FP4 or FP8) requires a scale tensor where each
924
+ scale covers a group of output elements:
925
+
926
+ - FP4 (VEC16_UE4M3): each scale covers 16 elements, stored as ``float8_e4m3fn``
927
+ - FP8 (VEC32_UE8M0): each scale covers 32 elements, stored as ``uint8``.
928
+
929
+ Args:
930
+ result_class: The tensor class used to create the output tensor.
931
+ num_output_elements: Total number of output elements.
932
+ using_fp4_ab: Whether FP4 operands are being used.
933
+ device_id: The CUDA device id.
934
+ stream_holder: The stream holder for tensor allocation.
935
+
936
+ Returns:
937
+ (d_out_scale_tensor, d_out_scale_mode) — the allocated scale tensor and the
938
+ corresponding ``cublaslt.MatmulMatrixScale`` mode.
939
+ """
940
+ if using_fp4_ab:
941
+ scale_mode = cublaslt.MatmulMatrixScale.VEC16_UE4M3
942
+ scale_group_size = 16
943
+ scale_dtype = "float8_e4m3fn" # UE4M3 format, consistent with input A/B scales
944
+ else:
945
+ scale_mode = cublaslt.MatmulMatrixScale.VEC32_UE8M0
946
+ scale_group_size = 32
947
+ scale_dtype = "uint8" # UE8M0 format
948
+
949
+ num_scales = num_output_elements // scale_group_size
950
+ d_out_scale = utils.create_empty_tensor(
951
+ result_class,
952
+ (num_scales,),
953
+ scale_dtype,
954
+ device_id,
955
+ stream_holder,
956
+ verify_strides=False,
957
+ )
958
+ return d_out_scale, scale_mode
959
+
960
+
961
+ @utils.docstring_decorator(SHARED_MM_DOCUMENTATION, skip_missing=False)
962
+ class Matmul:
963
+ """
964
+ Create a stateful object encapsulating the specified matrix multiplication computation
965
+ :math:`\\alpha a @ b + \\beta c` and the required resources to perform the operation. A
966
+ stateful object can be used to amortize the cost of preparation (planning in the case of
967
+ matrix multiplication) across multiple executions (also see the :ref:`Stateful APIs
968
+ <host api types>` section).
969
+
970
+ The function-form API :func:`matmul` is a convenient alternative to using stateful
971
+ objects for *single* use (the user needs to perform just one matrix multiplication, for
972
+ example), in which case there is no possibility of amortizing preparatory costs. The
973
+ function-form APIs are just convenience wrappers around the stateful object APIs.
974
+
975
+ Using the stateful object typically involves the following steps:
976
+
977
+ 1. **Problem Specification**: Initialize the object with a defined operation and
978
+ options.
979
+ 2. **Preparation**: Use :meth:`plan` to determine the best algorithmic implementation
980
+ for this specific matrix multiplication operation.
981
+ 3. **Execution**: Perform the matrix multiplication computation with :meth:`execute`.
982
+ 4. **Resource Management**: Ensure all resources are released either by explicitly
983
+ calling :meth:`free` or by managing the stateful object within a context manager.
984
+
985
+ Detailed information on what's happening in the various phases described above can be
986
+ obtained by passing in a :class:`logging.Logger` object to :class:`MatmulOptions` or by
987
+ setting the appropriate options in the root logger object, which is used by default:
988
+
989
+ >>> import logging
990
+ >>> logging.basicConfig(
991
+ ... level=logging.INFO,
992
+ ... format="%(asctime)s %(levelname)-8s %(message)s",
993
+ ... datefmt="%m-%d %H:%M:%S",
994
+ ... )
995
+
996
+ A user can select the desired logging level and, in general, take advantage of all of
997
+ the functionality offered by the Python `logging` module.
998
+
999
+ Args:
1000
+ a: {a}
1001
+
1002
+ b: {b}
1003
+
1004
+ c: {c}
1005
+ {c_admonitions}
1006
+
1007
+ alpha: {alpha}
1008
+
1009
+ beta: {beta}
1010
+
1011
+ qualifiers: {qualifiers}
1012
+
1013
+ options: {options}
1014
+
1015
+ stream: {stream}
1016
+
1017
+ quantization_scales: {quantization_scales}
1018
+
1019
+ Semantics:
1020
+ {semantics}
1021
+
1022
+ Narrow-precision support:
1023
+ {narrow_precision}
1024
+
1025
+ .. seealso::
1026
+ :meth:`autotune`, :meth:`plan`, :meth:`reset_operands`,
1027
+ :meth:`reset_operands_unchecked`, :meth:`execute`
1028
+
1029
+ Examples:
1030
+
1031
+ >>> import numpy as np
1032
+ >>> import nvmath
1033
+
1034
+ Create two 2-D float64 ndarrays on the CPU:
1035
+
1036
+ >>> M, N, K = 1024, 1024, 1024
1037
+ >>> a = np.random.rand(M, K)
1038
+ >>> b = np.random.rand(K, N)
1039
+
1040
+ We will define a matrix multiplication operation followed by a RELU epilog function
1041
+ using the specialized matrix multiplication interface.
1042
+
1043
+ Create a Matmul object encapsulating the problem specification above:
1044
+
1045
+ >>> mm = nvmath.linalg.advanced.Matmul(a, b)
1046
+
1047
+ Options can be provided above to control the behavior of the operation using the
1048
+ `options` argument (see :class:`MatmulOptions`).
1049
+
1050
+ Next, plan the operation. The epilog is specified, and optionally, preferences can
1051
+ be specified for planning:
1052
+
1053
+ >>> epilog = nvmath.linalg.advanced.MatmulEpilog.RELU
1054
+ >>> algorithms = mm.plan(epilog=epilog)
1055
+
1056
+ Certain epilog choices (like :attr:`nvmath.linalg.advanced.MatmulEpilog.BIAS`)
1057
+ require additional input provided using the `epilog_inputs` argument to
1058
+ :meth:`plan`.
1059
+
1060
+ Now execute the matrix multiplication, and obtain the result `r1` as a NumPy
1061
+ ndarray.
1062
+
1063
+ >>> r1 = mm.execute()
1064
+
1065
+ Finally, free the object's resources. To avoid having to explicitly make this
1066
+ call, it's recommended to use the Matmul object as a context manager as shown below,
1067
+ if possible.
1068
+
1069
+ >>> mm.free()
1070
+
1071
+ Note that all :class:`Matmul` methods execute on the current stream by default.
1072
+ Alternatively, the `stream` argument can be used to run a method on a specified
1073
+ stream.
1074
+
1075
+ Let's now look at the same problem with CuPy ndarrays on the GPU.
1076
+
1077
+ >>> import cupy as cp
1078
+ >>> a = cp.random.rand(M, K)
1079
+ >>> b = cp.random.rand(K, N)
1080
+
1081
+ Create an Matmul object encapsulating the problem specification described earlier
1082
+ and use it as a context manager.
1083
+
1084
+ >>> with nvmath.linalg.advanced.Matmul(a, b) as mm:
1085
+ ... algorithms = mm.plan(epilog=epilog)
1086
+ ...
1087
+ ... # Execute the operation to get the first result.
1088
+ ... r1 = mm.execute()
1089
+ ...
1090
+ ... # Update operands A and B in-place (see reset_operands() for an
1091
+ ... # alternative).
1092
+ ... a[:] = cp.random.rand(M, K)
1093
+ ... b[:] = cp.random.rand(K, N)
1094
+ ...
1095
+ ... # Execute the operation to get the new result.
1096
+ ... r2 = mm.execute()
1097
+
1098
+
1099
+ All the resources used by the object are released at the end of the block.
1100
+
1101
+ Further examples can be found in the `nvmath/examples/linalg/advanced/matmul
1102
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/linalg/advanced/matmul>`_
1103
+ directory.
1104
+ """
1105
+
1106
+ def __init__(
1107
+ self,
1108
+ a,
1109
+ b,
1110
+ /,
1111
+ c=None,
1112
+ *,
1113
+ alpha=None,
1114
+ beta=None,
1115
+ qualifiers=None,
1116
+ quantization_scales=None,
1117
+ options=None,
1118
+ stream: utils.AnyStream | int | None = None,
1119
+ ):
1120
+ options = utils.check_or_create_options(_configuration.MatmulOptions, options, "Matrix multiplication options")
1121
+ assert options is not None
1122
+ self.options = options
1123
+
1124
+ if c is None and options.inplace:
1125
+ raise ValueError("The operation cannot be inplace if operand C is not provided.")
1126
+
1127
+ self.logger = options.logger if options.logger is not None else logging.getLogger()
1128
+
1129
+ if options.inplace and options.result_type is not None:
1130
+ self.logger.warning(
1131
+ f"Matmul: The provided result type {options.result_type} in options is ignored since \
1132
+ the operation is in-place."
1133
+ )
1134
+ self.inplace = options.inplace
1135
+
1136
+ def check_dtype(dtype, operand_name):
1137
+ if dtype not in SUPPORTED_TYPES:
1138
+ raise ValueError(f"The dtype of operand {operand_name} ({dtype}) is not supported.")
1139
+
1140
+ # The matrix multiplication has two required operands 'a' and 'b', and one optional
1141
+ # operand 'c'.
1142
+ a = tensor_wrapper.wrap_operand(a)
1143
+ b = tensor_wrapper.wrap_operand(b)
1144
+ check_dtype(a.dtype, "A")
1145
+ check_dtype(b.dtype, "B")
1146
+ self.logger.info("= SPECIFICATION PHASE =")
1147
+ if self.inplace:
1148
+ self.logger.info("The MM operation will be performed in-place (the result will be written into operand C).")
1149
+ self.logger.info(f"The data type of operand A is '{a.dtype}', and that of operand B is '{b.dtype}'.")
1150
+
1151
+ # Detect FP4 A/B operands, and validate compatibility/format.
1152
+ # This check is done as early as possible.
1153
+ self.using_fp4_ab = _detect_and_validate_ab_operands_for_fp4(a, b, self.logger)
1154
+ if self.using_fp4_ab and not options.block_scaling:
1155
+ # FP4 only supports block scaling, at least for now
1156
+ raise ValueError(
1157
+ f"When using FP4 (float4_e2m1fn_x2) A, B operands, block_scaling=True is required. "
1158
+ f"Got block_scaling={options.block_scaling}."
1159
+ )
1160
+
1161
+ self.num_operands = 2
1162
+ if c is not None:
1163
+ self.num_operands = 3
1164
+ c = tensor_wrapper.wrap_operand(c)
1165
+ if len(c.shape) < 2:
1166
+ raise ValueError(
1167
+ "In order to avoid broadcasting behavior ambiguity, `c` must be at least 2-D. "
1168
+ "Use a singleton dimension to convert your input array to 2-D."
1169
+ )
1170
+ check_dtype(c.dtype, "C")
1171
+ self.logger.info(f"The data type of operand C is {c.dtype}.")
1172
+
1173
+ if c is not None and beta is None:
1174
+ raise ValueError("A value for beta must be provided if operand C is provided.")
1175
+
1176
+ if (a.dtype, b.dtype) not in NAMES_TO_DEFAULT_SCALE_TYPE:
1177
+ raise ValueError(f"Unsupported combination of dtypes for operands A {a.dtype} and B {b.dtype}.")
1178
+
1179
+ # Currently, a.dtype != b.dtype is only supported for FP8 (different FP8 kinds are
1180
+ # allowed), so we assume that A and B have equal width.
1181
+ self.input_type_width = typemaps.NAME_TO_DATA_WIDTH[a.dtype]
1182
+
1183
+ assert self.num_operands == 2 or self.num_operands == 3, "Internal Error."
1184
+
1185
+ _check_extents(a.shape, "a")
1186
+ _check_extents(b.shape, "b")
1187
+ if c is not None:
1188
+ _check_extents(c.shape, "c")
1189
+
1190
+ # Infer the library package & device ID the operands belong to.
1191
+ operands = [a, b]
1192
+ if self.num_operands == 3:
1193
+ operands.append(c)
1194
+ self.operands: MutableSequence[utils.TensorHolder] | None = operands
1195
+
1196
+ self.package = utils.get_operands_package(operands)
1197
+ self.memory_space = "cuda"
1198
+ self.device_id = utils.get_operands_device_id(operands)
1199
+ if self.device_id == "cpu":
1200
+ if self.package == "numpy":
1201
+ self.package = "cuda"
1202
+ self.memory_space = "cpu"
1203
+ self.device_id = options.device_id
1204
+ self.logger.info(
1205
+ f"The input operands' memory space is {self.memory_space}, and the execution space is on device {self.device_id}."
1206
+ )
1207
+
1208
+ # Allocate device memory (in stream context) if needed.
1209
+ stream_holder = utils.get_or_create_stream(self.device_id, stream, self.package)
1210
+ self.logger.info(f"The specified stream for the Matmul ctor is {stream_holder.obj}.")
1211
+
1212
+ # Copy operands to device (and store reference to CPU operand), if needed.
1213
+ self.cpu_c_ref = None
1214
+ if self.memory_space == "cpu":
1215
+ if self.inplace:
1216
+ self.cpu_c_ref = self.operands[2] # Hold reference, needed for inplace operations.
1217
+ self.operands = list(tensor_wrapper.to(self.operands, self.device_id, stream_holder))
1218
+
1219
+ # Set qualifiers.
1220
+ self.qualifiers = qualifiers if qualifiers is not None else np.zeros((3,), dtype=_configuration.matrix_qualifiers_dtype)
1221
+ if self.qualifiers.dtype != _configuration.matrix_qualifiers_dtype:
1222
+ raise ValueError(
1223
+ "The qualifiers must be specified as a NumPy array of length 3 corresponding to the operands A, B, and "
1224
+ "C of type 'matrix_qualifiers_dtype'."
1225
+ )
1226
+ # Set qualifiers based on torch lazy conjugation flag if not provided.
1227
+ self.qualifiers[0]["is_conjugate"] = self.qualifiers[0]["is_conjugate"] ^ self.operands[0].is_conjugate
1228
+ self.qualifiers[1]["is_conjugate"] = self.qualifiers[1]["is_conjugate"] ^ self.operands[1].is_conjugate
1229
+ self.lazy_conjugation = (self.operands[0].is_conjugate, self.operands[1].is_conjugate, False)
1230
+ if self.num_operands == 3:
1231
+ self.qualifiers[2]["is_conjugate"] = self.qualifiers[2]["is_conjugate"] ^ self.operands[2].is_conjugate
1232
+ if self.qualifiers[2]["is_conjugate"]:
1233
+ raise ValueError("The conjugate flag is currently not supported for operand C.")
1234
+
1235
+ # Set blocking or non-blocking behavior.
1236
+ self.blocking = self.options.blocking is True or self.memory_space == "cpu"
1237
+ if self.blocking:
1238
+ self.call_prologue = "This call is blocking and will return only after the operation is complete."
1239
+ else:
1240
+ self.call_prologue = (
1241
+ "This call is non-blocking and will return immediately after the operation is launched on the device."
1242
+ )
1243
+
1244
+ # The result class is that of the first wrapped device operand.
1245
+ self.result_class = self.operands[0].__class__
1246
+
1247
+ # Set memory allocator.
1248
+ self.allocator = (
1249
+ options.allocator
1250
+ if options.allocator is not None
1251
+ else memory._MEMORY_MANAGER[self.package](self.device_id, self.logger)
1252
+ )
1253
+
1254
+ # Set memory limit.
1255
+ self.memory_limit = utils.get_memory_limit_from_device_id(self.options.memory_limit, self.device_id)
1256
+ self.logger.info(f"The memory limit is {formatters.MemoryStr(self.memory_limit)}.")
1257
+
1258
+ # Set handle. We don't destroy handles we create.
1259
+ if options.handle is not None:
1260
+ self.handle = options.handle
1261
+ else:
1262
+ self.handle = get_handle(self.device_id)
1263
+
1264
+ # Determine the data types for a and b.
1265
+ self.a_dtype = typemaps.NAME_TO_DATA_TYPE[a.dtype]
1266
+ self.b_dtype = typemaps.NAME_TO_DATA_TYPE[b.dtype]
1267
+ self.a_dtype_name = a.dtype
1268
+ self.b_dtype_name = b.dtype
1269
+
1270
+ self.is_complex = "complex" in self.a_dtype_name or "complex" in self.b_dtype_name
1271
+
1272
+ # Determine the data types for c and d.
1273
+ self.d_dtype = None if self.inplace else options.result_type
1274
+ if self.num_operands == 3:
1275
+ self.c_dtype = typemaps.NAME_TO_DATA_TYPE[c.dtype]
1276
+ if self.d_dtype is None:
1277
+ self.d_dtype = self.c_dtype
1278
+
1279
+ elif self.num_operands == 2:
1280
+ if self.d_dtype is None:
1281
+ self.d_dtype = self.a_dtype
1282
+
1283
+ # c_dtype matches d_dtype, except if output is FP4/FP8, then C uses float16
1284
+ if self.d_dtype in (CudaDataType.CUDA_R_4F_E2M1, CudaDataType.CUDA_R_8F_E5M2, CudaDataType.CUDA_R_8F_E4M3):
1285
+ self.c_dtype = CudaDataType.CUDA_R_16F
1286
+ else:
1287
+ self.c_dtype = self.d_dtype
1288
+
1289
+ self.c_dtype_name = typemaps.DATA_TYPE_TO_NAME[self.c_dtype]
1290
+ self.d_dtype_name = typemaps.DATA_TYPE_TO_NAME[self.d_dtype]
1291
+ self.c_dtype_width = typemaps.NAME_TO_DATA_WIDTH[self.c_dtype_name]
1292
+ self.d_dtype_width = typemaps.NAME_TO_DATA_WIDTH[self.d_dtype_name]
1293
+
1294
+ self.logger.info(f"The data type for the result D is '{self.d_dtype_name}'.")
1295
+
1296
+ def assert_valid_compute_type(compute_type):
1297
+ if compute_type not in COMPUTE_TYPE_TO_DEFAULT_SCALE_TYPE["real"]:
1298
+ message = f"Unsupported compute type. The compute type '{repr(compute_type)}' is currently not supported."
1299
+ raise ValueError(message)
1300
+
1301
+ # Determine the scale type.
1302
+ if options.scale_type is None:
1303
+ if options.compute_type is not None:
1304
+ assert_valid_compute_type(options.compute_type)
1305
+ if self.is_complex:
1306
+ scale_type_map = COMPUTE_TYPE_TO_DEFAULT_SCALE_TYPE["complex"]
1307
+ else:
1308
+ scale_type_map = COMPUTE_TYPE_TO_DEFAULT_SCALE_TYPE["real"]
1309
+ self.scale_type = scale_type_map[options.compute_type]
1310
+ else:
1311
+ self.scale_type = NAMES_TO_DEFAULT_SCALE_TYPE[(self.a_dtype_name, self.b_dtype_name)]
1312
+ self.scale_type_name = typemaps.DATA_TYPE_TO_NAME[self.scale_type]
1313
+ else:
1314
+ self.scale_type = options.scale_type
1315
+ if self.scale_type not in SCALE_TYPE_TO_DEFAULT_COMPUTE_TYPE:
1316
+ message = f"Unsupported scale type. The data type '{repr(self.scale_type)}' is currently not supported."
1317
+ raise ValueError(message)
1318
+ self.scale_type_name = typemaps.DATA_TYPE_TO_NAME[self.scale_type]
1319
+ self.logger.info(f"The scale type is '{self.scale_type_name}'.")
1320
+
1321
+ # Determine the compute type.
1322
+ if options.compute_type is None:
1323
+ if options.scale_type is not None:
1324
+ self.compute_type = SCALE_TYPE_TO_DEFAULT_COMPUTE_TYPE[options.scale_type]
1325
+ else:
1326
+ self.compute_type = NAMES_TO_DEFAULT_COMPUTE_TYPE[(self.a_dtype_name, self.b_dtype_name)]
1327
+ else:
1328
+ self.compute_type = options.compute_type
1329
+ assert_valid_compute_type(self.compute_type)
1330
+ self.logger.info(f"The compute type is {self.compute_type.name}.")
1331
+
1332
+ def is_supported(atype, btype, compute_type, scale_type):
1333
+ ct = cublas.ComputeType
1334
+ st = CudaDataType
1335
+ abtype = atype if atype == btype else (atype, btype)
1336
+ if compute_type in (ct.COMPUTE_16F, ct.COMPUTE_16F_PEDANTIC):
1337
+ return scale_type == st.CUDA_R_16F and abtype == "float16"
1338
+ elif compute_type == ct.COMPUTE_32F_PEDANTIC:
1339
+ if scale_type == st.CUDA_R_32F:
1340
+ return abtype in ("float32", "bfloat16", "float16", "float8_e4m3fn", "float8_e5m2")
1341
+ elif scale_type == st.CUDA_C_32F:
1342
+ return abtype == "complex64"
1343
+ elif compute_type == ct.COMPUTE_32F:
1344
+ if scale_type == st.CUDA_R_32F:
1345
+ return abtype in (
1346
+ "float32",
1347
+ "bfloat16",
1348
+ "float16",
1349
+ "float8_e4m3fn",
1350
+ "float8_e5m2",
1351
+ ("float8_e4m3fn", "float8_e5m2"),
1352
+ ("float8_e5m2", "float8_e4m3fn"),
1353
+ )
1354
+ elif scale_type == st.CUDA_C_32F:
1355
+ return abtype == "complex64"
1356
+ elif compute_type in (
1357
+ ct.COMPUTE_32F_FAST_16F,
1358
+ ct.COMPUTE_32F_FAST_16BF,
1359
+ ct.COMPUTE_32F_FAST_TF32,
1360
+ ct.COMPUTE_32F_EMULATED_16BFX9,
1361
+ ):
1362
+ if scale_type == st.CUDA_R_32F:
1363
+ return abtype == "float32"
1364
+ if scale_type == st.CUDA_C_32F:
1365
+ return abtype == "complex64"
1366
+ elif compute_type in (ct.COMPUTE_64F, ct.COMPUTE_64F_PEDANTIC, ct.COMPUTE_64F_EMULATED_FIXEDPOINT):
1367
+ if scale_type == st.CUDA_R_64F:
1368
+ return abtype == "float64"
1369
+ if scale_type == st.CUDA_C_64F:
1370
+ return abtype == "complex128"
1371
+ return False
1372
+
1373
+ def is_supported_nvfp4(ctype, dtype, compute_type, scale_type):
1374
+ """
1375
+ Validate type combinations based on cuBLAS documentation.
1376
+ https://docs.nvidia.com/cuda/cublas/index.html#id105,
1377
+ see Table 4 and related text.
1378
+ """
1379
+ ct = cublas.ComputeType
1380
+ st = CudaDataType
1381
+
1382
+ # Check compute/scale types
1383
+ if not (compute_type == ct.COMPUTE_32F and scale_type == st.CUDA_R_32F):
1384
+ raise ValueError(
1385
+ f"Selected scale_type={repr(self.scale_type)} compute_type={repr(self.compute_type)} "
1386
+ f"are not supported. Only the following combination is supported: "
1387
+ f"A and B must both be float4_e2m1fn_x2, compute_type=COMPUTE_32F, scale_type=CUDA_R_32F."
1388
+ )
1389
+
1390
+ # Check valid C/D type combinations (Table 4)
1391
+ valid_cd_combos = {
1392
+ ("bfloat16", "bfloat16"),
1393
+ ("bfloat16", "float4_e2m1fn_x2"),
1394
+ ("float16", "float16"),
1395
+ ("float16", "float4_e2m1fn_x2"),
1396
+ ("float32", "float32"),
1397
+ }
1398
+ if (ctype, dtype) not in valid_cd_combos:
1399
+ raise ValueError(
1400
+ f"Invalid C/D type combination for FP4: ctype={self.c_dtype_name}, dtype={self.d_dtype_name}. "
1401
+ f"Valid combinations are: (bfloat16, bfloat16), (bfloat16, float4_e2m1fn_x2), "
1402
+ f"(float16, float16), (float16, float4_e2m1fn_x2), (float32, float32). "
1403
+ f"See cuBLAS documentation Table 4."
1404
+ )
1405
+
1406
+ if self.using_fp4_ab:
1407
+ # Validate remaining type combinations (A and B are already
1408
+ # known to be float4_e2m1fn_x2)
1409
+ is_supported_nvfp4(self.c_dtype_name, self.d_dtype_name, self.compute_type, self.scale_type)
1410
+ elif not is_supported(self.a_dtype_name, self.b_dtype_name, self.compute_type, self.scale_type):
1411
+ raise ValueError(
1412
+ f"Selected scale_type={repr(self.scale_type)} compute_type={repr(self.compute_type)} "
1413
+ + f"are not supported for data types {self.a_dtype_name} (A) and {self.b_dtype_name} (B)."
1414
+ )
1415
+
1416
+ # Set alpha and beta.
1417
+ self.alpha = np.zeros((1,), dtype=self.scale_type_name)
1418
+ try:
1419
+ self.alpha[0] = alpha if alpha is not None else 1
1420
+ except (ValueError, TypeError) as e:
1421
+ raise ValueError(f"The value provided for alpha {alpha} is not convertible to dtype '{self.alpha.dtype}'.") from e
1422
+
1423
+ self.beta = np.zeros((1,), dtype=self.scale_type_name)
1424
+ if beta is not None and self.num_operands == 2:
1425
+ self.logger.warning(f"Matmul: The provided beta value {beta} is ignored since operand C is not specified.")
1426
+ try:
1427
+ self.beta[0] = beta if beta is not None and self.num_operands == 3 else 0
1428
+ except (ValueError, TypeError) as e:
1429
+ raise ValueError(f"The value provided for beta {beta} is not convertible to dtype '{self.beta.dtype}'.") from e
1430
+
1431
+ # Set narrow-precision (FP8 and lower) quantization_scales.
1432
+ if self.input_type_width <= 8:
1433
+ self.quantization_scales = self._validate_operand_scales(quantization_scales, all_required=True)
1434
+ elif quantization_scales is not None:
1435
+ self.logger.warning(
1436
+ "Matmul: The provided scales are ignored, since they are only applicable to narrow-precision (FP8 and lower) "
1437
+ "operations."
1438
+ )
1439
+
1440
+ if self.options.result_amax and self.d_dtype_width > 8:
1441
+ raise ValueError("result_amax=True is allowed only for narrow-precision (FP8 and lower) results")
1442
+
1443
+ # Check operands alignment if needed
1444
+ if self.input_type_width <= 8:
1445
+ for operand, operand_name in zip(self.operands, "ABC", strict=False):
1446
+ if operand.data_ptr % 16 != 0:
1447
+ raise ValueError(
1448
+ f"For narrow-precision (FP8 and lower) multiplication, operand {operand_name} should be aligned to 16 "
1449
+ "bytes."
1450
+ )
1451
+
1452
+ # Capture physical operand extents and strides for consistency check when resetting
1453
+ # operands. In the case of FP4, these must be the packed (physical) values,
1454
+ # not the logical ones , because reset() receives packed FP4 tensors
1455
+ # and must compare against the same physical format.
1456
+ # Hence, these two do not need special handling for FP4.
1457
+ self.operand_extents = tuple(o.shape for o in self.operands)
1458
+ self.operand_strides = tuple(o.strides for o in self.operands)
1459
+
1460
+ if self.using_fp4_ab:
1461
+ a_layout, b_layout = _create_fp4_ab_layouts(
1462
+ self.operands[0].shape,
1463
+ self.operands[0].strides,
1464
+ self.qualifiers[0]["is_conjugate"],
1465
+ self.operands[1].shape,
1466
+ self.operands[1].strides,
1467
+ self.qualifiers[1]["is_conjugate"],
1468
+ )
1469
+ self.logger.info(f"A: packed shape {self.operands[0].shape} -> logical shape {a_layout.shape}")
1470
+ self.logger.info(f"B: packed shape {self.operands[1].shape} -> logical shape {b_layout.shape}")
1471
+ else:
1472
+ self.logger.info(f"A: shape {self.operands[0].shape}, strides {self.operands[0].strides}")
1473
+ self.logger.info(f"B: shape {self.operands[1].shape}, strides {self.operands[1].strides}")
1474
+ a_layout = MatrixLayout(self.operands[0].shape, self.operands[0].strides, self.qualifiers[0]["is_conjugate"])
1475
+ b_layout = MatrixLayout(self.operands[1].shape, self.operands[1].strides, self.qualifiers[1]["is_conjugate"])
1476
+
1477
+ # C is never FP4 (only A and B can be float4_e2m1fn_x2),
1478
+ # so no packed-to-logical conversion needed.
1479
+ c_layout = MatrixLayout(self.operands[2].shape, self.operands[2].strides) if self.num_operands == 3 else None # type: ignore[union-attr]
1480
+
1481
+ # Enforce equal batch shape for A and B if block_scaling=True.
1482
+ if self.options.block_scaling and a_layout.shape[:-2] != b_layout.shape[:-2]:
1483
+ raise ValueError(
1484
+ "When block_scaling=True, the batch dimensions of A and B must match (broadcasting is not supported)."
1485
+ )
1486
+
1487
+ # Get the operation traits.
1488
+ self.mm_traits = get_mm_traits(a_layout, b_layout, c_layout, self.inplace, self.logger)
1489
+ self.result_traits = None # Wait till planning to determine this based on the epilog.
1490
+ self.logger.info(
1491
+ f"The matrix multiplication attributes are M = {self.mm_traits.M}, N = {self.mm_traits.N}, and "
1492
+ f"K = {self.mm_traits.K}."
1493
+ )
1494
+ self.logger.info(
1495
+ f"The batch count is {self.mm_traits.batch_count}, and the batch shape is {self.mm_traits.batch_shape} "
1496
+ f"with batch axis order {self.mm_traits.batch_axis_order}."
1497
+ )
1498
+
1499
+ # Create and set the operation descriptor.
1500
+ self.mm_desc = cublaslt.matmul_desc_create(self.compute_type, self.scale_type)
1501
+
1502
+ self.mm_desc_ifc = matmul_desc_ifc.MatmulDescInterface(self.mm_desc)
1503
+ self.mm_desc_ifc.compute_type = self.compute_type
1504
+ self.mm_desc_ifc.scale_type = self.scale_type
1505
+
1506
+ # Guard SM count target and fast accumulation flag.
1507
+ version = cublaslt.get_version()
1508
+ if options.sm_count_target > 0:
1509
+ if version < 111103:
1510
+ raise ValueError(f"The 'sm_count_target' option is not supported in cuBLASLt version {version}.")
1511
+ self.mm_desc_ifc.sm_count_target = options.sm_count_target
1512
+ self.logger.info(f"The SM count target is {options.sm_count_target}.")
1513
+
1514
+ if options.fast_accumulation:
1515
+ if version < 111103:
1516
+ raise ValueError(f"The 'fast_accumulation' option is not supported in cuBLASLt version {version}.")
1517
+ self.mm_desc_ifc.fast_accum = options.fast_accumulation
1518
+ self.logger.info(f"The flag for fast accumulation mode is {options.fast_accumulation}.")
1519
+
1520
+ if self.input_type_width == 8 and version < 120800:
1521
+ raise ValueError(
1522
+ f"FP8 is not supported for cuBLASLt version {version}. cuBLASLt version 12.8 or higher is required."
1523
+ )
1524
+
1525
+ # Planning preferences
1526
+ self.preferences = None
1527
+
1528
+ # Epilog attributes.
1529
+ self.epilog = None
1530
+
1531
+ # Epilog attributes: name-to-operand.
1532
+ self.epilog_operands: dict[str, typing.Any] = {}
1533
+
1534
+ # Epilog attributes: epilog input name-to-handler.
1535
+ self.epilog_input_name_to_handler: dict[str, typing.Any] = {}
1536
+
1537
+ # Epilog attributes: name-to-output tensor.
1538
+ self.epilog_outputs: dict[str, typing.Any] = {}
1539
+
1540
+ # Keep track of epilog input traits for resetting operands.
1541
+ self.epilog_inputs_traits: dict[str, typing.Any] = {}
1542
+
1543
+ # Keep track of epilog output handlers to allocate output in execute().
1544
+ self.epilog_output_handlers: list[EpilogOutputHandler] = []
1545
+
1546
+ # Non-epilog aux outputs. Currently, only used for quantization outputs (amax etc.)
1547
+ self.aux_outputs = None
1548
+
1549
+ # Plan attributes.
1550
+ self.preference_ptr = None
1551
+ self.a_layout_ptr, self.b_layout_ptr, self.c_layout_ptr, self.d_layout_ptr = None, None, None, None
1552
+ self.flop_count = 0
1553
+ self.mm_planned = False
1554
+
1555
+ # Algorithm attributes.
1556
+ self.algorithms_buffer = None
1557
+ self.algorithm_objects = None
1558
+ self.cached_best_algorithm_struct = None
1559
+
1560
+ # Workspace attributes.
1561
+ self.workspace_ptr: None | memory.MemoryPointer = None
1562
+ self.workspace_size = 0
1563
+ self.workspace_allocated_size = 0
1564
+ self.workspace_allocated_here = False
1565
+
1566
+ # Attributes to establish stream ordering.
1567
+ self.workspace_stream = None
1568
+ self.last_compute_event = None
1569
+
1570
+ # Track whether the user has called release_operands().
1571
+ self._operands_released = False
1572
+
1573
+ # Device-side array with the quantization_scales
1574
+ self.quantization_scales_device: dict[str, utils.TensorHolder] = {}
1575
+
1576
+ self.valid_state = True
1577
+ self.logger.info("The Matmul operation has been created.")
1578
+
1579
+ def __enter__(self):
1580
+ return self
1581
+
1582
+ def __exit__(self, exc_type, exc_value, traceback):
1583
+ self.free()
1584
+
1585
+ def _check_valid_matmul(self, *args, **kwargs):
1586
+ """
1587
+ Check if the Matmul object is alive and well.
1588
+ """
1589
+ if not self.valid_state:
1590
+ raise InvalidMatmulState("The Matmul object cannot be used after resources are free'd")
1591
+
1592
+ def _check_valid_operands(self, *args, **kwargs):
1593
+ """
1594
+ Check if the operands are available for the operation.
1595
+ """
1596
+ what = kwargs["what"]
1597
+ if self._operands_released:
1598
+ raise RuntimeError(
1599
+ f"{what} cannot be performed after the operands have been released. "
1600
+ f"Use reset_operands() to provide new operands before performing the {what.lower()}."
1601
+ )
1602
+
1603
+ def _free_plan_resources(self, exception: Exception | None = None) -> bool:
1604
+ """
1605
+ Free resources allocated in planning.
1606
+ """
1607
+
1608
+ # Destroy matrix layouts.
1609
+ if self.a_layout_ptr is not None:
1610
+ cublaslt.matrix_layout_destroy(self.a_layout_ptr)
1611
+ self.a_layout_ptr = None
1612
+ if self.b_layout_ptr is not None:
1613
+ cublaslt.matrix_layout_destroy(self.b_layout_ptr)
1614
+ self.b_layout_ptr = None
1615
+ if self.c_layout_ptr != self.d_layout_ptr and self.c_layout_ptr is not None:
1616
+ cublaslt.matrix_layout_destroy(self.c_layout_ptr)
1617
+ self.c_layout_ptr = None
1618
+ if self.d_layout_ptr is not None:
1619
+ cublaslt.matrix_layout_destroy(self.d_layout_ptr)
1620
+ self.d_layout_ptr = None
1621
+
1622
+ if self.preference_ptr is not None:
1623
+ cublaslt.matmul_preference_destroy(self.preference_ptr)
1624
+ self.preference_ptr = None
1625
+
1626
+ self.mm_planned = False
1627
+ return True
1628
+
1629
+ def _check_planned(self, *args, **kwargs):
1630
+ what = kwargs["what"]
1631
+ if not self.mm_planned:
1632
+ raise RuntimeError(f"{what} cannot be performed before plan() has been called.")
1633
+
1634
+ def _free_workspace_memory(self, exception: Exception | None = None) -> bool:
1635
+ """
1636
+ Free workspace by releasing the MemoryPointer object.
1637
+ """
1638
+ if self.workspace_ptr is None:
1639
+ return True
1640
+
1641
+ self.workspace_ptr = None
1642
+ self.workspace_allocated_size = 0
1643
+ self.logger.debug("[_free_workspace_memory] The workspace has been released.")
1644
+
1645
+ return True
1646
+
1647
+ def _reset_workspace_allocation_tracking(self):
1648
+ """
1649
+ Reset workspace allocation tracking attributes to False at the end of the methods
1650
+ where workspace memory is potentially allocated. This is necessary to prevent any
1651
+ exceptions raised before method entry from using stale tracking values.
1652
+ """
1653
+ self.workspace_allocated_here = False
1654
+
1655
+ @utils.precondition(_check_valid_matmul)
1656
+ def _release_workspace_memory_perhaps(self, release_workspace):
1657
+ """
1658
+ Free workspace memory if it's larger than the specified limit.
1659
+ """
1660
+ if not release_workspace:
1661
+ return True
1662
+
1663
+ # Establish ordering wrt the computation and free workspace if requested.
1664
+ if self.last_compute_event is not None:
1665
+ self.workspace_stream.wait(self.last_compute_event)
1666
+ self.logger.debug("Established ordering with respect to the computation before releasing the workspace.")
1667
+ self.last_compute_event = None
1668
+
1669
+ self.logger.debug("[_release_workspace_memory_perhaps] The workspace memory will be released.")
1670
+ return self._free_workspace_memory()
1671
+
1672
+ def _release_workspace_memory_perhaps_wrapper(self, exception: Exception | None = None) -> bool:
1673
+ """
1674
+ This is used in @atomic.
1675
+ """
1676
+ self._release_workspace_memory_perhaps(release_workspace=self.workspace_allocated_here)
1677
+ self._reset_workspace_allocation_tracking()
1678
+ return True
1679
+
1680
+ @utils.precondition(_check_valid_matmul)
1681
+ @utils.precondition(_check_planned, "Workspace memory allocation")
1682
+ @utils.atomic(_free_workspace_memory, method=True)
1683
+ def _allocate_workspace_memory(self, stream_holder: utils.StreamHolder):
1684
+ """
1685
+ Allocate workspace memory using the specified allocator.
1686
+ """
1687
+
1688
+ assert self.workspace_size is not None, "Internal Error."
1689
+ assert self.workspace_allocated_here is False, "Internal Error."
1690
+
1691
+ if self.workspace_size == 0: # For performance, bypass allocator for workspace size == 0.
1692
+ self.workspace_ptr = memory.MemoryPointer(0, 0, finalizer=None)
1693
+ else:
1694
+ self.logger.debug("Allocating workspace for performing the matrix multiplication...")
1695
+ with utils.device_ctx(self.device_id), stream_holder.ctx:
1696
+ try:
1697
+ if isinstance(self.allocator, memory.BaseCUDAMemoryManagerAsync):
1698
+ self.workspace_ptr = self.allocator.memalloc_async(self.workspace_size, stream_holder.obj)
1699
+ else:
1700
+ self.workspace_ptr = self.allocator.memalloc(self.workspace_size)
1701
+ self.workspace_allocated_here = True
1702
+ except TypeError as e:
1703
+ message = (
1704
+ "The method 'memalloc' in the allocator object must conform to the interface in the "
1705
+ "'BaseCUDAMemoryManager' protocol."
1706
+ )
1707
+ raise TypeError(message) from e
1708
+
1709
+ self.workspace_allocated_size = self.workspace_size
1710
+ self.workspace_stream = stream_holder.obj
1711
+ self.logger.debug(
1712
+ f"Finished allocating device workspace of size {formatters.MemoryStr(self.workspace_size)} in the context "
1713
+ f"of stream {self.workspace_stream}."
1714
+ )
1715
+
1716
+ def _allocate_workspace_memory_perhaps(self, stream_holder: utils.StreamHolder):
1717
+ """
1718
+ Allocate workspace memory using the specified allocator, if it hasn't already been
1719
+ done.
1720
+ """
1721
+
1722
+ if self.workspace_ptr is not None and self.workspace_allocated_size >= self.workspace_size:
1723
+ return
1724
+
1725
+ return self._allocate_workspace_memory(stream_holder)
1726
+
1727
+ @utils.precondition(_check_valid_matmul)
1728
+ def applicable_algorithm_ids(self, limit=8):
1729
+ """Obtain the algorithm IDs that are applicable to this matrix multiplication.
1730
+
1731
+ Args:
1732
+ limit: The maximum number of applicable algorithm IDs that is desired
1733
+
1734
+ Returns:
1735
+ A sequence of algorithm IDs that are applicable to this matrix multiplication
1736
+ problem specification, in random order.
1737
+ """
1738
+ ...
1739
+ algo_ids = cublaslt.matmul_algo_get_ids(
1740
+ self.handle,
1741
+ self.compute_type,
1742
+ self.scale_type,
1743
+ self.a_dtype,
1744
+ self.b_dtype,
1745
+ self.c_dtype,
1746
+ self.d_dtype,
1747
+ limit,
1748
+ )
1749
+ return algo_ids
1750
+
1751
+ def _validate_scalar_scale(self, operand: str):
1752
+ """
1753
+ Validates a scalar scale.
1754
+ """
1755
+ if self.options.block_scaling:
1756
+ raise ValueError(f"A scalar tensor-wide scale factor is not allowed for {operand.upper()} when block_scaling=True.")
1757
+
1758
+ def _validate_tensor_scale(self, scale, operand: str, operand_size=None):
1759
+ """
1760
+ Validates a tensor scale.
1761
+
1762
+ Args:
1763
+ scale: The tensor scale to validate
1764
+ operand: The operand name (a, b, c, d)
1765
+ operand_size: Size of the operand (needed for block scaling shape validation)
1766
+ """
1767
+ # Package validation: Normalize "numpy" to "cuda" to match the behavior in __init__.
1768
+ # When operands are NumPy on CPU, self.package is set to "cuda" (execution package),
1769
+ # so we must also normalize NumPy scales to "cuda" to allow the same input format.
1770
+ # This handles the NumPy <=> CuPy asymmetry where NumPy on CPU is accepted as input
1771
+ # but internally converted to CuPy for CUDA execution.
1772
+ scale_package = utils.infer_object_package(scale)
1773
+ scale_package = "cuda" if scale_package == "numpy" else scale_package
1774
+ if scale_package != self.package:
1775
+ raise TypeError(
1776
+ f"The quantization scaling tensor for {operand.upper()} must belong to the same package as the operands."
1777
+ )
1778
+
1779
+ # Wrap temporarily since this is needed for validation
1780
+ scale_wrapped = tensor_wrapper.wrap_operand(scale)
1781
+
1782
+ # Device/memory space validation
1783
+ expected_device_id = "cpu" if self.memory_space == "cpu" else self.device_id
1784
+ if expected_device_id != scale_wrapped.device_id:
1785
+ raise ValueError(
1786
+ f'The scale for {operand.upper()} is on device "{scale_wrapped.device_id}", '
1787
+ f'but it should be on device "{expected_device_id}" to match the operands memory space.'
1788
+ )
1789
+
1790
+ # Shape and dtype validation for non-block-scaling
1791
+ if not self.options.block_scaling:
1792
+ if scale_wrapped.shape not in ((1,), ()):
1793
+ raise ValueError(
1794
+ f"The scale for {operand.upper()} must be of shape (1,) or (). Got {scale_wrapped.shape} instead."
1795
+ )
1796
+ if scale_wrapped.dtype != "float32":
1797
+ raise ValueError(f"The scale for {operand.upper()} must be float32 type. Got {scale_wrapped.dtype} instead.")
1798
+
1799
+ # Shape and dtype validation for block-scaling
1800
+ elif self.input_type_width == 8:
1801
+ # FP8: 32-element blocks with 128x128 tiled layout (VEC32_UE8M0)
1802
+ # Dtype validation (always possible)
1803
+ if scale_wrapped.dtype != "uint8":
1804
+ raise ValueError(f"Block scales for {operand.upper()} must be uint8 tensor.")
1805
+
1806
+ # Shape validation (only if operand_size is available)
1807
+ if operand_size is not None:
1808
+ expected_shape = (operand_size // 32,)
1809
+ if scale_wrapped.shape != expected_shape:
1810
+ raise ValueError(
1811
+ f"Scales for {operand.upper()} should have shape {expected_shape}. Got {scale_wrapped.shape}."
1812
+ )
1813
+ elif self.using_fp4_ab:
1814
+ # FP4: 16-element blocks with 128x64 tiled layout (VEC16_UE4M3)
1815
+ # Dtype validation (always possible)
1816
+ if scale_wrapped.dtype != "float8_e4m3fn":
1817
+ raise ValueError(
1818
+ f"Block scales for {operand.upper()} must be float8_e4m3fn tensor (UE4M3 format). "
1819
+ f"Got {scale_wrapped.dtype}."
1820
+ )
1821
+
1822
+ # Shape validation (only if operand_size is available)
1823
+ if operand_size is not None:
1824
+ expected_shape = (operand_size // 16,)
1825
+ if scale_wrapped.shape != expected_shape:
1826
+ raise ValueError(
1827
+ f"Scales for {operand.upper()} should have shape {expected_shape}. Got {scale_wrapped.shape}."
1828
+ )
1829
+ else:
1830
+ raise ValueError("block_scaling == True is not supported for non-FP8/FP4 types.")
1831
+
1832
+ def _validate_operand_scales(self, quantization_scales, all_required):
1833
+ """
1834
+ Validates quantization scales, wrapping them into a MatmulQuantizationScales
1835
+ object if needed.
1836
+
1837
+ Args:
1838
+ quantization_scales: The quantization scales to validate.
1839
+ all_required: Whether all scales are required.
1840
+
1841
+ Returns:
1842
+ A MatmulQuantizationScales object with the validated quantization scales.
1843
+ """
1844
+ if quantization_scales is None:
1845
+ raise ValueError(
1846
+ "Scales are required for narrow-precision (FP8 and lower) operations. "
1847
+ "Please set `quantization_scales` argument."
1848
+ )
1849
+
1850
+ # wrap the quantization scales into a MatmulQuantizationScales object if needed
1851
+ # otherwise, return the quantization scales as is
1852
+ quantization_scales = utils.check_or_create_options(
1853
+ _configuration.MatmulQuantizationScales, quantization_scales, "Scale factors"
1854
+ )
1855
+
1856
+ # Validate which scales are required/allowed
1857
+ expected_scales = "AB"
1858
+ if self.d_dtype_width <= 8 and not self.options.block_scaling:
1859
+ expected_scales += "D"
1860
+ elif quantization_scales.d is not None:
1861
+ if self.options.block_scaling:
1862
+ raise ValueError("Quantization scaling is not supported for D when `block_scaling` option is enabled.")
1863
+ if self.d_dtype_width > 8:
1864
+ raise ValueError(
1865
+ "Quantization scaling is not supported for D when it is not a narrow-precision (FP8 and lower) type."
1866
+ )
1867
+
1868
+ if self.num_operands == 3 and self.c_dtype_width <= 8:
1869
+ expected_scales += "C"
1870
+ elif quantization_scales.c is not None:
1871
+ raise ValueError(
1872
+ "Quantization scaling is not supported for C when it is not a narrow-precision (FP8 and lower) type."
1873
+ )
1874
+
1875
+ if all_required:
1876
+ for operand in expected_scales:
1877
+ if getattr(quantization_scales, operand.lower()) is None:
1878
+ raise ValueError(f"Scale for {operand.upper()} is not specified")
1879
+
1880
+ # Validate each scale by delegating to scalar/tensor specific validators
1881
+ for operand in "abcd":
1882
+ scale = getattr(quantization_scales, operand)
1883
+ if scale is None:
1884
+ continue
1885
+
1886
+ if isinstance(scale, (int, float)):
1887
+ self._validate_scalar_scale(operand)
1888
+ else:
1889
+ # For block scaling, pass operand size for shape validation
1890
+ if self.options.block_scaling and operand in ("a", "b"):
1891
+ operand_idx = 0 if operand == "a" else 1
1892
+ operand_size = self.operands[operand_idx].size # type: ignore[union-attr,index]
1893
+
1894
+ # For FP4, data is always packed: double the size to get logical size
1895
+ if self.using_fp4_ab:
1896
+ operand_size *= 2 # Packed tensor has half the elements
1897
+ self.logger.debug(f"FP4 scale validation for {operand}: adjusted operand_size={operand_size}")
1898
+ else:
1899
+ operand_size = None
1900
+ self._validate_tensor_scale(scale, operand, operand_size)
1901
+
1902
+ return quantization_scales
1903
+
1904
+ def _validate_epilog_aux_scale(self, aux_quantization_scale, *, required):
1905
+ is_narrow_aux = (
1906
+ self.preferences.epilog.aux_type is not None
1907
+ and typemaps.NAME_TO_DATA_WIDTH[typemaps.DATA_TYPE_TO_NAME[self.preferences.epilog.aux_type]] <= 8
1908
+ )
1909
+ if aux_quantization_scale is not None and not is_narrow_aux:
1910
+ raise ValueError(
1911
+ "Scales for epilog auxiliary output are not supported when `preferences.epilog.aux_type` is not set to a "
1912
+ "narrow-precision type."
1913
+ )
1914
+ elif aux_quantization_scale is None and is_narrow_aux and required:
1915
+ raise ValueError(
1916
+ '"aux_quantization_scale" epilog input is required when `preferences.epilog.aux_type` is set to a '
1917
+ "narrow-precision type."
1918
+ )
1919
+
1920
+ # Validate scalar vs tensor scale (same as for operand scales)
1921
+ if aux_quantization_scale is not None:
1922
+ if isinstance(aux_quantization_scale, (int, float)):
1923
+ self._validate_scalar_scale("epilog_aux")
1924
+ else:
1925
+ # No operand_size for epilog_aux scales
1926
+ self._validate_tensor_scale(aux_quantization_scale, "epilog_aux", operand_size=None)
1927
+
1928
+ def _prepare_validated_scalar_scale(self, scale: int | float, operand: str, stream_holder: utils.StreamHolder):
1929
+ """
1930
+ Converts validated scalar to float32 tensor and copies to GPU.
1931
+ Assumes validation already done in _validate_scalar_scale.
1932
+ """
1933
+ # If it's a scalar, copy to GPU. Float32 is the only type allowed by
1934
+ # cublasLtMatmulScale_t for tensor-wide scaling.
1935
+ self.logger.debug(f"Scale for {operand.upper()} will be copied to device {self.device_id}.")
1936
+ scale_op = tensor_wrapper.wrap_operand(np.asarray([scale], dtype="float32"))
1937
+ self.quantization_scales_device[operand] = scale_op.to(self.device_id, stream_holder)
1938
+
1939
+ def _prepare_validated_tensor_scale(self, scale, operand: str, stream_holder: utils.StreamHolder):
1940
+ """
1941
+ Wraps validated tensor and copies to GPU.
1942
+ Assumes all validation already done in _validate_tensor_scale (called in __init__).
1943
+ This is pure preparation - no validation here.
1944
+
1945
+ Note: We wrap the tensor a second time here (first wrap was for validation).
1946
+ This is acceptable because wrapping is cheap and we get early error detection.
1947
+ """
1948
+ # Wrap the scale (second time - first was for validation in __init__)
1949
+ self.quantization_scales_device[operand] = tensor_wrapper.wrap_operand(scale)
1950
+
1951
+ # Copy to GPU if on CPU (no validation, just preparation)
1952
+ if self.quantization_scales_device[operand].device in (None, "cpu"):
1953
+ self.logger.debug(f"Scale for {operand.upper()} will be copied to device {self.device_id}.")
1954
+ self.quantization_scales_device[operand] = self.quantization_scales_device[operand].to(
1955
+ self.device_id, stream_holder
1956
+ )
1957
+
1958
+ def _prepare_single_validated_scale(self, scale, operand: str, cublas_operand: str, stream_holder: utils.StreamHolder):
1959
+ """
1960
+ Prepares a single validated scale and sets its pointer/mode in mm_desc_ifc.
1961
+ Used for both operand scales (a,b,c,d) and epilog scales (epilog_aux).
1962
+ Assumes validation already done.
1963
+ """
1964
+ if scale is None:
1965
+ return
1966
+
1967
+ # Delegate to specific preparer (validation already done)
1968
+ if isinstance(scale, (int, float)):
1969
+ self._prepare_validated_scalar_scale(scale, operand, stream_holder)
1970
+ else:
1971
+ self._prepare_validated_tensor_scale(scale, operand, stream_holder)
1972
+
1973
+ # Set pointer and mode in descriptor
1974
+ setattr(self.mm_desc_ifc, f"{cublas_operand}_scale_pointer", self.quantization_scales_device[operand].data_ptr)
1975
+
1976
+ if self.options.block_scaling:
1977
+ # MXFP8 uses 32-element blocks with UE8M0, FP4 uses 16-element blocks with UE4M3
1978
+ if self.using_fp4_ab:
1979
+ # FP4: 16-element block scaling with UE4M3 format
1980
+ self.logger.debug(f"Using VEC16_UE4M3 scale mode for operand {operand.upper()}.")
1981
+ setattr(self.mm_desc_ifc, f"{cublas_operand}_scale_mode", cublaslt.MatmulMatrixScale.VEC16_UE4M3)
1982
+ else:
1983
+ # FP8: 32-element block scaling with UE8M0 format
1984
+ self.logger.debug(f"Using VEC32_UE8M0 scale mode for operand {operand.upper()}.")
1985
+ setattr(self.mm_desc_ifc, f"{cublas_operand}_scale_mode", cublaslt.MatmulMatrixScale.VEC32_UE8M0)
1986
+ else:
1987
+ self.logger.debug(f"Using SCALAR_32F scale mode for operand {operand.upper()}.")
1988
+ setattr(self.mm_desc_ifc, f"{cublas_operand}_scale_mode", cublaslt.MatmulMatrixScale.SCALAR_32F)
1989
+
1990
+ def _prepare_operand_quantization_scales(self, scales, stream_holder: utils.StreamHolder):
1991
+ """
1992
+ Prepares validated operand scales (a,b,c,d).
1993
+ Assumes scales are validated and wrapped into a MatmulQuantizationScales object.
1994
+ """
1995
+ for operand in "abcd":
1996
+ scale = getattr(scales, operand)
1997
+ self._prepare_single_validated_scale(scale, operand, cublas_operand=operand, stream_holder=stream_holder)
1998
+
1999
+ @utils.precondition(_check_valid_matmul)
2000
+ @utils.atomic(_free_plan_resources, method=True)
2001
+ def plan(
2002
+ self, *, preferences=None, algorithms=None, epilog=None, epilog_inputs=None, stream: utils.AnyStream | int | None = None
2003
+ ): # Epilog inputs require as many inputs (with specific shapes etc) as required by the epilogue. It's a dict.
2004
+ """
2005
+ Plan the matrix multiplication operation, considering the epilog (if provided).
2006
+
2007
+ Args:
2008
+ preferences: {preferences}
2009
+
2010
+ algorithms: {algorithms}
2011
+
2012
+ epilog: {epilog}
2013
+
2014
+ epilog_inputs: {epilog_inputs}
2015
+
2016
+ stream: {stream}
2017
+
2018
+ Returns:
2019
+ A sequence of :class:`nvmath.linalg.advanced.Algorithm` objects that are
2020
+ applicable to this matrix multiplication problem specification, heuristically
2021
+ ordered from fastest to slowest.
2022
+
2023
+ Notes:
2024
+ Epilogs that have ``BIAS`` in their name need an epilog input with the key
2025
+ ``'bias'``. Epilogs that have ``DRELU`` need an epilog input with the key
2026
+ ``'relu_aux'``, which is produced in a "forward pass" epilog like ``RELU_AUX``
2027
+ or ``RELU_AUX_BIAS``. Similarly, epilogs with ``DGELU`` in their name require an
2028
+ epilog input with the key ``'gelu_aux'``, produced in the corresponding forward
2029
+ pass operation.
2030
+
2031
+ Examples:
2032
+
2033
+ >>> import numpy as np
2034
+ >>> import nvmath
2035
+
2036
+ Create two 3-D float64 ndarrays on the CPU representing batched matrices, along
2037
+ with a bias vector:
2038
+
2039
+ >>> batch = 32
2040
+ >>> M, N, K = 1024, 1024, 1024
2041
+ >>> a = np.random.rand(batch, M, K)
2042
+ >>> b = np.random.rand(batch, K, N)
2043
+ >>> # The bias vector will be broadcast along the columns, as well as along the
2044
+ >>> # batch dimension.
2045
+ >>> bias = np.random.rand(M)
2046
+
2047
+ We will define a matrix multiplication operation followed by a
2048
+ :attr:`nvmath.linalg.advanced.MatmulEpilog.RELU_BIAS` epilog function.
2049
+
2050
+ >>> with nvmath.linalg.advanced.Matmul(a, b) as mm:
2051
+ ... # Plan the operation with RELU_BIAS epilog and corresponding epilog
2052
+ ... # input.
2053
+ ... p = nvmath.linalg.advanced.MatmulPlanPreferences(limit=8)
2054
+ ... epilog = nvmath.linalg.advanced.MatmulEpilog.RELU_BIAS
2055
+ ... epilog_inputs = {{"bias": bias}}
2056
+ ... # The preferences can also be provided as a dict: {{'limit': 8}}
2057
+ ... algorithms = mm.plan(
2058
+ ... preferences=p,
2059
+ ... epilog=epilog,
2060
+ ... epilog_inputs=epilog_inputs,
2061
+ ... )
2062
+ ...
2063
+ ... # Execute the matrix multiplication, and obtain the result `r` as a
2064
+ ... # NumPy ndarray.
2065
+ ... r = mm.execute()
2066
+
2067
+ Some epilogs like :attr:`nvmath.linalg.advanced.MatmulEpilog.RELU_AUX` produce
2068
+ auxiliary output.
2069
+
2070
+ >>> with nvmath.linalg.advanced.Matmul(a, b) as mm:
2071
+ ... # Plan the operation with RELU_AUX epilog>
2072
+ ... epilog = nvmath.linalg.advanced.MatmulEpilog.RELU_AUX
2073
+ ... algorithms = mm.plan(epilog=epilog)
2074
+ ...
2075
+ ... # Execute the matrix multiplication, and obtain the result `r` along
2076
+ ... # with the auxiliary output.
2077
+ ... r, auxiliary = mm.execute()
2078
+
2079
+ The auxiliary output is a Python `dict` with the names of each auxiliary output
2080
+ as keys.
2081
+
2082
+ Further examples can be found in the `nvmath/examples/linalg/advanced/matmul
2083
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/linalg/advanced/matmul>`_
2084
+ directory.
2085
+ """
2086
+ log_info = self.logger.isEnabledFor(logging.INFO)
2087
+
2088
+ self.logger.info("= PLANNING PHASE =")
2089
+
2090
+ # Clear epilog operands, since different epilogs can be provided in different calls.
2091
+ # We don't need to worry about ordering, since it's the user's responsibility to
2092
+ # order calls that accept a stream argument. This applies to CPU operands as well,
2093
+ # even though we move them to the GPU, since the execution is blocking.
2094
+ self.epilog_operands = {} # Clear operands in case of repeated planning.
2095
+ self.epilog_input_name_to_handler = {} # Clear input name to handler map as well,
2096
+ self.epilog_inputs_traits = {} # ... and the input traits as well.
2097
+
2098
+ preferences = utils.check_or_create_options(
2099
+ _configuration.MatmulPlanPreferences, preferences, "Matrix multiplication plan preferences"
2100
+ )
2101
+ self.preferences = preferences
2102
+
2103
+ mm_traits = self.mm_traits
2104
+
2105
+ stream_holder = utils.get_or_create_stream(self.device_id, stream, self.package)
2106
+ self.logger.info(f"The specified stream for the matrix multiplication plan is {stream_holder.obj}.")
2107
+
2108
+ # Base FLOP count.
2109
+ self.flop_count = 2 * mm_traits.M * mm_traits.N * mm_traits.K
2110
+ self.logger.info(f"The base matrix multiplication FLOP count is {formatters.FLOPSStr(self.flop_count, 'FLOP')}.")
2111
+
2112
+ if epilog is None and epilog_inputs is not None:
2113
+ self.logger.warning(
2114
+ f"Matmul: The provided epilog inputs {epilog_inputs.keys()} are ignored since an epilog is not specified."
2115
+ )
2116
+
2117
+ self.epilog = epilog
2118
+ epilog_ordering = None
2119
+ if epilog is not None:
2120
+ assert epilog in EPILOG_INPUT_HANDLERS_MAP, "Not supported."
2121
+ self.logger.info(f"The specified epilog is {epilog.name}.")
2122
+
2123
+ epilog_minimum_versions = EPILOG_MINIMUM_VERSIONS_MAP[epilog]
2124
+ batched_epilog_minimum_versions = BATCHED_EPILOG_MINIMUM_VERSIONS_MAP[epilog]
2125
+ version = cublaslt.get_version()
2126
+ if version < epilog_minimum_versions["cublaslt"]:
2127
+ message = (
2128
+ f"The epilog {epilog.name} requires cublaslt >= {epilog_minimum_versions['cublaslt']}; "
2129
+ f"you have version {version}. Update to CUDA Toolkit >= {epilog_minimum_versions['ctk']}."
2130
+ )
2131
+ raise ValueError(message)
2132
+
2133
+ if len(mm_traits.batch_shape) > 0 and version < batched_epilog_minimum_versions["cublaslt"]:
2134
+ message = (
2135
+ f"The epilog {epilog.name} supports batching in "
2136
+ f"cublaslt >= {batched_epilog_minimum_versions['cublaslt']}; "
2137
+ f"you have version {version}. Update to CUDA Toolkit >= {epilog_minimum_versions['ctk']}."
2138
+ )
2139
+ raise ValueError(message)
2140
+ if (
2141
+ self.mm_traits.c_layout_traits is not None
2142
+ and self.mm_traits.c_layout_traits.order == cublaslt.Order.ROW
2143
+ and epilog
2144
+ in [
2145
+ cublaslt.Epilogue.BGRADA,
2146
+ cublaslt.Epilogue.BGRADB,
2147
+ ]
2148
+ ):
2149
+ msg = f"The epilog {epilog.name} requires input matrix 'c' to be F-contiguous (column-major)."
2150
+ raise ValueError(msg)
2151
+ if (
2152
+ version < 120804
2153
+ # A has one row
2154
+ and self.mm_traits.M == 1
2155
+ # C is broadcast
2156
+ and self.mm_traits.c_layout_traits is not None
2157
+ and self.mm_traits.c_layout_traits.ld == 0
2158
+ # Using both an bias epilog and C
2159
+ and self.epilog & _configuration.MatmulEpilog.BIAS > 0
2160
+ ):
2161
+ message = (
2162
+ "When matrix 'a' has one row, "
2163
+ "simultaneously broadcasting matrix 'c' and using a BIAS epilog requires cublaslt >= 120804; "
2164
+ f"You have version {version}. Update to CUDA Toolkit >= 12.8.1."
2165
+ )
2166
+ raise ValueError(message)
2167
+
2168
+ # Take a copy of the user-provided inputs.
2169
+ if epilog_inputs is not None:
2170
+ epilog_inputs = epilog_inputs.copy()
2171
+ else:
2172
+ epilog_inputs = {}
2173
+
2174
+ # Get the dtype of auxiliary buffer
2175
+ aux_dtype_name = (
2176
+ typemaps.DATA_TYPE_TO_NAME[self.preferences.epilog.aux_type] # type: ignore[attr-defined]
2177
+ if self.preferences.epilog.aux_type is not None # type: ignore[attr-defined]
2178
+ else None
2179
+ )
2180
+
2181
+ # Extract aux quantization scale from the inputs.
2182
+ aux_quantization_scale = epilog_inputs.pop("aux_quantization_scale", None)
2183
+ self._validate_epilog_aux_scale(aux_quantization_scale, required=True)
2184
+ self._prepare_single_validated_scale(
2185
+ aux_quantization_scale, "epilog_aux", cublas_operand="epilogue_aux", stream_holder=stream_holder
2186
+ )
2187
+
2188
+ epilog_input_handler_types = EPILOG_INPUT_HANDLERS_MAP[epilog]
2189
+ if epilog_input_handler_types:
2190
+ epilog_input_handlers = [
2191
+ handler_type(self.logger, mm_traits, epilog, self.c_dtype_name, self.d_dtype_name, aux_dtype_name)
2192
+ for handler_type in epilog_input_handler_types
2193
+ ]
2194
+
2195
+ # Check if the epilog requires a specific result layout, and if the
2196
+ # requirement is consistent for all the handlers.
2197
+ epilog_input_handlers_ordering = {h.order for h in epilog_input_handlers}
2198
+ assert len(epilog_input_handlers_ordering) == 1, "Internal error."
2199
+ epilog_ordering = epilog_input_handlers_ordering.pop()
2200
+
2201
+ required_epilog_input_names = {h.name for h in epilog_input_handlers}
2202
+
2203
+ self.logger.info(f"The epilog requires the following additional inputs: {required_epilog_input_names}.")
2204
+ if required_epilog_input_names != set(epilog_inputs.keys()):
2205
+ raise ValueError(
2206
+ f"The epilog {epilog.name} requires the following input tensors: "
2207
+ f"{required_epilog_input_names}. The provided tensor names are: {epilog_inputs.keys()}"
2208
+ )
2209
+
2210
+ # Wrap epilog inputs.
2211
+ for name in epilog_inputs:
2212
+ epilog_inputs[name] = tensor_wrapper.wrap_operand(epilog_inputs[name])
2213
+
2214
+ # Check if epilog inputs all belong to the same package, which is the same
2215
+ # as the package of the MM operands.
2216
+ epilog_package = utils.get_operands_package(list(epilog_inputs.values()))
2217
+ epilog_package = "cuda" if epilog_package == "numpy" else epilog_package # Handle the NumPy <=> CuPy asymmetry.
2218
+ if self.package != epilog_package:
2219
+ message = f"Library package mismatch for epilog: '{self.package}' => '{epilog_package}'"
2220
+ raise TypeError(message)
2221
+
2222
+ # When we get here, epilog_inputs should only contain tensors because
2223
+ # we popped aux_quantization_scale from the dictionary above.
2224
+ assert all(isinstance(v, tensor_wrapper.TensorHolder) for v in epilog_inputs.values()), "Internal error."
2225
+ # Since all epilog inputs are tensors, we can ensure they all are on
2226
+ # the same memory space as the operands provided at initialization,
2227
+ # as per the documentation for the epilog_inputs parameter.
2228
+ device_id = utils.get_operands_device_id(list(epilog_inputs.values()))
2229
+ expected_device = "cpu" if self.memory_space == "cpu" else self.device_id
2230
+ if device_id != expected_device:
2231
+ raise ValueError(
2232
+ f"The epilog inputs must be in the same memory space as the operands. "
2233
+ f"Expected device '{expected_device}' (operands' memory space is '{self.memory_space}'), "
2234
+ f"but epilog inputs are on device '{device_id}'."
2235
+ )
2236
+
2237
+ # Move epilog inputs to the GPU, if needed.
2238
+ if device_id == "cpu":
2239
+ for e in required_epilog_input_names:
2240
+ self.logger.debug(f"The epilog input {e} will be copied to device{self.device_id}.")
2241
+ self.epilog_operands[e] = epilog_inputs[e].to(self.device_id, stream_holder)
2242
+ else:
2243
+ for e in required_epilog_input_names:
2244
+ self.epilog_operands[e] = epilog_inputs[e]
2245
+
2246
+ # First validate all epilog inputs. Use the GPU tensors in case metadata has
2247
+ # changed.
2248
+ for handler in epilog_input_handlers:
2249
+ handler.validate(epilog_inputs[handler.name])
2250
+
2251
+ # Finally, update the MM descriptor. Note that we pass in
2252
+ # self.epilog_operands (which are on the GPU).
2253
+ for handler in epilog_input_handlers:
2254
+ handler.update(self.mm_desc_ifc, self.epilog_operands[handler.name])
2255
+ self.epilog_input_name_to_handler[handler.name] = handler
2256
+
2257
+ # Capture the epilog operands traits for consistency checks when resetting
2258
+ # operands.
2259
+ self.epilog_inputs_traits = {
2260
+ name: EpilogInputTraits(
2261
+ dtype=self.epilog_operands[name].dtype,
2262
+ extents=self.epilog_operands[name].shape,
2263
+ strides=self.epilog_operands[name].strides,
2264
+ )
2265
+ for name in self.epilog_operands
2266
+ }
2267
+
2268
+ epilog_output_handler_types = EPILOG_OUTPUT_HANDLERS_MAP[epilog]
2269
+ if epilog_output_handler_types:
2270
+ self.epilog_output_handlers = epilog_output_handlers = [
2271
+ handler_type(self.logger, mm_traits, epilog, self.c_dtype_name, self.d_dtype_name, aux_dtype_name)
2272
+ for handler_type in epilog_output_handler_types
2273
+ ]
2274
+ # Check if the epilog requires a specific result layout, and if the
2275
+ # requirement is consistent for all the handlers.
2276
+ epilog_output_handlers_ordering = {h.order for h in epilog_output_handlers}
2277
+ assert len(epilog_output_handlers_ordering) == 1, "Internal error."
2278
+ op_epilog_ordering = epilog_output_handlers_ordering.pop()
2279
+ if epilog_ordering is None:
2280
+ epilog_ordering = op_epilog_ordering
2281
+ else:
2282
+ assert epilog_ordering == op_epilog_ordering, "Internal error."
2283
+
2284
+ # Update the MM descriptor, except for the device pointer.
2285
+ for ohandler in epilog_output_handlers:
2286
+ ohandler.update(self.mm_desc_ifc)
2287
+
2288
+ # Set the epilog. At this point, we're sure that the epilog inputs, if any, are
2289
+ # valid and have been set.
2290
+ self.mm_desc_ifc.epilogue = epilog
2291
+
2292
+ # Fill the result traits, now that we know the epilog.
2293
+ self.result_traits = result_traits = get_result_traits(mm_traits, epilog_ordering, self.logger) # type: ignore[assignment]
2294
+ assert self.result_traits is not None, "Internal Error. self.result_traits should have been set by self.plan()."
2295
+ self.logger.info(
2296
+ f"The layout order for the result D is {self.result_traits.d_layout_traits.order.name}, with LD "
2297
+ f"{self.result_traits.d_layout_traits.ld}, and batch offset "
2298
+ f"{self.result_traits.d_layout_traits.batch_offset}."
2299
+ )
2300
+
2301
+ # Internally transpose operand A if required (conjugate flag) and create layout.
2302
+ transpose = False
2303
+ if mm_traits.a_layout_traits.is_conjugate and self.is_complex:
2304
+ self.mm_desc_ifc.transa = cublas.Operation.C
2305
+ transpose = True
2306
+ self.logger.debug(
2307
+ "To conjugate A, the operand A will be internally transposed and the matrix multiplication will be "
2308
+ "performed with OP_C for operand A."
2309
+ )
2310
+ if self.input_type_width <= 8:
2311
+ # narrow-precision (FP8 and lower) data types are only supported for transa=OP_T
2312
+ self.mm_desc_ifc.transa = cublas.Operation.T
2313
+ transpose = True
2314
+ self.logger.debug(
2315
+ "For narrow-precision (FP8 and lower) multiplication, the operand A will be internally transposed and the "
2316
+ "matrix multiplication will be performed with OP_T for operand A."
2317
+ )
2318
+ m, n, ld, a_order = mm_traits.a_layout_traits.get_mm_layout(transpose=transpose)
2319
+ self.a_layout_ptr = cublaslt.matrix_layout_create(self.a_dtype, rows=m, cols=n, ld=ld)
2320
+ self.logger.debug(f"Layout for A: rows = {m}, cols = {n}, ld = {ld}.")
2321
+
2322
+ # Internally transpose operand B if required (conjugate flag, or epilog is BGRADB)
2323
+ # and create layout.
2324
+ transpose = False
2325
+ if mm_traits.b_layout_traits.is_conjugate and self.is_complex:
2326
+ self.mm_desc_ifc.transb = cublas.Operation.C
2327
+ transpose = True
2328
+ self.logger.debug(
2329
+ "To conjugate B, the operand B will be internally transposed and the matrix multiplication will be "
2330
+ "performed with OP_C for operand B."
2331
+ )
2332
+ elif epilog == _configuration.MatmulEpilog.BGRADB:
2333
+ self.mm_desc_ifc.transb = cublas.Operation.T
2334
+ transpose = True
2335
+ self.logger.debug(
2336
+ "For BGRADB epilog, the operand B will be internally transposed and the matrix multiplication will be "
2337
+ "performed with OP_T for operand B."
2338
+ )
2339
+ m, n, ld, b_order = mm_traits.b_layout_traits.get_mm_layout(transpose=transpose)
2340
+ self.b_layout_ptr = cublaslt.matrix_layout_create(self.b_dtype, rows=m, cols=n, ld=ld)
2341
+ self.logger.debug(f"Layout for B: rows = {m}, cols = {n}, ld = {ld}.")
2342
+
2343
+ self.d_layout_ptr = cublaslt.matrix_layout_create(
2344
+ self.d_dtype, rows=mm_traits.M, cols=mm_traits.N, ld=result_traits.d_layout_traits.ld
2345
+ )
2346
+
2347
+ layout_a_ifc = matrix_layout_ifc.MatrixLayoutInterface(self.a_layout_ptr)
2348
+ layout_a_ifc.order = a_order
2349
+ layout_a_ifc.batch_count = mm_traits.batch_count
2350
+ layout_a_ifc.strided_batch_offset = mm_traits.a_layout_traits.batch_offset
2351
+
2352
+ layout_b_ifc = matrix_layout_ifc.MatrixLayoutInterface(self.b_layout_ptr)
2353
+ layout_b_ifc.order = b_order
2354
+ layout_b_ifc.batch_count = mm_traits.batch_count
2355
+ layout_b_ifc.strided_batch_offset = mm_traits.b_layout_traits.batch_offset
2356
+
2357
+ layout_d_ifc = matrix_layout_ifc.MatrixLayoutInterface(self.d_layout_ptr)
2358
+ layout_d_ifc.order = result_traits.d_layout_traits.order
2359
+ layout_d_ifc.batch_count = mm_traits.batch_count
2360
+ layout_d_ifc.strided_batch_offset = result_traits.d_layout_traits.batch_offset
2361
+
2362
+ if self.num_operands == 2: # By defn, this cannot be inplace.
2363
+ if self.c_dtype == self.d_dtype:
2364
+ # If C and D have equal types, reuse the layout.
2365
+ self.c_layout_ptr = self.d_layout_ptr
2366
+ else:
2367
+ # Otherwise, create a D-like layout, but with different type.
2368
+ self.c_layout_ptr = cublaslt.matrix_layout_create(
2369
+ self.c_dtype, rows=mm_traits.M, cols=mm_traits.N, ld=result_traits.d_layout_traits.ld
2370
+ )
2371
+ layout_c_ifc = matrix_layout_ifc.MatrixLayoutInterface(self.c_layout_ptr)
2372
+ layout_c_ifc.order = result_traits.d_layout_traits.order
2373
+ layout_c_ifc.batch_count = mm_traits.batch_count
2374
+ layout_c_ifc.strided_batch_offset = result_traits.d_layout_traits.batch_offset
2375
+ else:
2376
+ # For inplace operation, use the same layout for C and D.
2377
+ if self.inplace:
2378
+ self.c_layout_ptr = self.d_layout_ptr
2379
+ else:
2380
+ self.c_layout_ptr = cublaslt.matrix_layout_create(
2381
+ self.c_dtype, rows=mm_traits.M, cols=mm_traits.N, ld=mm_traits.c_layout_traits.ld
2382
+ )
2383
+ layout_c_ifc = matrix_layout_ifc.MatrixLayoutInterface(self.c_layout_ptr)
2384
+ layout_c_ifc.order = mm_traits.c_layout_traits.order
2385
+ layout_c_ifc.batch_count = mm_traits.batch_count
2386
+ layout_c_ifc.strided_batch_offset = mm_traits.c_layout_traits.batch_offset
2387
+
2388
+ # FP8 block scaling dimension requirements
2389
+ if (
2390
+ self.input_type_width == 8
2391
+ and self.options.block_scaling
2392
+ and (mm_traits.M % 128 != 0 or mm_traits.N % 128 != 0 or mm_traits.K % 128 != 0)
2393
+ ):
2394
+ raise ValueError(f"M={mm_traits.M} N={mm_traits.N} K={mm_traits.K} must be divisible by 128 for FP8 block_scaling.")
2395
+
2396
+ # Note: FP4 block scaling dimension requirements (M,N % 128, K % 64) are checked
2397
+ # earlier in __init__ via validate_fp4_ab_dimensions().
2398
+
2399
+ # General FP8 alignment
2400
+ if self.input_type_width == 8 and (mm_traits.M % 16 != 0 or mm_traits.N % 16 != 0 or mm_traits.K % 16 != 0):
2401
+ raise ValueError(f"M={mm_traits.M} N={mm_traits.N} K={mm_traits.K} must be divisible by 16 for FP8 operations")
2402
+
2403
+ # General FP4 alignment
2404
+ if self.using_fp4_ab and (mm_traits.M % 16 != 0 or mm_traits.N % 16 != 0 or mm_traits.K % 16 != 0):
2405
+ raise ValueError(f"M={mm_traits.M} N={mm_traits.N} K={mm_traits.K} must be divisible by 16 for FP4 operations")
2406
+
2407
+ if self.options.block_scaling and self.d_dtype_width <= 8: # FP8 and FP4
2408
+ self.mm_desc_ifc.alpha_vector_batch_stride = 1 # Workaround for library caching issue
2409
+
2410
+ # cublasLtMatmulAlgoGetHeuristic requires the scale pointer to be set.
2411
+ num_output_elements = mm_traits.M * mm_traits.N * mm_traits.batch_count
2412
+ d_out_scale, d_out_scale_mode = _create_d_out_scale_and_scale_mode(
2413
+ self.result_class, num_output_elements, self.using_fp4_ab, self.device_id, stream_holder
2414
+ )
2415
+ self.aux_outputs = {"d_out_scale": d_out_scale}
2416
+ self.mm_desc_ifc.d_out_scale_pointer = d_out_scale.data_ptr
2417
+ self.mm_desc_ifc.d_out_scale_mode = d_out_scale_mode
2418
+
2419
+ limit = preferences.limit
2420
+ if algorithms is None:
2421
+ num_algorithms = np.empty((1,), dtype=np.int32)
2422
+ self.algorithms_buffer = cublaslt.MatmulHeuristicResult(limit)
2423
+ else:
2424
+ assert all(isinstance(algo, _algorithmmod.Algorithm) for algo in algorithms), (
2425
+ "The algorithms passed to plan() are of wrong type."
2426
+ )
2427
+ num_algorithms = len(algorithms)
2428
+
2429
+ if self.preference_ptr is None:
2430
+ self.preference_ptr = cublaslt.matmul_preference_create()
2431
+ else:
2432
+ # We need to create a new preferences object to avoid preferences being set in a
2433
+ # cumulative manner if plan() is called multiple times.
2434
+ cublaslt.matmul_preference_destroy(self.preference_ptr)
2435
+ self.preference_ptr = cublaslt.matmul_preference_create()
2436
+
2437
+ if self.input_type_width <= 8:
2438
+ self._prepare_operand_quantization_scales(self.quantization_scales, stream_holder)
2439
+
2440
+ if algorithms is None:
2441
+ # Set preferences.
2442
+ preference_ifc = matmul_pref_ifc.MatmulPreferenceInterface(self.preference_ptr)
2443
+ preference_ifc.max_workspace_bytes = self.memory_limit
2444
+ preference_ifc.reduction_scheme_mask = preferences.reduction_scheme_mask
2445
+ preference_ifc.max_waves_count = preferences.max_waves_count
2446
+ preference_ifc.impl_mask = preferences.numerical_impl_mask
2447
+
2448
+ # Set minimum alignments.
2449
+ a_ptr, b_ptr = self.operands[0].data_ptr, self.operands[1].data_ptr
2450
+ preference_ifc.min_alignment_a_bytes = min(256, pointer_aligned_to(a_ptr))
2451
+ preference_ifc.min_alignment_b_bytes = min(256, pointer_aligned_to(b_ptr))
2452
+ self.logger.debug(f"The minimum alignment for operand A is {preference_ifc.min_alignment_a_bytes} bytes.")
2453
+ self.logger.debug(f"The minimum alignment for operand B is {preference_ifc.min_alignment_b_bytes} bytes.")
2454
+ if self.num_operands == 3:
2455
+ c_ptr = self.operands[2].data_ptr
2456
+ preference_ifc.min_alignment_c_bytes = min(256, pointer_aligned_to(c_ptr))
2457
+ self.logger.debug(f"The minimum alignment for operand C is {preference_ifc.min_alignment_c_bytes} bytes.")
2458
+ # The result alignment should be 256 bytes.
2459
+ self.logger.debug("The minimum alignment for the result D is the default 256 bytes.")
2460
+
2461
+ self.logger.info("Starting matrix multiplication planning...")
2462
+ assert isinstance(self.device_id, int), self.device_id
2463
+ assert stream_holder is not None
2464
+ with utils.cuda_call_ctx(stream_holder, blocking=True, timing=log_info) as (
2465
+ _,
2466
+ elapsed,
2467
+ ):
2468
+ cublaslt.matmul_algo_get_heuristic(
2469
+ self.handle,
2470
+ self.mm_desc,
2471
+ self.a_layout_ptr,
2472
+ self.b_layout_ptr,
2473
+ self.c_layout_ptr,
2474
+ self.d_layout_ptr,
2475
+ self.preference_ptr,
2476
+ limit,
2477
+ self.algorithms_buffer.ptr,
2478
+ num_algorithms.ctypes.data,
2479
+ )
2480
+
2481
+ num_algorithms = num_algorithms[0]
2482
+ if num_algorithms == 0:
2483
+ raise RuntimeError("Planning failed to find any suitable algorithm.")
2484
+ assert self.algorithms_buffer is not None, (
2485
+ "Internal Error. self.algorithms_buffer should have been set by self.plan()."
2486
+ )
2487
+ self.algorithms_buffer = self.algorithms_buffer[:num_algorithms]
2488
+
2489
+ # Create algorithm objects.
2490
+ self.algorithm_objects = tuple(_algorithmmod.Algorithm(a) for a in self.algorithms_buffer)
2491
+ else:
2492
+ self.algorithm_objects = tuple(algorithms)
2493
+ self.algorithms_buffer = cublaslt.MatmulHeuristicResult(len(algorithms))
2494
+ for i, algo in enumerate(algorithms):
2495
+ # we wrap it too well that it's hard to copy-construct...
2496
+ self.algorithms_buffer[i] = algo.algorithm._data
2497
+
2498
+ # Cache the first (best) algorithm struct.
2499
+ self.cached_best_algorithm_struct = self.algorithms_buffer[0]["algo"]
2500
+
2501
+ # Create the map from object to buffer.
2502
+ self.algorithm_object_to_buffer = dict(zip(self.algorithm_objects, self.algorithms_buffer, strict=True))
2503
+
2504
+ self.workspace_size = int(np.max(self.algorithms_buffer["workspace_size"]))
2505
+ if self.workspace_size > 0 and self.epilog:
2506
+ self.workspace_size += 16 # Workaround for library issue
2507
+
2508
+ if algorithms is None:
2509
+ self.logger.info(
2510
+ f"The plan found {num_algorithms} suitable algorithms within the requested limit of {limit} "
2511
+ f"algorithms, with a workspace requirement of {formatters.MemoryStr(self.workspace_size)}."
2512
+ )
2513
+ else:
2514
+ self.logger.info(
2515
+ f"The plan is using {num_algorithms} algorithm passed through the algorithms argument, with a "
2516
+ f"workspace requirement of {formatters.MemoryStr(self.workspace_size)}."
2517
+ )
2518
+
2519
+ self.mm_planned = True
2520
+ if algorithms is None and elapsed.data is not None:
2521
+ self.logger.info(f"The matrix multiplication planning phase took {elapsed.data:.3f} ms to complete.")
2522
+
2523
+ return self.algorithm_objects
2524
+
2525
+ @property
2526
+ def algorithms(self):
2527
+ """
2528
+ After planning using :meth:`plan()`, get the sequence of algorithm objects to
2529
+ inquire their capabilities, configure them, or serialize them for later use.
2530
+
2531
+ Returns:
2532
+ A sequence of :class:`nvmath.linalg.advanced.Algorithm` objects that are
2533
+ applicable to this matrix multiplication problem specification.
2534
+ """
2535
+ return self.algorithm_objects
2536
+
2537
+ def _check_and_set_operand(
2538
+ self,
2539
+ operand,
2540
+ operand_name,
2541
+ mm_desc_ifc,
2542
+ stream_holder,
2543
+ *,
2544
+ operand_index=None,
2545
+ epilog_name=None,
2546
+ package=None,
2547
+ dtype=None,
2548
+ extents=None,
2549
+ strides=None,
2550
+ ):
2551
+ """
2552
+ Check to make sure that the provided operand is consistent with the one it's
2553
+ updating, and update it.
2554
+ """
2555
+ assert (operand_index is None) ^ (epilog_name is None), "Internal Error."
2556
+ assert self.operands is not None, "Internal Error."
2557
+
2558
+ # Make sure that the data type and extents match.
2559
+ utils.check_attribute_match(dtype, operand.dtype, "data type")
2560
+ utils.check_attribute_match(extents, operand.shape, "extents")
2561
+
2562
+ package = utils.infer_object_package(operand.tensor)
2563
+
2564
+ # Conjugate flag of the provided operands must match the original qualifiers
2565
+ if operand_index is not None and self.lazy_conjugation[operand_index] != operand.is_conjugate:
2566
+ raise ValueError(f"The provided operand {operand_name} has different conjugate flag than the original operand")
2567
+
2568
+ device_id = operand.device_id
2569
+ # When memory_space is "cpu", operands should be on CPU even though
2570
+ # self.device_id is the execution device.
2571
+ expected_device_id = "cpu" if self.memory_space == "cpu" else self.device_id
2572
+ if expected_device_id != device_id:
2573
+ raise ValueError(
2574
+ f'The operand {operand_name} is on device "{device_id}", but it should be on device '
2575
+ f'"{expected_device_id}" to match the original operand.'
2576
+ )
2577
+
2578
+ if device_id == "cpu":
2579
+ package = "cuda" if package == "numpy" else package # Handle the NumPy <=> CuPy asymmetry.
2580
+ if self.package != package:
2581
+ message = f"Library package mismatch: '{self.package}' => '{package}'"
2582
+ raise TypeError(message)
2583
+
2584
+ # Check if we have a GPU buffer to update into.
2585
+ if operand_index is not None:
2586
+ o = self.operands[operand_index]
2587
+ else:
2588
+ o = self.epilog_operands[epilog_name]
2589
+ if o is None: # No buffer, create one.
2590
+ # Copy operand across memory spaces (CPU to GPU).
2591
+ o = operand.to(self.device_id, stream_holder)
2592
+ if operand_index is not None:
2593
+ self.operands[operand_index] = o
2594
+ else:
2595
+ self.epilog_operands[epilog_name] = o
2596
+ # Update the epilog pointer, since we're starting afresh.
2597
+ self.epilog_input_name_to_handler[epilog_name].update(mm_desc_ifc, o)
2598
+ else:
2599
+ # In-place copy to existing device pointer because the new operand is on the
2600
+ # CPU.
2601
+ o.copy_(operand, stream_holder=stream_holder)
2602
+ if self.memory_space == "cpu" and operand_index == 2:
2603
+ # Hold references, needed for inplace operations.
2604
+ self.cpu_c_ref = operand
2605
+ else:
2606
+ if self.package != package:
2607
+ message = f"Library package mismatch: '{self.package}' => '{package}'"
2608
+ raise TypeError(message)
2609
+
2610
+ utils.check_attribute_match(strides, operand.strides, "strides")
2611
+
2612
+ # Finally, replace the original operand by the new one.
2613
+ if operand_index is not None:
2614
+ self.operands[operand_index] = operand
2615
+ else:
2616
+ self.epilog_operands[epilog_name] = operand
2617
+ # Update the epilog pointer, since we're starting afresh.
2618
+ self.epilog_input_name_to_handler[epilog_name].update(mm_desc_ifc, operand)
2619
+
2620
+ self.logger.info(f"Operand '{operand_name}' has been reset to the new value.")
2621
+
2622
+ return
2623
+
2624
+ def _reset_quantization_scales_unchecked_from_host(
2625
+ self,
2626
+ quantization_scales: _configuration.MatmulQuantizationScales,
2627
+ stream_holder: utils.StreamHolder[utils.AnyStream],
2628
+ ):
2629
+ """(Private method) Unchecked method to reset quantization residing on the CPU.
2630
+ This method updates quantization scales for operands A, B, C, and D.
2631
+
2632
+ Args:
2633
+ quantization_scales: MatmulQuantizationScales object
2634
+ containing new scale values for operands. Can contain scalars,
2635
+ single-element tensors (for tensor-wide scaling),
2636
+ or properly shaped uint8 tensors (for block scaling).
2637
+
2638
+ stream_holder: Stream holder for GPU operations.
2639
+
2640
+ This method updates the stored scale values in `self.quantization_scales`
2641
+ and the device-side tensor wrappers in `self.quantization_scales_device`.
2642
+ It also updates the cuBLAS descriptor pointers via `self.mm_desc_ifc`.
2643
+ """
2644
+ # Note: when updating the aux scale, we only need to update the "data",
2645
+ # but the scale_mode does not need to be update since it is unchanged
2646
+ # from initialization. This is guaranteed by reset_operands_unchecked
2647
+ # requirement for the newly provided operands to match the
2648
+ # original operands' properties.
2649
+
2650
+ qsd = self.quantization_scales_device
2651
+ mm_desc_ifc = self.mm_desc_ifc
2652
+
2653
+ def _wrap_and_copy(scale):
2654
+ if isinstance(scale, (int, float)):
2655
+ return tensor_wrapper.wrap_operand(np.asarray(scale, dtype="float32")).to(self.device_id, stream_holder)
2656
+ return tensor_wrapper.wrap_operand(scale).to(self.device_id, stream_holder)
2657
+
2658
+ if quantization_scales.a is not None:
2659
+ host_scale = quantization_scales.a
2660
+ self.quantization_scales.a = host_scale
2661
+ holder = qsd["a"] = _wrap_and_copy(host_scale)
2662
+ mm_desc_ifc.set_a_scale_pointer_unchecked(holder.data_ptr)
2663
+
2664
+ if quantization_scales.b is not None:
2665
+ host_scale = quantization_scales.b
2666
+ self.quantization_scales.b = host_scale
2667
+ holder = qsd["b"] = _wrap_and_copy(host_scale)
2668
+ mm_desc_ifc.set_b_scale_pointer_unchecked(holder.data_ptr)
2669
+
2670
+ if quantization_scales.c is not None:
2671
+ host_scale = quantization_scales.c
2672
+ self.quantization_scales.c = host_scale
2673
+ holder = qsd["c"] = _wrap_and_copy(host_scale)
2674
+ mm_desc_ifc.set_c_scale_pointer_unchecked(holder.data_ptr)
2675
+
2676
+ if quantization_scales.d is not None:
2677
+ host_scale = quantization_scales.d
2678
+ self.quantization_scales.d = host_scale
2679
+ holder = qsd["d"] = _wrap_and_copy(host_scale)
2680
+ mm_desc_ifc.set_d_scale_pointer_unchecked(holder.data_ptr)
2681
+
2682
+ def _reset_aux_quantization_scale_unchecked(
2683
+ self, aux_quantization_scale, stream: utils.AnyStream | int | None, stream_holder: utils.StreamHolder | None
2684
+ ):
2685
+ """(Private method) Unchecked method to reset aux_quantization_scale.
2686
+
2687
+ Args:
2688
+ aux_quantization_scale: The aux quantization scale (scalar or tensor).
2689
+ stream: The stream for GPU operations (may be None).
2690
+ stream_holder: The stream holder (may be None, will be created if needed).
2691
+
2692
+ Returns:
2693
+ The stream_holder (either the input or newly created one).
2694
+
2695
+ Notes:
2696
+ - to minimize overhead, the stream logic is as follows:
2697
+ - if the operation does not need a stream, then `stream_holder` is not used.
2698
+ - if the operation needs to use a stream, and `stream_holder` is None,
2699
+ then a stream holder is created from `stream` and `self.package`.
2700
+ - if the operation needs to use a stream, and `stream_holder`
2701
+ is not None, then it is used directly.
2702
+ """
2703
+ # Note: when updating the aux scale, we only need to update the "data",
2704
+ # but the scale_mode does not need to be update since it is unchanged
2705
+ # from initialization. This is guaranteed by reset_operands_unchecked
2706
+ # requirement for the newly provided operands to match the
2707
+ # original operands' properties.
2708
+
2709
+ if isinstance(aux_quantization_scale, (int, float)):
2710
+ # Scalar scales always need stream holder because we need to do a CPU->GPU copy.
2711
+ if stream_holder is None:
2712
+ stream_holder = utils.get_or_create_stream(self.device_id, stream, self.package)
2713
+ scale_op = tensor_wrapper.wrap_operand(np.asarray(aux_quantization_scale, dtype="float32"))
2714
+ self.quantization_scales_device["epilog_aux"] = scale_op.to(self.device_id, stream_holder)
2715
+ self.mm_desc_ifc.epilogue_aux_scale_pointer = self.quantization_scales_device["epilog_aux"].data_ptr
2716
+ else:
2717
+ # aux_quantization_scale is a tensor
2718
+ member_tensor_holder = self.quantization_scales_device["epilog_aux"]
2719
+ if self.memory_space == "cuda":
2720
+ member_tensor_holder.tensor = aux_quantization_scale
2721
+ self.mm_desc_ifc.epilogue_aux_scale_pointer = member_tensor_holder.data_ptr
2722
+ else:
2723
+ # wrap and copy to GPU
2724
+ if stream_holder is None:
2725
+ stream_holder = utils.get_or_create_stream(self.device_id, stream, self.package)
2726
+ aux_quantization_scale_wrapped = tensor_wrapper.wrap_operand(aux_quantization_scale)
2727
+ member_tensor_holder = aux_quantization_scale_wrapped.to(self.device_id, stream_holder)
2728
+ self.mm_desc_ifc.epilogue_aux_scale_pointer = member_tensor_holder.data_ptr
2729
+
2730
+ return stream_holder
2731
+
2732
+ def _reset_quantization_scales_unchecked_on_device(
2733
+ self,
2734
+ quantization_scales_obj: _configuration.MatmulQuantizationScales,
2735
+ stream: utils.AnyStream | int | None,
2736
+ stream_holder: utils.StreamHolder | None,
2737
+ ):
2738
+ """(Private method) Unchecked method to reset quantization residing on GPU.
2739
+ This method directly updates tensor references without wrapping overhead.
2740
+
2741
+ Args:
2742
+ quantization_scales_obj: Quantization scales object.
2743
+ stream_holder: Stream holder (may be None, will be created if needed).
2744
+
2745
+ Returns:
2746
+ The stream_holder (either the input or newly created one).
2747
+
2748
+ Notes:
2749
+ - to minimize overhead, the stream logic is as follows:
2750
+ - if the operation does not need a stream, then `stream_holder` is not used.
2751
+ - if the operation needs a stream, and `stream_holder` is None,
2752
+ then a stream holder is created from `stream` and `self.package`.
2753
+ - if the operation needs to use a stream, and `stream_holder` is
2754
+ not None, then it is used directly.
2755
+ """
2756
+ # Note: when updating the aux scale, we only need to update the "data",
2757
+ # but the scale_mode does not need to be update since it is unchanged
2758
+ # from initialization. This is guaranteed by reset_operands_unchecked
2759
+ # requirement for the newly provided operands to match the
2760
+ # original operands' properties.
2761
+
2762
+ mm_desc_ifc = self.mm_desc_ifc
2763
+
2764
+ def _update_scale(holder, scale):
2765
+ nonlocal stream_holder
2766
+ if isinstance(scale, (int, float)):
2767
+ if stream_holder is None:
2768
+ stream_holder = utils.get_or_create_stream(self.device_id, stream, self.package)
2769
+ holder.tensor = (
2770
+ tensor_wrapper.wrap_operand(np.asarray(scale, dtype="float32")).to(self.device_id, stream_holder).tensor
2771
+ )
2772
+ else:
2773
+ holder.tensor = scale
2774
+
2775
+ if quantization_scales_obj.a is not None:
2776
+ scale = quantization_scales_obj.a
2777
+ self.quantization_scales.a = scale
2778
+ holder = self.quantization_scales_device["a"]
2779
+ _update_scale(holder, scale)
2780
+ mm_desc_ifc.set_a_scale_pointer_unchecked(holder.data_ptr)
2781
+
2782
+ if quantization_scales_obj.b is not None:
2783
+ scale = quantization_scales_obj.b
2784
+ self.quantization_scales.b = scale
2785
+ holder = self.quantization_scales_device["b"]
2786
+ _update_scale(holder, scale)
2787
+ mm_desc_ifc.set_b_scale_pointer_unchecked(holder.data_ptr)
2788
+
2789
+ if quantization_scales_obj.c is not None:
2790
+ scale = quantization_scales_obj.c
2791
+ self.quantization_scales.c = scale
2792
+ holder = self.quantization_scales_device["c"]
2793
+ _update_scale(holder, scale)
2794
+ mm_desc_ifc.set_c_scale_pointer_unchecked(holder.data_ptr)
2795
+
2796
+ if quantization_scales_obj.d is not None:
2797
+ scale = quantization_scales_obj.d
2798
+ self.quantization_scales.d = scale
2799
+ holder = self.quantization_scales_device["d"]
2800
+ _update_scale(holder, scale)
2801
+ mm_desc_ifc.set_d_scale_pointer_unchecked(holder.data_ptr)
2802
+
2803
+ return stream_holder
2804
+
2805
+ def reset_operands_unchecked(
2806
+ self,
2807
+ *,
2808
+ a=None,
2809
+ b=None,
2810
+ c=None,
2811
+ alpha=None,
2812
+ beta=None,
2813
+ quantization_scales=None,
2814
+ epilog_inputs=None,
2815
+ stream: utils.AnyStream | int | None = None,
2816
+ ):
2817
+ """
2818
+ {reset_operands_unchecked}
2819
+ """
2820
+
2821
+ # After release_operands(), the TensorHolder wrappers of the operands are
2822
+ # still alive since only .tensor was cleared. When the operands' memory space
2823
+ # is the same as the execution space (i.e. CUDA), the code below can reuse
2824
+ # them directly.
2825
+ # This is not the case for CPU memory space, where device mirrors
2826
+ # need re-allocation. Rather than duplicating all the non-trivial
2827
+ # re-initialization logic here, which would add significant overhead and,
2828
+ # therefore, undo the performance benefits of this method, we delegate
2829
+ # the first reset after a release to reset_operands(); subsequent resets
2830
+ # take the fast path below.
2831
+ if self._operands_released and self.memory_space == "cpu":
2832
+ self.reset_operands(
2833
+ a=a,
2834
+ b=b,
2835
+ c=c,
2836
+ alpha=alpha,
2837
+ beta=beta,
2838
+ quantization_scales=quantization_scales,
2839
+ epilog_inputs=epilog_inputs,
2840
+ stream=stream,
2841
+ )
2842
+ return
2843
+
2844
+ # Deal with alpha, beta which are scalars and we don't need the stream holder.
2845
+ if alpha is not None:
2846
+ self.alpha[0] = alpha
2847
+
2848
+ if beta is not None:
2849
+ self.beta[0] = beta
2850
+
2851
+ # Stream holder optimization: Initialize to None and create lazily
2852
+ # only when needed. The `stream` parameter is None or the user-provided CUDA stream
2853
+ # The `stream_holder` is the internal wrapper
2854
+ # around the stream that we reuse across multiple operations
2855
+ # in this method to avoid redundant wrapper creation overhead.
2856
+ # Each method that needs a stream holder will:
2857
+ # - Create one if stream_holder is None (lazy creation)
2858
+ # - Return the stream_holder so subsequent operations can reuse it
2859
+ # This is better than creating a new one for each operation,
2860
+ # which is useful when needs to be reused across multiple operations (e.g.,
2861
+ # copying scalar quantization scales, copying CPU operands to GPU, etc.).
2862
+ # This approach is a good compromise between stream manipulation overhead
2863
+ # and code readability, since it allows us to keep the code below
2864
+ # quite separated into different methods. One alternative would be to simplify
2865
+ # the logic of stream holder creation but that would imply having a
2866
+ # single giant method, making the code more complex and hard to reason about.
2867
+ stream_holder = None
2868
+
2869
+ # The "unchecked" contract means the caller guarantees that new operands
2870
+ # match the original operands' memory space. We exploit this below by
2871
+ # using the self.memory_space attribute (set in __init__) to determine
2872
+ # the code path without inspecting individual operands:
2873
+ #
2874
+ # - If self.memory_space == "cuda":
2875
+ # All tensor operands are guaranteed to already be on GPU.
2876
+ # We directly update references avoiding wrapping overhead
2877
+ # and stream holder creation (unless needed for scalar scales).
2878
+ #
2879
+ # - If self.memory_space == "cpu":
2880
+ # All tensor operands are on CPU and must be copied to GPU.
2881
+ # We create a stream holder upfront and wrap+copy each operand
2882
+ # to the execution device.
2883
+
2884
+ # Handle aux_quantization_scale first (needs special handling because of the
2885
+ # particular set of strings that are used to identify/store this internally).
2886
+ if epilog_inputs is not None and "aux_quantization_scale" in epilog_inputs:
2887
+ aux_quantization_scale = epilog_inputs.get("aux_quantization_scale")
2888
+ stream_holder = self._reset_aux_quantization_scale_unchecked(aux_quantization_scale, stream, stream_holder)
2889
+
2890
+ # Convert quantization scales to object if needed
2891
+ quantization_scales_obj = quantization_scales
2892
+ if quantization_scales is not None and isinstance(quantization_scales, dict):
2893
+ quantization_scales_obj = _configuration.MatmulQuantizationScales(**quantization_scales)
2894
+
2895
+ # Branch based on memory space
2896
+ if self.memory_space == "cuda":
2897
+ # Update a, b, c operands by directly updating tensor references
2898
+ if a is not None:
2899
+ self.operands[0].tensor = a # type: ignore[index, union-attr]
2900
+ if b is not None:
2901
+ self.operands[1].tensor = b # type: ignore[index, union-attr]
2902
+ if c is not None:
2903
+ self.operands[2].tensor = c # type: ignore[index, union-attr]
2904
+
2905
+ # Handle epilog inputs (except aux_quantization_scale handled earlier).
2906
+ # The reset_operands_* contract guarantees that operand properties
2907
+ # (shape, strides, dtype) are unchanged, so only the data pointer needs
2908
+ # updating in the cuBLASLt descriptor while all other attributes
2909
+ # (batch stride, leading dimension, data type) remain valid from before.
2910
+ if epilog_inputs is not None:
2911
+ for epilog_name, epilog_value in epilog_inputs.items():
2912
+ if epilog_name != "aux_quantization_scale":
2913
+ self.epilog_operands[epilog_name].tensor = epilog_value
2914
+ self.epilog_input_name_to_handler[epilog_name].update_pointer(
2915
+ self.mm_desc_ifc, self.epilog_operands[epilog_name].data_ptr
2916
+ )
2917
+
2918
+ # handle quantization scales (ignore returned stream_holder, not needed anymore)
2919
+ if quantization_scales_obj is not None:
2920
+ self._reset_quantization_scales_unchecked_on_device(quantization_scales_obj, stream, stream_holder)
2921
+ else:
2922
+ # Create stream holder (required for CPU->GPU copies)
2923
+ if stream_holder is None:
2924
+ stream_holder = utils.get_or_create_stream(self.device_id, stream, self.package)
2925
+
2926
+ # Wrap and copy a, b, c operands to GPU
2927
+ if a is not None:
2928
+ a_wrapped = tensor_wrapper.wrap_operand(a)
2929
+ self.operands[0] = a_wrapped.to(self.device_id, stream_holder) # type: ignore[index]
2930
+ if b is not None:
2931
+ b_wrapped = tensor_wrapper.wrap_operand(b)
2932
+ self.operands[1] = b_wrapped.to(self.device_id, stream_holder) # type: ignore[index]
2933
+ if c is not None:
2934
+ c_wrapped = tensor_wrapper.wrap_operand(c)
2935
+ self.operands[2] = c_wrapped.to(self.device_id, stream_holder) # type: ignore[index]
2936
+ # For inplace operations with CPU operands,
2937
+ # hold reference to the CPU operand.
2938
+ if self.inplace:
2939
+ self.cpu_c_ref = c_wrapped
2940
+
2941
+ # Handle epilog inputs (except aux_quantization_scale which was handled earlier)
2942
+ if epilog_inputs is not None:
2943
+ for epilog_name, epilog_value in epilog_inputs.items():
2944
+ if epilog_name != "aux_quantization_scale":
2945
+ epilog_input_wrapped = tensor_wrapper.wrap_operand(epilog_value)
2946
+ self.epilog_operands[epilog_name] = epilog_input_wrapped.to(self.device_id, stream_holder)
2947
+ self.epilog_input_name_to_handler[epilog_name].update(
2948
+ self.mm_desc_ifc, self.epilog_operands[epilog_name]
2949
+ )
2950
+
2951
+ # handle quantization scales
2952
+ if quantization_scales_obj is not None:
2953
+ self._reset_quantization_scales_unchecked_from_host(quantization_scales_obj, stream_holder)
2954
+
2955
+ self._operands_released = False
2956
+
2957
+ @utils.precondition(_check_valid_matmul)
2958
+ def reset_operands(
2959
+ self,
2960
+ *,
2961
+ a=None,
2962
+ b=None,
2963
+ c=None,
2964
+ alpha=None,
2965
+ beta=None,
2966
+ quantization_scales=None,
2967
+ epilog_inputs=None,
2968
+ stream: utils.AnyStream | int | None = None,
2969
+ ):
2970
+ """
2971
+ Reset one or more operands held by this :class:`Matmul` instance. Only the operands
2972
+ explicitly passed are updated; omitted operands retain their current values.
2973
+
2974
+ This method will perform various checks on the new operands to make sure:
2975
+
2976
+ - The shapes, strides, datatypes match those of the old ones.
2977
+ - The packages that the operands belong to match those of the old ones.
2978
+ - If input tensors are on GPU, the device must match.
2979
+
2980
+ .. versionchanged:: 0.9
2981
+ All parameters are now keyword-only.
2982
+
2983
+ Args:
2984
+ a: {a}
2985
+
2986
+ b: {b}
2987
+
2988
+ c: {c}
2989
+ {c_admonitions}
2990
+
2991
+ alpha: {alpha}
2992
+
2993
+ beta: {beta}
2994
+
2995
+ epilog_inputs: {epilog_inputs}
2996
+
2997
+ stream: {stream}
2998
+
2999
+ quantization_scales: {quantization_scales}
3000
+
3001
+ Examples:
3002
+
3003
+ >>> import cupy as cp
3004
+ >>> import nvmath
3005
+
3006
+ Create two 3-D float64 ndarrays on the GPU:
3007
+
3008
+ >>> M, N, K = 128, 128, 256
3009
+ >>> a = cp.random.rand(M, K)
3010
+ >>> b = cp.random.rand(K, N)
3011
+
3012
+ Create an matrix multiplication object as a context manager
3013
+
3014
+ >>> with nvmath.linalg.advanced.Matmul(a, b) as mm:
3015
+ ... # Plan the operation.
3016
+ ... algorithms = mm.plan()
3017
+ ...
3018
+ ... # Execute the MM to get the first result.
3019
+ ... r1 = mm.execute()
3020
+ ...
3021
+ ... # Reset the operands to new CuPy ndarrays.
3022
+ ... a_new = cp.random.rand(M, K)
3023
+ ... b_new = cp.random.rand(K, N)
3024
+ ... mm.reset_operands(a=a_new, b=b_new)
3025
+ ...
3026
+ ... # Execute to get the new result corresponding to the updated operands.
3027
+ ... r2 = mm.execute()
3028
+
3029
+ With :meth:`reset_operands`, minimal overhead is achieved as problem
3030
+ specification and planning are only performed once.
3031
+
3032
+ For the particular example above, the operands are on the GPU, so calling
3033
+ :meth:`reset_operands` only updates internal references and is efficient. An
3034
+ alternative would be to modify the existing operands in-place (e.g.
3035
+ ``a[:]=a_new`` and ``b[:]=b_new``), but that would copy data and have
3036
+ performance implications. When using in-place updates, the operand memory space
3037
+ must be accessible from the execution space.
3038
+
3039
+ For more details, please refer to `inplace update example
3040
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/linalg/advanced/matmul/example05_stateful_inplace.py>`_.
3041
+
3042
+ .. seealso::
3043
+ :meth:`release_operands`, :meth:`reset_operands_unchecked`
3044
+ """
3045
+
3046
+ if c is not None and self.num_operands == 2:
3047
+ raise ValueError(
3048
+ "The matrix multiplication problem specification does not include operand C, so it cannot be reset."
3049
+ )
3050
+
3051
+ # If operands have been released, all required operands must be provided
3052
+ if self._operands_released:
3053
+ # Check that all main operands are provided
3054
+ all_main_provided = a is not None and b is not None and (self.num_operands != 3 or c is not None)
3055
+
3056
+ # Check epilog_inputs if required
3057
+ epilog_names = self.epilog_inputs_traits.keys()
3058
+ epilog_ok = True
3059
+ if epilog_names:
3060
+ if epilog_inputs is None:
3061
+ epilog_ok = False
3062
+ elif epilog_names != epilog_inputs.keys():
3063
+ raise ValueError(
3064
+ f"The epilog inputs {epilog_names} are required. "
3065
+ f"The provided epilog input names are {epilog_inputs.keys()}."
3066
+ )
3067
+
3068
+ scales_ok = True
3069
+ needs_scales = self.input_type_width <= 8
3070
+ if needs_scales and quantization_scales is None:
3071
+ scales_ok = False
3072
+
3073
+ if not all_main_provided or not epilog_ok or not scales_ok:
3074
+ raise ValueError(
3075
+ "After release_operands(), all required operands must be provided to reset_operands(). "
3076
+ f"Required: a, b{', c' if self.num_operands == 3 else ''}"
3077
+ f"{', quantization_scales' if needs_scales else ''}"
3078
+ f"{', epilog_inputs' if epilog_names else ''}"
3079
+ )
3080
+
3081
+ # Initialize operands and epilog_operands to prepare for new values
3082
+ self.operands = [None] * self.num_operands # type: ignore[list-item]
3083
+ epilog_names = self.epilog_inputs_traits.keys()
3084
+ self.epilog_operands = dict.fromkeys(epilog_names)
3085
+ if needs_scales:
3086
+ self.quantization_scales = _configuration.MatmulQuantizationScales()
3087
+
3088
+ # Update alpha.
3089
+ if alpha is not None:
3090
+ try:
3091
+ self.alpha[0] = alpha
3092
+ except (ValueError, TypeError) as e:
3093
+ raise ValueError(
3094
+ f"The value provided for alpha {alpha} is not convertible to dtype '{self.alpha.dtype}'."
3095
+ ) from e
3096
+
3097
+ # Update beta.
3098
+ if beta is not None:
3099
+ if self.num_operands == 2:
3100
+ self.logger.warning(f"Matmul: The provided beta value {beta} is ignored since operand C is not specified.")
3101
+ else:
3102
+ try:
3103
+ self.beta[0] = beta
3104
+ except (ValueError, TypeError) as e:
3105
+ raise ValueError(
3106
+ f"The value provided for beta {beta} is not convertible to dtype '{self.beta.dtype}'."
3107
+ ) from e
3108
+
3109
+ stream_holder = utils.get_or_create_stream(self.device_id, stream, self.package)
3110
+
3111
+ # Update quantization_scales.
3112
+ if quantization_scales is not None:
3113
+ quantization_scales = self._validate_operand_scales(quantization_scales, all_required=False)
3114
+ if quantization_scales.a is not None:
3115
+ self.quantization_scales.a = quantization_scales.a
3116
+ if quantization_scales.b is not None:
3117
+ self.quantization_scales.b = quantization_scales.b
3118
+ if quantization_scales.c is not None:
3119
+ self.quantization_scales.c = quantization_scales.c
3120
+ if quantization_scales.d is not None:
3121
+ self.quantization_scales.d = quantization_scales.d
3122
+ self._prepare_operand_quantization_scales(self.quantization_scales, stream_holder)
3123
+
3124
+ if epilog_inputs is not None and "aux_quantization_scale" in epilog_inputs:
3125
+ epilog_inputs = epilog_inputs.copy()
3126
+ aux_quantization_scale = epilog_inputs.pop("aux_quantization_scale")
3127
+ self._validate_epilog_aux_scale(aux_quantization_scale, required=False)
3128
+ self._prepare_single_validated_scale(
3129
+ aux_quantization_scale, "epilog_aux", cublas_operand="epilogue_aux", stream_holder=stream_holder
3130
+ )
3131
+
3132
+ # Reset the provided operands.
3133
+ if a is not None:
3134
+ a = tensor_wrapper.wrap_operand(a)
3135
+ index = 0
3136
+ self._check_and_set_operand(
3137
+ a,
3138
+ "A",
3139
+ self.mm_desc_ifc,
3140
+ stream_holder,
3141
+ operand_index=index,
3142
+ dtype=self.a_dtype_name,
3143
+ extents=self.operand_extents[index],
3144
+ strides=self.operand_strides[index],
3145
+ )
3146
+
3147
+ if b is not None:
3148
+ b = tensor_wrapper.wrap_operand(b)
3149
+ index = 1
3150
+ self._check_and_set_operand(
3151
+ b,
3152
+ "B",
3153
+ self.mm_desc_ifc,
3154
+ stream_holder,
3155
+ operand_index=index,
3156
+ dtype=self.b_dtype_name,
3157
+ extents=self.operand_extents[index],
3158
+ strides=self.operand_strides[index],
3159
+ )
3160
+
3161
+ if c is not None: # If we get here, we know that C is one of the operands in the problem specification.
3162
+ c = tensor_wrapper.wrap_operand(c)
3163
+ index = 2
3164
+ self._check_and_set_operand(
3165
+ c,
3166
+ "C",
3167
+ self.mm_desc_ifc,
3168
+ stream_holder,
3169
+ operand_index=index,
3170
+ dtype=self.c_dtype_name,
3171
+ extents=self.operand_extents[index],
3172
+ strides=self.operand_strides[index],
3173
+ )
3174
+
3175
+ # Reset the provided epilog inputs.
3176
+ if epilog_inputs is not None:
3177
+ for name in epilog_inputs:
3178
+ epilog_input = tensor_wrapper.wrap_operand(epilog_inputs[name])
3179
+ self._check_and_set_operand(
3180
+ epilog_input,
3181
+ name,
3182
+ self.mm_desc_ifc,
3183
+ stream_holder,
3184
+ epilog_name=name,
3185
+ dtype=self.epilog_inputs_traits[name].dtype,
3186
+ extents=self.epilog_inputs_traits[name].extents,
3187
+ strides=self.epilog_inputs_traits[name].strides,
3188
+ )
3189
+
3190
+ self._operands_released = False
3191
+
3192
+ @utils.precondition(_check_valid_matmul)
3193
+ def release_operands(self):
3194
+ """
3195
+ {release_operands}
3196
+ """
3197
+
3198
+ # CUDA memory space:
3199
+ # self.operands, self.epilog_operands, and
3200
+ # self.quantization_scales_device hold direct user references;
3201
+ # self.quantization_scales holds the user-provided scales object.
3202
+ # CPU memory space:
3203
+ # self.operands, self.epilog_operands, and
3204
+ # self.quantization_scales_device hold internal device mirrors;
3205
+ # self.quantization_scales holds the user-provided scales object;
3206
+ # self.cpu_c_ref holds a direct reference to the user's CPU operand C.
3207
+ # In both cases, release all of them.
3208
+ # Note that if/when possible, we keep the TensorHolder wrappers and
3209
+ # container structures alive and only release the internal tensor reference.
3210
+ # This is useful when reset_operands_unchecked is called subsequently
3211
+ # because it can reuse the existing wrappers, saving overhead.
3212
+
3213
+ self.operands[0].tensor = None # A
3214
+ self.operands[1].tensor = None # B
3215
+ if self.num_operands == 3:
3216
+ self.operands[2].tensor = None # C
3217
+ for op in self.epilog_operands.values():
3218
+ op.tensor = None
3219
+ for op in self.quantization_scales_device.values():
3220
+ op.tensor = None
3221
+
3222
+ # For the quant scales, the attribute itself might not exist,
3223
+ # so we need to check for that first.
3224
+ if hasattr(self, "quantization_scales"):
3225
+ self.quantization_scales.a = None
3226
+ self.quantization_scales.b = None
3227
+ self.quantization_scales.c = None
3228
+ self.quantization_scales.d = None
3229
+
3230
+ if self.memory_space == "cpu" and self.cpu_c_ref is not None:
3231
+ self.cpu_c_ref.tensor = None
3232
+
3233
+ self._operands_released = True
3234
+ self.logger.info("User-provided operands have been released.")
3235
+
3236
+ @utils.precondition(_check_valid_matmul)
3237
+ @utils.precondition(_check_planned, "Autotuning")
3238
+ @utils.precondition(_check_valid_operands, "Autotuning")
3239
+ @utils.atomic(_release_workspace_memory_perhaps_wrapper, method=True)
3240
+ def autotune(
3241
+ self, iterations=10, prune=None, release_workspace=False, stream: utils.AnyStream | int | None = None
3242
+ ): # Prune means keep top N of the algorithms only.
3243
+ """
3244
+ Autotune the matrix multiplication to order the algorithms from the fastest measured
3245
+ execution time to the slowest. Once autotuned, the optimally-ordered algorithm
3246
+ sequence can be accessed using :py:attr:`algorithms`.
3247
+
3248
+ .. note::
3249
+ This function will benchmark each of the algorithms and order the algorithms
3250
+ based on the benchmark results. The measurements can be impacted by factors
3251
+ such as GPU temperature, clock settings, or power consumption. Autotuning
3252
+ in an unstable environment can result in a suboptimal algorithm ordering.
3253
+ If you experience performance problems, consider omitting the autotuning.
3254
+
3255
+ Args:
3256
+ iterations: The number of autotuning iterations to perform.
3257
+
3258
+ prune: An integer N, specifying the top N fastest algorithms to retain after
3259
+ autotuning. The default is to retain all algorithms.
3260
+
3261
+ release_workspace: {release_workspace}
3262
+
3263
+ stream: {stream}
3264
+ """
3265
+ self.logger.info("= AUTOTUNING PHASE =")
3266
+ # Measure time taken for autotuning.
3267
+ from timeit import default_timer as timer
3268
+
3269
+ self.logger.info("Starting autotuning...")
3270
+ start = timer()
3271
+
3272
+ assert self.algorithm_objects is not None, "Internal Error. self.algorithm_objects should have been set by self.plan()."
3273
+ num_algorithms = len(self.algorithm_objects)
3274
+ if num_algorithms == 1:
3275
+ self.logger.info("Skipping the autotuning, because only one algorithm has been returned in the planning phase.")
3276
+ return
3277
+ limit = min(prune, num_algorithms) if prune is not None else num_algorithms
3278
+ self.logger.info(
3279
+ f"The number of algorithms in the plan is {num_algorithms}, from which the top {limit} will be retained."
3280
+ )
3281
+ self.logger.info(f"The requested number of iterations is {iterations}.")
3282
+
3283
+ # Autotune setup.
3284
+ stream_holder = utils.get_or_create_stream(self.device_id, stream, self.package)
3285
+
3286
+ # Allocate workspace if needed.
3287
+ self._allocate_workspace_memory_perhaps(stream_holder)
3288
+
3289
+ # Create empty tensors for auxiliary output.
3290
+ epilog_outputs = {}
3291
+ for handler in self.epilog_output_handlers:
3292
+ name = handler.name
3293
+ shape, strides, dtype_name = handler.attributes()
3294
+ epilog_outputs[name] = aux = utils.create_empty_tensor(
3295
+ self.result_class,
3296
+ shape,
3297
+ dtype_name,
3298
+ self.device_id,
3299
+ stream_holder,
3300
+ verify_strides=False,
3301
+ strides=strides,
3302
+ )
3303
+
3304
+ # Update the data pointer in the MM descriptor.
3305
+ handler.update_ptr(self.mm_desc_ifc, aux.data_ptr)
3306
+
3307
+ # Take a copy of `c` for autotuning to avoid clobbering it if inplace=True.
3308
+ c = None
3309
+ if self.inplace:
3310
+ # `c` is guaranteed to exist. Clone `c` and copy into it, since `tensor.to`
3311
+ # returns the original tensor when the device ID is the same.
3312
+ c = self.operands[2]
3313
+ c = utils.create_empty_tensor(
3314
+ c.__class__, c.shape, c.dtype, self.device_id, stream_holder, verify_strides=False, strides=c.strides
3315
+ )
3316
+ c.copy_(self.operands[2], stream_holder)
3317
+ assert c.data_ptr != self.operands[2].data_ptr, "Internal error."
3318
+ elif self.num_operands == 3:
3319
+ c = self.operands[2]
3320
+
3321
+ # Create empty tensor for the result, if the operation is not in-place.
3322
+ assert self.result_traits is not None, "Internal Error. self.result_traits should have been set by self.plan()."
3323
+ if self.inplace:
3324
+ result = c
3325
+ else:
3326
+ result = utils.create_empty_tensor(
3327
+ self.result_class,
3328
+ self.result_traits.result_shape,
3329
+ self.d_dtype_name,
3330
+ self.device_id,
3331
+ stream_holder,
3332
+ verify_strides=False,
3333
+ strides=self.result_traits.result_strides,
3334
+ )
3335
+ result_ptr = result.data_ptr
3336
+
3337
+ a, b = self.operands[0], self.operands[1]
3338
+ raw_workspace_ptr = utils.get_ptr_from_memory_pointer(self.workspace_ptr)
3339
+ alpha_ptr, a_ptr, b_ptr, beta_ptr = self.alpha.ctypes.data, a.data_ptr, b.data_ptr, self.beta.ctypes.data
3340
+ # If `c` is not provided, set c_ptr to nullptr.
3341
+ if self.num_operands == 3:
3342
+ c_ptr = c.data_ptr
3343
+ else:
3344
+ assert self.beta[0] == 0.0, "Internal error: beta must be zero if `c` is not specified."
3345
+ c_ptr = 0
3346
+
3347
+ def execute_matmul(algorithm_ptr):
3348
+ cublaslt.matmul(
3349
+ self.handle,
3350
+ self.mm_desc,
3351
+ alpha_ptr,
3352
+ a_ptr,
3353
+ self.a_layout_ptr,
3354
+ b_ptr,
3355
+ self.b_layout_ptr,
3356
+ beta_ptr,
3357
+ c_ptr,
3358
+ self.c_layout_ptr,
3359
+ result_ptr,
3360
+ self.d_layout_ptr,
3361
+ algorithm_ptr,
3362
+ raw_workspace_ptr,
3363
+ self.workspace_size,
3364
+ stream_holder.ptr,
3365
+ )
3366
+
3367
+ def flush_cache():
3368
+ """
3369
+ Write data to a temporary buffer to flush the L2 cache.
3370
+ """
3371
+
3372
+ @functools.cache
3373
+ def get_l2_cache_size(device_id):
3374
+ device = Device(device_id)
3375
+ return device.properties.l2_cache_size
3376
+
3377
+ l2_cache_size = get_l2_cache_size(self.device_id)
3378
+ cpu_buffer = np.zeros(l2_cache_size, dtype=np.uint8)
3379
+ tensor_wrapper.wrap_operand(cpu_buffer).to(device_id=self.device_id, stream_holder=stream_holder)
3380
+
3381
+ # Tune.
3382
+ with utils.cuda_call_ctx(stream_holder, blocking=False, timing=False) as (
3383
+ self.last_compute_event,
3384
+ _,
3385
+ ):
3386
+ gpu_times = np.empty(shape=(len(self.algorithms_buffer), iterations), dtype=float)
3387
+ algorithm_idxs = list(range(len(self.algorithms_buffer)))
3388
+ timing_enabled_options = EventOptions(enable_timing=True)
3389
+ start0 = stream_holder.obj.device.create_event(options=timing_enabled_options)
3390
+ end0 = stream_holder.obj.device.create_event(options=timing_enabled_options)
3391
+ for i in range(iterations):
3392
+ random.shuffle(algorithm_idxs)
3393
+ for algorithm_idx in algorithm_idxs:
3394
+ algorithm_ptr = self.algorithms_buffer[algorithm_idx]["algo"].ctypes.data
3395
+ stream_holder.obj.record(start0)
3396
+ execute_matmul(algorithm_ptr=algorithm_ptr)
3397
+ stream_holder.obj.record(end0)
3398
+ # FIXME: @dching Calling sync() here could slow down tuning by forcing
3399
+ # the device to wait for the host to record the elapsed time. It may be
3400
+ # faster to store references to 2 * len(iterations) *
3401
+ # len(algorithms_buffer) Events and compute the elapsed time at the end.
3402
+ end0.sync()
3403
+ gpu_times[algorithm_idx, i] = end0 - start0
3404
+ # Avoid potential overflow for inplace operations.
3405
+ if self.inplace:
3406
+ c.copy_(self.operands[2], stream_holder)
3407
+ flush_cache()
3408
+
3409
+ gpu_times = np.median(gpu_times, axis=1)
3410
+
3411
+ # Establish ordering wrt the computation and free workspace if requested.
3412
+ self._release_workspace_memory_perhaps(release_workspace=release_workspace)
3413
+ self._reset_workspace_allocation_tracking()
3414
+
3415
+ # Get the sort order based on the GPU times.
3416
+ sorted_gpu_times, sort_order = zip(*sorted(zip(gpu_times, range(num_algorithms), strict=True)), strict=True)
3417
+
3418
+ # Reorder the algorithms buffer and algorithm objects according to the sort order,
3419
+ # and prune it.
3420
+ sort_order = sort_order[:limit]
3421
+ self.algorithms_buffer = self.algorithms_buffer[list(sort_order)]
3422
+ self.algorithm_objects = tuple(self.algorithm_objects[i] for i in sort_order)
3423
+
3424
+ # Update cached first (best) algorithm struct after tuning.
3425
+ self.cached_best_algorithm_struct = self.algorithms_buffer[0]["algo"]
3426
+
3427
+ # Create the map from object to buffer.
3428
+ self.algorithm_object_to_buffer = dict(zip(self.algorithm_objects, self.algorithms_buffer, strict=True))
3429
+
3430
+ gpu_times_str = ", ".join(f"{t:0.3f}" for t in gpu_times)
3431
+ self.logger.info(f"The autotuned GPU times (in milliseconds) are: {gpu_times_str}.")
3432
+ self.logger.info(f"The corresponding sort order is: {sort_order}.")
3433
+ orig_flop_rate = self.flop_count / gpu_times[0] * 1000
3434
+ if sort_order[0] != 0:
3435
+ self.logger.info(
3436
+ f"Autotuning found that the algorithm originally ranked {sort_order[0]} is the best out of the "
3437
+ f"{num_algorithms} in the plan, and moved it to rank 0."
3438
+ )
3439
+ new_flop_rate = self.flop_count / sorted_gpu_times[0] * 1000
3440
+ self.logger.info(
3441
+ f"Autotuning has improved performance from {formatters.FLOPSStr(orig_flop_rate, 'FLOP/s')} to "
3442
+ f"{formatters.FLOPSStr(new_flop_rate, 'FLOP/s')}."
3443
+ )
3444
+ else:
3445
+ self.logger.info(
3446
+ f"Autotuning found that the algorithm ranked best by the plan heuristics remains the best out of the "
3447
+ f"{num_algorithms} algorithms in the plan."
3448
+ )
3449
+ self.logger.info(f"The best performance remains at {formatters.FLOPSStr(orig_flop_rate, 'FLOP/s')}.")
3450
+
3451
+ end = timer()
3452
+ self.logger.info(f"The autotuning took {(end - start) * 1000.0:.3f} ms to complete.")
3453
+
3454
+ def _create_result_tensor(self, stream_holder: utils.StreamHolder, log_debug: bool):
3455
+ """
3456
+ Create the output result tensor for the matmul operation.
3457
+
3458
+ We need special treatment for FP4 output (``float4_e2m1fn_x2``),
3459
+ because the logical shape ``(M, N)`` from ``result_traits`` is converted
3460
+ to a packed shape where one dimension is halved
3461
+ (since two FP4 values are packed per byte).
3462
+
3463
+ For all other dtypes, the logical shape is used directly.
3464
+ """
3465
+ assert self.result_traits is not None, (
3466
+ "Internal error: result_traits must be set by plan() before creating the result tensor."
3467
+ )
3468
+
3469
+ if self.d_dtype != CudaDataType.CUDA_R_4F_E2M1:
3470
+ # Non-FP4 output: no packing needed, use logical shape as-is.
3471
+ result_shape = self.result_traits.result_shape
3472
+ result_strides = self.result_traits.result_strides
3473
+
3474
+ if log_debug:
3475
+ self.logger.debug(
3476
+ f"The output tensor shape = {result_shape} with strides = "
3477
+ f"{result_strides} and data type '{self.d_dtype_name}'."
3478
+ )
3479
+
3480
+ self.result = utils.create_empty_tensor(
3481
+ self.result_class,
3482
+ result_shape,
3483
+ self.d_dtype_name,
3484
+ self.device_id,
3485
+ stream_holder,
3486
+ verify_strides=False,
3487
+ strides=result_strides,
3488
+ )
3489
+ return
3490
+
3491
+ # If we are here, we are dealing with FP4 output that must be packed.
3492
+ # Convert logical shape/strides to packed (physical) shape/strides.
3493
+ # Batching dimensions (leading) are unchanged.
3494
+ result_ndim = len(self.result_traits.result_shape)
3495
+ assert result_ndim >= 2, "Internal error: result must be at least 2D."
3496
+ if self.result_traits.d_layout_traits.order == cublaslt.Order.ROW:
3497
+ packed_axis = result_ndim - 1
3498
+ else:
3499
+ packed_axis = result_ndim - 2
3500
+ result_packed_shape = tuple(e if i != packed_axis else e // 2 for i, e in enumerate(self.result_traits.result_shape))
3501
+ result_packed_strides = tuple(
3502
+ s // 2 if i != packed_axis else s for i, s in enumerate(self.result_traits.result_strides)
3503
+ )
3504
+
3505
+ if log_debug:
3506
+ self.logger.debug(
3507
+ f"FP4 output: logical shape = {tuple(self.result_traits.result_shape)}, "
3508
+ f"logical strides = {tuple(self.result_traits.result_strides)} -> "
3509
+ f"packed shape = {result_packed_shape}, packed strides = {result_packed_strides}, "
3510
+ f"data type '{self.d_dtype_name}'."
3511
+ )
3512
+
3513
+ self.result = utils.create_empty_tensor(
3514
+ self.result_class,
3515
+ result_packed_shape,
3516
+ self.d_dtype_name,
3517
+ self.device_id,
3518
+ stream_holder,
3519
+ verify_strides=False,
3520
+ strides=result_packed_strides,
3521
+ )
3522
+
3523
+ @utils.precondition(_check_valid_matmul)
3524
+ @utils.precondition(_check_planned, "Execution")
3525
+ @utils.precondition(_check_valid_operands, "Execution")
3526
+ @utils.atomic(_release_workspace_memory_perhaps_wrapper, method=True)
3527
+ def execute(self, *, algorithm=None, release_workspace=False, stream: utils.AnyStream | int | None = None):
3528
+ """
3529
+ Execute a prepared (planned and possibly autotuned) matrix multiplication.
3530
+
3531
+ Args:
3532
+ algorithm: (Experimental) An algorithm chosen from the sequence returned by
3533
+ :meth:`plan` or :py:attr:`algorithms`. By default, the first algorithm in
3534
+ the sequence is used.
3535
+
3536
+ release_workspace: {release_workspace}
3537
+
3538
+ stream: {stream}
3539
+
3540
+ Returns:
3541
+ {result}
3542
+ """
3543
+ log_info = self.logger.isEnabledFor(logging.INFO)
3544
+ log_debug = self.logger.isEnabledFor(logging.DEBUG)
3545
+
3546
+ if log_info:
3547
+ self.logger.info("= EXECUTION PHASE =")
3548
+ stream_holder = utils.get_or_create_stream(self.device_id, stream, self.package)
3549
+ if log_info:
3550
+ self.logger.info(f"The specified stream for execute() is {stream_holder.obj}.")
3551
+
3552
+ # Allocate workspace if needed.
3553
+ self._allocate_workspace_memory_perhaps(stream_holder)
3554
+
3555
+ # Create empty tensors for auxiliary output.
3556
+ for handler in self.epilog_output_handlers:
3557
+ name = handler.name
3558
+ shape, strides, dtype_name = handler.attributes()
3559
+ if log_debug:
3560
+ self.logger.debug(f"Beginning auxiliary output tensor '{name}' creation...")
3561
+ self.logger.debug(f"The '{name}' tensor shape = {shape} with strides = {strides} and data type '{dtype_name}'.")
3562
+ self.epilog_outputs[name] = aux = utils.create_empty_tensor(
3563
+ self.result_class,
3564
+ shape,
3565
+ dtype_name,
3566
+ self.device_id,
3567
+ stream_holder,
3568
+ verify_strides=False,
3569
+ strides=strides,
3570
+ )
3571
+ if log_debug:
3572
+ self.logger.debug(f"The auxiliary output tensor '{name}' has been created.")
3573
+ if self.preferences.epilog.aux_amax: # type: ignore[attr-defined]
3574
+ if "float8" not in dtype_name:
3575
+ raise ValueError("epilog.aux_amax=True is not supported when epilog output type is not FP8.")
3576
+ self.epilog_outputs[f"{name}_amax"] = utils.create_empty_tensor(
3577
+ self.result_class,
3578
+ (1,),
3579
+ "float32", # This is the only type allowed by cuBLAS for AMAX.
3580
+ self.device_id,
3581
+ stream_holder,
3582
+ verify_strides=False,
3583
+ )
3584
+ self.mm_desc_ifc.epilogue_aux_amax_pointer = self.epilog_outputs[f"{name}_amax"].data_ptr
3585
+
3586
+ # Update the data pointer in the MM descriptor.
3587
+ handler.update_ptr(self.mm_desc_ifc, aux.data_ptr)
3588
+
3589
+ # Create empty tensor for the result, if the operation is not in-place.
3590
+ assert self.result_traits is not None, "Internal Error. self.result_traits should have been set by self.plan()"
3591
+ if self.inplace:
3592
+ if log_debug:
3593
+ self.logger.debug("The operation is in-place (operand C will be overwritten).")
3594
+ self.result = self.operands[2]
3595
+ else:
3596
+ if log_debug:
3597
+ self.logger.debug("Beginning output (empty) tensor creation...")
3598
+ self._create_result_tensor(stream_holder, log_debug)
3599
+ if log_debug:
3600
+ self.logger.debug("The output (empty) tensor has been created.")
3601
+
3602
+ self.aux_outputs = {}
3603
+
3604
+ if self.options.result_amax:
3605
+ self.aux_outputs["result_amax"] = utils.create_empty_tensor(
3606
+ self.result_class,
3607
+ (1,),
3608
+ "float32", # This is the only type allowed by cuBLAS for AMAX.
3609
+ self.device_id,
3610
+ stream_holder,
3611
+ verify_strides=False,
3612
+ )
3613
+ self.mm_desc_ifc.amax_d_pointer = self.aux_outputs["result_amax"].data_ptr
3614
+
3615
+ if self.options.block_scaling and self.d_dtype_width <= 8: # FP8 and FP4
3616
+ num_output_elements = (
3617
+ self.mm_traits.batch_count * self.result_traits.result_shape[-1] * self.result_traits.result_shape[-2]
3618
+ )
3619
+ d_out_scale, _ = _create_d_out_scale_and_scale_mode(
3620
+ self.result_class, num_output_elements, self.using_fp4_ab, self.device_id, stream_holder
3621
+ )
3622
+ self.aux_outputs["d_out_scale"] = d_out_scale
3623
+ self.mm_desc_ifc.d_out_scale_pointer = d_out_scale.data_ptr
3624
+
3625
+ # Select the first (best) algorithm if one is not provided.
3626
+ if algorithm is None:
3627
+ algorithm_struct = self.cached_best_algorithm_struct
3628
+ if log_info:
3629
+ self.logger.info(
3630
+ "The highest ranked algorithm in the plan (algorithm id = "
3631
+ f"{self.algorithm_objects[0].algorithm_id}) will be used."
3632
+ )
3633
+ else:
3634
+ if algorithm not in self.algorithm_objects:
3635
+ raise ValueError("Algorithm passed to execute() has to be included in the plan() algorithms")
3636
+ algorithm_struct = self.algorithm_object_to_buffer[algorithm]["algo"]
3637
+ if log_info:
3638
+ self.logger.info(f"The specified algorithm (algorithm id = {algorithm.algorithm_id}) will be used.")
3639
+
3640
+ a, b = self.operands[0], self.operands[1]
3641
+
3642
+ # If `c` is not provided, set c_ptr to nullptr.
3643
+ if self.num_operands == 3:
3644
+ c_ptr = self.operands[2].data_ptr
3645
+ else:
3646
+ assert self.beta[0] == 0.0, "Internal error: beta must be zero if `c` is not specified."
3647
+ c_ptr = 0
3648
+
3649
+ raw_workspace_ptr = utils.get_ptr_from_memory_pointer(self.workspace_ptr)
3650
+ if log_info:
3651
+ self.logger.info("Starting matrix multiplication...")
3652
+ self.logger.info(f"{self.call_prologue}")
3653
+ with utils.cuda_call_ctx(stream_holder, self.blocking, timing=log_info) as (
3654
+ self.last_compute_event,
3655
+ elapsed,
3656
+ ):
3657
+ cublaslt.matmul(
3658
+ self.handle,
3659
+ self.mm_desc,
3660
+ self.alpha.ctypes.data,
3661
+ a.data_ptr,
3662
+ self.a_layout_ptr,
3663
+ b.data_ptr,
3664
+ self.b_layout_ptr,
3665
+ self.beta.ctypes.data,
3666
+ c_ptr,
3667
+ self.c_layout_ptr,
3668
+ self.result.data_ptr,
3669
+ self.d_layout_ptr,
3670
+ algorithm_struct.ctypes.data,
3671
+ raw_workspace_ptr,
3672
+ self.workspace_size,
3673
+ stream_holder.ptr,
3674
+ )
3675
+
3676
+ if log_info and elapsed.data is not None:
3677
+ self.logger.info(f"The matrix multiplication calculation took {elapsed.data:.3f} ms to complete.")
3678
+
3679
+ # Establish ordering wrt the computation and free workspace if requested.
3680
+ if release_workspace:
3681
+ self._release_workspace_memory_perhaps(True)
3682
+
3683
+ # Return the result and auxiliary outputs, if present.
3684
+ all_outputs = self.epilog_outputs | self.aux_outputs
3685
+ if self.memory_space == "cpu":
3686
+ if self.inplace:
3687
+ # Overwrite operand C.
3688
+ assert self.cpu_c_ref is not None, "Internal error."
3689
+ c = self.cpu_c_ref
3690
+ c.copy_(self.result, stream_holder=stream_holder)
3691
+ out = c.tensor
3692
+ else:
3693
+ out = self.result.to("cpu", stream_holder=stream_holder).tensor
3694
+ # Copy auxiliary output to CPU.
3695
+ aux = {name: all_outputs[name].to("cpu", stream_holder=stream_holder).tensor for name in all_outputs}
3696
+ else:
3697
+ out = self.result.tensor
3698
+ # Return the unwrapped epilog output tensor(s).
3699
+ aux = {name: all_outputs[name].tensor for name in all_outputs}
3700
+
3701
+ # Release internal reference to the result to permit recycling of memory.
3702
+ self.result = None
3703
+ self.aux_outputs = {}
3704
+ self.epilog_outputs = {}
3705
+ self._reset_workspace_allocation_tracking()
3706
+
3707
+ if aux:
3708
+ return out, aux
3709
+
3710
+ return out
3711
+
3712
+ def free(self):
3713
+ """Free Matmul resources.
3714
+
3715
+ It is recommended that the :class:`Matmul` object be used within a context, but if
3716
+ it is not possible then this method must be called explicitly to ensure that the
3717
+ matrix multiplication resources (especially internal library objects) are properly
3718
+ cleaned up.
3719
+ """
3720
+
3721
+ if not self.valid_state:
3722
+ return
3723
+
3724
+ try:
3725
+ # Ensure ordering with respect to the last computation
3726
+ # to avoid race conditions when releasing internal resources.
3727
+ if self.last_compute_event is not None:
3728
+ if self.workspace_stream is not None:
3729
+ self.workspace_stream.wait(self.last_compute_event)
3730
+ self.last_compute_event = None
3731
+
3732
+ self._free_workspace_memory()
3733
+
3734
+ self._free_plan_resources()
3735
+
3736
+ # Destroy matmul descriptor.
3737
+ if self.mm_desc is not None:
3738
+ cublaslt.matmul_desc_destroy(self.mm_desc)
3739
+ self.mm_desc = None
3740
+
3741
+ # Note: self.handle is obtained via get_handle and doesn't need
3742
+ # explicit destruction, it's cached globally
3743
+ # and cleaned up at program exit.
3744
+ # Set all attributes to None except for logger and valid_state.
3745
+ _keep = {"logger", "valid_state"}
3746
+ for attr in list(vars(self)):
3747
+ if attr not in _keep:
3748
+ setattr(self, attr, None)
3749
+
3750
+ except Exception as e:
3751
+ self.logger.critical("Internal error: only part of the Matmul object's resources have been released.")
3752
+ self.logger.critical(str(e))
3753
+ raise e
3754
+ finally:
3755
+ self.valid_state = False
3756
+
3757
+ self.logger.info("The Matmul object's resources have been released.")
3758
+
3759
+
3760
+ @utils.docstring_decorator(SHARED_MM_DOCUMENTATION, skip_missing=False)
3761
+ def matmul(
3762
+ a,
3763
+ b,
3764
+ /,
3765
+ c=None,
3766
+ *,
3767
+ alpha=None,
3768
+ beta=None,
3769
+ epilog=None,
3770
+ epilog_inputs=None,
3771
+ qualifiers=None,
3772
+ quantization_scales=None,
3773
+ options=None,
3774
+ preferences=None,
3775
+ algorithm=None,
3776
+ stream: utils.AnyStream | int | None = None,
3777
+ ):
3778
+ """
3779
+ Perform the specified matrix multiplication computation :math:`F(\\alpha a @ b + \\beta
3780
+ c)`, where :math:`F` is the epilog. This function-form is a wrapper around the stateful
3781
+ :class:`Matmul` object APIs and is meant for *single* use (the user needs to perform
3782
+ just one matrix multiplication, for example), in which case there is no possibility of
3783
+ amortizing preparatory costs.
3784
+
3785
+ Detailed information on what's happening within this function can be obtained by passing
3786
+ in a :class:`logging.Logger` object to :class:`MatmulOptions` or by setting the
3787
+ appropriate options in the root logger object, which is used by default:
3788
+
3789
+ >>> import logging
3790
+ >>> logging.basicConfig(
3791
+ ... level=logging.INFO,
3792
+ ... format="%(asctime)s %(levelname)-8s %(message)s",
3793
+ ... datefmt="%m-%d %H:%M:%S",
3794
+ ... )
3795
+
3796
+ A user can select the desired logging level and, in general, take advantage of all of
3797
+ the functionality offered by the Python `logging` module.
3798
+
3799
+ Args:
3800
+ a: {a}
3801
+
3802
+ b: {b}
3803
+
3804
+ c: {c}
3805
+ {c_admonitions}
3806
+
3807
+ alpha: {alpha}
3808
+
3809
+ beta: {beta}
3810
+
3811
+ epilog: {epilog}
3812
+
3813
+ epilog_inputs: {epilog_inputs}
3814
+
3815
+ qualifiers: {qualifiers}
3816
+
3817
+ options: {options}
3818
+
3819
+ preferences: {preferences}
3820
+
3821
+ algorithm: An object of type :class:`Algorithm` objects can be directly provided to
3822
+ bypass planning, if desired. The algorithm object must be compatible with the
3823
+ matrix multiplication. A typical use for this option is to provide an algorithm
3824
+ that has been serialized (pickled) from a previously planned and autotuned
3825
+ matrix multiplication.
3826
+
3827
+ stream: {stream}
3828
+
3829
+ quantization_scales: {quantization_scales}
3830
+
3831
+ Returns:
3832
+ {result}
3833
+
3834
+ Semantics:
3835
+ {semantics}
3836
+
3837
+ Narrow-precision support:
3838
+ {narrow_precision}
3839
+
3840
+ .. seealso::
3841
+ :class:`Matmul`, :class:`MatmulOptions`, :class:`MatmulEpilog`,
3842
+ :class:`MatmulPlanPreferences`
3843
+
3844
+ Examples:
3845
+
3846
+ >>> import cupy as cp
3847
+ >>> import nvmath
3848
+
3849
+ Create three float32 ndarrays on the GPU:
3850
+
3851
+ >>> M, N, K = 128, 64, 256
3852
+ >>> a = cp.random.rand(M, K, dtype=cp.float32)
3853
+ >>> b = cp.random.rand(K, N, dtype=cp.float32)
3854
+ >>> c = cp.random.rand(M, N, dtype=cp.float32)
3855
+
3856
+ Perform the operation :math:`\\alpha A @ B + \\beta C` using :func:`matmul`. The
3857
+ result `r` is also a CuPy float32 ndarray:
3858
+
3859
+ >>> r = nvmath.linalg.advanced.matmul(a, b, c, alpha=1.23, beta=0.74)
3860
+
3861
+ An epilog can be used as well. Here we perform
3862
+ :math:`RELU(\\alpha A @ B + \\beta C)`:
3863
+
3864
+ >>> epilog = nvmath.linalg.advanced.MatmulEpilog.RELU
3865
+ >>> r = nvmath.linalg.advanced.matmul(a, b, c, alpha=1.23, beta=0.74, epilog=epilog)
3866
+
3867
+ Options can be provided to customize the operation:
3868
+
3869
+ >>> compute_type = nvmath.linalg.advanced.MatmulComputeType.COMPUTE_32F_FAST_TF32
3870
+ >>> o = nvmath.linalg.advanced.MatmulOptions(compute_type=compute_type)
3871
+ >>> r = nvmath.linalg.advanced.matmul(a, b, options=o)
3872
+
3873
+ See `MatmulOptions` for the complete list of available options.
3874
+
3875
+ The package current stream is used by default, but a stream can be explicitly
3876
+ provided to the Matmul operation. This can be done if the operands are computed on a
3877
+ different stream, for example:
3878
+
3879
+ >>> s = cp.cuda.Stream()
3880
+ >>> with s:
3881
+ ... a = cp.random.rand(M, K)
3882
+ ... b = cp.random.rand(K, N)
3883
+ >>> r = nvmath.linalg.advanced.matmul(a, b, stream=s)
3884
+
3885
+ The operation above runs on stream `s` and is ordered with respect to the input
3886
+ computation.
3887
+
3888
+ Create NumPy ndarrays on the CPU.
3889
+
3890
+ >>> import numpy as np
3891
+ >>> a = np.random.rand(M, K)
3892
+ >>> b = np.random.rand(K, N)
3893
+
3894
+ Provide the NumPy ndarrays to :func:`matmul`, with the result also being a NumPy
3895
+ ndarray:
3896
+
3897
+ >>> r = nvmath.linalg.advanced.matmul(a, b)
3898
+
3899
+ Notes:
3900
+ - This function is a convenience wrapper around :class:`Matmul` and is
3901
+ specifically meant for *single* use.
3902
+
3903
+ Further examples can be found in the `nvmath/examples/linalg/advanced/matmul
3904
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/linalg/advanced/matmul>`_
3905
+ directory.
3906
+ """
3907
+
3908
+ # Set algorithm limit to 1, but take a copy first if needed.
3909
+ if isinstance(preferences, _configuration.MatmulPlanPreferences):
3910
+ preferences = copy.copy(preferences)
3911
+
3912
+ preferences = utils.check_or_create_options(
3913
+ _configuration.MatmulPlanPreferences, preferences, "Matrix multiplication plan preferences"
3914
+ )
3915
+ preferences.limit = 1
3916
+
3917
+ if algorithm is None:
3918
+ algorithms = None
3919
+ else:
3920
+ algorithms = [algorithm] # The type of algorithm should be algorithm.Algorithm and will be checked in plan()
3921
+
3922
+ with Matmul(
3923
+ a,
3924
+ b,
3925
+ c=c,
3926
+ alpha=alpha,
3927
+ beta=beta,
3928
+ qualifiers=qualifiers,
3929
+ options=options,
3930
+ stream=stream,
3931
+ quantization_scales=quantization_scales,
3932
+ ) as mm:
3933
+ mm.plan(preferences=preferences, epilog=epilog, epilog_inputs=epilog_inputs, stream=stream, algorithms=algorithms)
3934
+
3935
+ r = mm.execute(stream=stream)
3936
+
3937
+ return r