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