nvmath-python 1.0.0__cp314-cp314t-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) 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 +130 -0
  7. nvmath/_internal/threadsafe.py +106 -0
  8. nvmath/_internal/utils.py +43 -0
  9. nvmath/_internal/workspace.py +490 -0
  10. nvmath/_utils.py +147 -0
  11. nvmath/bindings/__init__.py +60 -0
  12. nvmath/bindings/_internal/__init__.pxd +0 -0
  13. nvmath/bindings/_internal/__init__.py +0 -0
  14. nvmath/bindings/_internal/common_types.pxd +31 -0
  15. nvmath/bindings/_internal/cublas.cp314t-win_amd64.pyd +0 -0
  16. nvmath/bindings/_internal/cublas.pxd +530 -0
  17. nvmath/bindings/_internal/cublasLt.cp314t-win_amd64.pyd +0 -0
  18. nvmath/bindings/_internal/cublasLt.pxd +59 -0
  19. nvmath/bindings/_internal/cublasMp.pxd +52 -0
  20. nvmath/bindings/_internal/cudss.cp314t-win_amd64.pyd +0 -0
  21. nvmath/bindings/_internal/cudss.pxd +54 -0
  22. nvmath/bindings/_internal/cufft.cp314t-win_amd64.pyd +0 -0
  23. nvmath/bindings/_internal/cufft.pxd +70 -0
  24. nvmath/bindings/_internal/cufftMp.pxd +77 -0
  25. nvmath/bindings/_internal/curand.cp314t-win_amd64.pyd +0 -0
  26. nvmath/bindings/_internal/curand.pxd +42 -0
  27. nvmath/bindings/_internal/cusolver.cp314t-win_amd64.pyd +0 -0
  28. nvmath/bindings/_internal/cusolver.pxd +15 -0
  29. nvmath/bindings/_internal/cusolverDn.cp314t-win_amd64.pyd +0 -0
  30. nvmath/bindings/_internal/cusolverDn.pxd +406 -0
  31. nvmath/bindings/_internal/cusolverMp.pxd +71 -0
  32. nvmath/bindings/_internal/cusolverSp.cp314t-win_amd64.pyd +0 -0
  33. nvmath/bindings/_internal/cusolverSp.pxd +75 -0
  34. nvmath/bindings/_internal/cusparse.cp314t-win_amd64.pyd +0 -0
  35. nvmath/bindings/_internal/cusparse.pxd +471 -0
  36. nvmath/bindings/_internal/cusparseLt.cp314t-win_amd64.pyd +0 -0
  37. nvmath/bindings/_internal/cusparseLt.pxd +48 -0
  38. nvmath/bindings/_internal/cutensor.cp314t-win_amd64.pyd +0 -0
  39. nvmath/bindings/_internal/cutensor.pxd +58 -0
  40. nvmath/bindings/_internal/mathdx.cp314t-win_amd64.pyd +0 -0
  41. nvmath/bindings/_internal/mathdx.pxd +116 -0
  42. nvmath/bindings/_internal/nvshmem.pxd +29 -0
  43. nvmath/bindings/_internal/utils.cp314t-win_amd64.pyd +0 -0
  44. nvmath/bindings/_internal/utils.pxd +174 -0
  45. nvmath/bindings/_internal/utils.pyi +10 -0
  46. nvmath/bindings/cublas.cp314t-win_amd64.pyd +0 -0
  47. nvmath/bindings/cublas.pxd +558 -0
  48. nvmath/bindings/cublas.pyi +812 -0
  49. nvmath/bindings/cublasLt.cp314t-win_amd64.pyd +0 -0
  50. nvmath/bindings/cublasLt.pxd +109 -0
  51. nvmath/bindings/cublasLt.pyi +1461 -0
  52. nvmath/bindings/cublasMp.pxd +85 -0
  53. nvmath/bindings/cublasMp.pyi +267 -0
  54. nvmath/bindings/cudss.cp314t-win_amd64.pyd +0 -0
  55. nvmath/bindings/cudss.pxd +98 -0
  56. nvmath/bindings/cudss.pyi +443 -0
  57. nvmath/bindings/cufft.cp314t-win_amd64.pyd +0 -0
  58. nvmath/bindings/cufft.pxd +118 -0
  59. nvmath/bindings/cufft.pyi +301 -0
  60. nvmath/bindings/cufftMp.pxd +124 -0
  61. nvmath/bindings/cufftMp.pyi +326 -0
  62. nvmath/bindings/curand.cp314t-win_amd64.pyd +0 -0
  63. nvmath/bindings/curand.pxd +71 -0
  64. nvmath/bindings/curand.pyi +189 -0
  65. nvmath/bindings/cusolver.cp314t-win_amd64.pyd +0 -0
  66. nvmath/bindings/cusolver.pxd +62 -0
  67. nvmath/bindings/cusolver.pyi +320 -0
  68. nvmath/bindings/cusolverDn.cp314t-win_amd64.pyd +0 -0
  69. nvmath/bindings/cusolverDn.pxd +430 -0
  70. nvmath/bindings/cusolverDn.pyi +422 -0
  71. nvmath/bindings/cusolverMp.pxd +98 -0
  72. nvmath/bindings/cusolverMp.pyi +114 -0
  73. nvmath/bindings/cusolverSp.cp314t-win_amd64.pyd +0 -0
  74. nvmath/bindings/cusolverSp.pxd +95 -0
  75. nvmath/bindings/cusolverSp.pyi +70 -0
  76. nvmath/bindings/cusparse.cp314t-win_amd64.pyd +0 -0
  77. nvmath/bindings/cusparse.pxd +546 -0
  78. nvmath/bindings/cusparse.pyi +1017 -0
  79. nvmath/bindings/cusparseLt.cp314t-win_amd64.pyd +0 -0
  80. nvmath/bindings/cusparseLt.pxd +99 -0
  81. nvmath/bindings/cusparseLt.pyi +252 -0
  82. nvmath/bindings/cutensor.cp314t-win_amd64.pyd +0 -0
  83. nvmath/bindings/cutensor.pxd +98 -0
  84. nvmath/bindings/cutensor.pyi +324 -0
  85. nvmath/bindings/cycublas.cp314t-win_amd64.pyd +0 -0
  86. nvmath/bindings/cycublas.pxd +664 -0
  87. nvmath/bindings/cycublasLt.cp314t-win_amd64.pyd +0 -0
  88. nvmath/bindings/cycublasLt.pxd +1045 -0
  89. nvmath/bindings/cycublasMp.pxd +171 -0
  90. nvmath/bindings/cycudss.cp314t-win_amd64.pyd +0 -0
  91. nvmath/bindings/cycudss.pxd +277 -0
  92. nvmath/bindings/cycufft.cp314t-win_amd64.pyd +0 -0
  93. nvmath/bindings/cycufft.pxd +333 -0
  94. nvmath/bindings/cycufftMp.pxd +342 -0
  95. nvmath/bindings/cycurand.cp314t-win_amd64.pyd +0 -0
  96. nvmath/bindings/cycurand.pxd +141 -0
  97. nvmath/bindings/cycusolver.cp314t-win_amd64.pyd +0 -0
  98. nvmath/bindings/cycusolver.pxd +137 -0
  99. nvmath/bindings/cycusolverDn.cp314t-win_amd64.pyd +0 -0
  100. nvmath/bindings/cycusolverDn.pxd +443 -0
  101. nvmath/bindings/cycusolverMp.pxd +107 -0
  102. nvmath/bindings/cycusolverSp.cp314t-win_amd64.pyd +0 -0
  103. nvmath/bindings/cycusolverSp.pxd +93 -0
  104. nvmath/bindings/cycusparse.cp314t-win_amd64.pyd +0 -0
  105. nvmath/bindings/cycusparse.pxd +679 -0
  106. nvmath/bindings/cycusparseLt.cp314t-win_amd64.pyd +0 -0
  107. nvmath/bindings/cycusparseLt.pxd +135 -0
  108. nvmath/bindings/cycutensor.cp314t-win_amd64.pyd +0 -0
  109. nvmath/bindings/cycutensor.pxd +189 -0
  110. nvmath/bindings/cymathdx.cp314t-win_amd64.pyd +0 -0
  111. nvmath/bindings/cymathdx.pxd +552 -0
  112. nvmath/bindings/cynvshmem.pxd +118 -0
  113. nvmath/bindings/mathdx.cp314t-win_amd64.pyd +0 -0
  114. nvmath/bindings/mathdx.pxd +182 -0
  115. nvmath/bindings/mathdx.pyi +1562 -0
  116. nvmath/bindings/nvpl/__init__.pxd +0 -0
  117. nvmath/bindings/nvpl/__init__.py +13 -0
  118. nvmath/bindings/nvpl/_internal/__init__.pxd +0 -0
  119. nvmath/bindings/nvpl/_internal/__init__.py +0 -0
  120. nvmath/bindings/nvpl/_internal/blas.cp314t-win_amd64.pyd +0 -0
  121. nvmath/bindings/nvpl/_internal/blas.pxd +237 -0
  122. nvmath/bindings/nvpl/_internal/fft.cp314t-win_amd64.pyd +0 -0
  123. nvmath/bindings/nvpl/_internal/fft.pxd +36 -0
  124. nvmath/bindings/nvpl/blas.cp314t-win_amd64.pyd +0 -0
  125. nvmath/bindings/nvpl/blas.pxd +131 -0
  126. nvmath/bindings/nvpl/blas.pyi +168 -0
  127. nvmath/bindings/nvpl/cyblas.cp314t-win_amd64.pyd +0 -0
  128. nvmath/bindings/nvpl/cyblas.pxd +280 -0
  129. nvmath/bindings/nvpl/cyfft.cp314t-win_amd64.pyd +0 -0
  130. nvmath/bindings/nvpl/cyfft.pxd +93 -0
  131. nvmath/bindings/nvpl/fft.cp314t-win_amd64.pyd +0 -0
  132. nvmath/bindings/nvpl/fft.pxd +100 -0
  133. nvmath/bindings/nvpl/fft.pyi +168 -0
  134. nvmath/bindings/nvshmem.pxd +54 -0
  135. nvmath/bindings/nvshmem.pyi +191 -0
  136. nvmath/device/__init__.py +38 -0
  137. nvmath/device/_deprecated.py +33 -0
  138. nvmath/device/common.py +315 -0
  139. nvmath/device/common_backend.py +131 -0
  140. nvmath/device/common_cuda.py +201 -0
  141. nvmath/device/common_numba.py +300 -0
  142. nvmath/device/common_numba_cuda_mlir.py +202 -0
  143. nvmath/device/common_opaque_tensor.py +201 -0
  144. nvmath/device/cublasdx.py +1606 -0
  145. nvmath/device/cublasdx_backend.py +860 -0
  146. nvmath/device/cublasdx_numba.py +1534 -0
  147. nvmath/device/cublasdx_numba_cuda_mlir.py +208 -0
  148. nvmath/device/cufftdx.py +373 -0
  149. nvmath/device/cufftdx_backend.py +220 -0
  150. nvmath/device/cufftdx_numba.py +140 -0
  151. nvmath/device/cufftdx_numba_cuda_mlir.py +79 -0
  152. nvmath/device/curand_kernel.py +9147 -0
  153. nvmath/device/cusolverdx.py +2708 -0
  154. nvmath/device/cusolverdx_backend.py +440 -0
  155. nvmath/device/cusolverdx_numba.py +567 -0
  156. nvmath/device/cusolverdx_numba_cuda_mlir.py +604 -0
  157. nvmath/device/cusolverdx_overload_backend.py +1029 -0
  158. nvmath/device/llvm_array.py +29 -0
  159. nvmath/device/random.py +441 -0
  160. nvmath/device/random_helpers.py +23 -0
  161. nvmath/device/random_states.py +187 -0
  162. nvmath/device/types.py +138 -0
  163. nvmath/device/vector_types_numba.py +259 -0
  164. nvmath/distributed/__init__.py +200 -0
  165. nvmath/distributed/_internal/__init__.py +0 -0
  166. nvmath/distributed/_internal/nccl.py +86 -0
  167. nvmath/distributed/_internal/nvshmem.py +307 -0
  168. nvmath/distributed/_internal/symmetric_memory.py +35 -0
  169. nvmath/distributed/_internal/tensor_ifc.py +70 -0
  170. nvmath/distributed/_internal/tensor_ifc_cupy.py +68 -0
  171. nvmath/distributed/_internal/tensor_ifc_host_device.py +172 -0
  172. nvmath/distributed/_internal/tensor_ifc_numpy.py +46 -0
  173. nvmath/distributed/_internal/tensor_ifc_torch.py +162 -0
  174. nvmath/distributed/_internal/tensor_wrapper.py +81 -0
  175. nvmath/distributed/_utils.py +167 -0
  176. nvmath/distributed/distribution/__init__.py +30 -0
  177. nvmath/distributed/distribution/_configuration.py +39 -0
  178. nvmath/distributed/distribution/distributions.py +1024 -0
  179. nvmath/distributed/distribution/redistribute.py +1284 -0
  180. nvmath/distributed/fft/__init__.py +7 -0
  181. nvmath/distributed/fft/_configuration.py +82 -0
  182. nvmath/distributed/fft/fft.py +2742 -0
  183. nvmath/distributed/linalg/__init__.py +22 -0
  184. nvmath/distributed/linalg/_internal/__init__.py +3 -0
  185. nvmath/distributed/linalg/_internal/epilog_protocol.py +586 -0
  186. nvmath/distributed/linalg/_internal/matmul_desc_ifc.py +28 -0
  187. nvmath/distributed/linalg/advanced/__init__.py +8 -0
  188. nvmath/distributed/linalg/advanced/_configuration.py +171 -0
  189. nvmath/distributed/linalg/advanced/matmulmod.py +3573 -0
  190. nvmath/distributed/linalg/generic/__init__.py +8 -0
  191. nvmath/distributed/linalg/generic/_caching.py +66 -0
  192. nvmath/distributed/linalg/generic/_configuration.py +61 -0
  193. nvmath/distributed/linalg/generic/_factorization.py +172 -0
  194. nvmath/distributed/linalg/generic/_initialization.py +966 -0
  195. nvmath/distributed/linalg/generic/_problem_spec.py +511 -0
  196. nvmath/distributed/linalg/generic/solvermod.py +1368 -0
  197. nvmath/distributed/process_group.py +408 -0
  198. nvmath/fft/__init__.py +7 -0
  199. nvmath/fft/_configuration.py +189 -0
  200. nvmath/fft/_exec_utils.py +82 -0
  201. nvmath/fft/_helpers.py +237 -0
  202. nvmath/fft/fft.py +3122 -0
  203. nvmath/internal/__init__.pxd +3 -0
  204. nvmath/internal/__init__.py +10 -0
  205. nvmath/internal/_bindings.cp314t-win_amd64.pyd +0 -0
  206. nvmath/internal/_bindings.pxd +18 -0
  207. nvmath/internal/_device_utils.py +45 -0
  208. nvmath/internal/_layout/__init__.pxd +3 -0
  209. nvmath/internal/_layout/__init__.py +7 -0
  210. nvmath/internal/_layout/_layout.cp314t-win_amd64.pyd +0 -0
  211. nvmath/internal/_layout/_layout.pxd +1303 -0
  212. nvmath/internal/_layout/_layout.pyi +1145 -0
  213. nvmath/internal/enum_utils.py +142 -0
  214. nvmath/internal/formatters.py +87 -0
  215. nvmath/internal/mem_limit.py +51 -0
  216. nvmath/internal/memory.cp314t-win_amd64.pyd +0 -0
  217. nvmath/internal/memory.pxd +13 -0
  218. nvmath/internal/memory.pyi +50 -0
  219. nvmath/internal/ndbuffer/__init__.pxd +3 -0
  220. nvmath/internal/ndbuffer/__init__.py +9 -0
  221. nvmath/internal/ndbuffer/_copy_kernel.cp314t-win_amd64.pyd +0 -0
  222. nvmath/internal/ndbuffer/_copy_kernel.pxd +10 -0
  223. nvmath/internal/ndbuffer/_jit.cp314t-win_amd64.pyd +0 -0
  224. nvmath/internal/ndbuffer/_jit.pxd +7 -0
  225. nvmath/internal/ndbuffer/_ndbuffer.cp314t-win_amd64.pyd +0 -0
  226. nvmath/internal/ndbuffer/_ndbuffer.pxd +40 -0
  227. nvmath/internal/ndbuffer/_ndbuffer.pyi +463 -0
  228. nvmath/internal/ndbuffer/copy_kernel/args.h +34 -0
  229. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/array_view.h +52 -0
  230. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/elementwise.h +68 -0
  231. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/grid_indexer.h +69 -0
  232. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/transposed.h +242 -0
  233. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/type_utils.h +39 -0
  234. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/utils.h +132 -0
  235. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/vec.h +159 -0
  236. nvmath/internal/ndbuffer/copy_kernel/elementwise.h +53 -0
  237. nvmath/internal/ndbuffer/copy_kernel/transposed.h +58 -0
  238. nvmath/internal/package_ifc.py +168 -0
  239. nvmath/internal/package_ifc_cuda.py +57 -0
  240. nvmath/internal/package_ifc_cupy.py +67 -0
  241. nvmath/internal/package_ifc_torch.py +69 -0
  242. nvmath/internal/package_wrapper.py +14 -0
  243. nvmath/internal/tensor_ifc.py +179 -0
  244. nvmath/internal/tensor_ifc_cupy.py +234 -0
  245. nvmath/internal/tensor_ifc_ndbuffer.py +147 -0
  246. nvmath/internal/tensor_ifc_numpy.py +184 -0
  247. nvmath/internal/tensor_ifc_torch.py +178 -0
  248. nvmath/internal/tensor_wrapper.py +160 -0
  249. nvmath/internal/typemaps.py +113 -0
  250. nvmath/internal/utils.py +805 -0
  251. nvmath/linalg/__init__.py +56 -0
  252. nvmath/linalg/_internal/__init__.py +3 -0
  253. nvmath/linalg/_internal/algo_cap_ifc.py +82 -0
  254. nvmath/linalg/_internal/algo_config_ifc.py +43 -0
  255. nvmath/linalg/_internal/batch.py +234 -0
  256. nvmath/linalg/_internal/enum_to_tuples.py +64 -0
  257. nvmath/linalg/_internal/epilog_protocol.py +766 -0
  258. nvmath/linalg/_internal/layout.py +624 -0
  259. nvmath/linalg/_internal/matmul_desc_ifc.py +28 -0
  260. nvmath/linalg/_internal/matmul_pref_ifc.py +27 -0
  261. nvmath/linalg/_internal/matrix_layout_ifc.py +26 -0
  262. nvmath/linalg/_internal/solver_utils.py +432 -0
  263. nvmath/linalg/_internal/typemaps.py +144 -0
  264. nvmath/linalg/_internal/utils.py +157 -0
  265. nvmath/linalg/advanced/__init__.py +8 -0
  266. nvmath/linalg/advanced/_algorithmmod.py +170 -0
  267. nvmath/linalg/advanced/_configuration.py +351 -0
  268. nvmath/linalg/advanced/helpers/__init__.py +5 -0
  269. nvmath/linalg/advanced/helpers/matmul.py +1316 -0
  270. nvmath/linalg/advanced/matmulmod.py +3734 -0
  271. nvmath/linalg/generic/__init__.py +53 -0
  272. nvmath/linalg/generic/_configuration/__init__.py +39 -0
  273. nvmath/linalg/generic/_configuration/layout.py +263 -0
  274. nvmath/linalg/generic/_configuration/match.py +734 -0
  275. nvmath/linalg/generic/_configuration/qualifiers.py +493 -0
  276. nvmath/linalg/generic/_configuration/solver_configuration.py +59 -0
  277. nvmath/linalg/generic/_configuration/wrap.py +217 -0
  278. nvmath/linalg/generic/_dtype.py +15 -0
  279. nvmath/linalg/generic/matmulmod.py +2094 -0
  280. nvmath/linalg/generic/solvermod.py +1301 -0
  281. nvmath/memory.py +279 -0
  282. nvmath/sparse/__init__.py +38 -0
  283. nvmath/sparse/_internal/__init__.py +21 -0
  284. nvmath/sparse/_internal/common_utils.py +147 -0
  285. nvmath/sparse/_internal/cudss_config_ifc.py +702 -0
  286. nvmath/sparse/_internal/cudss_data_ifc.py +399 -0
  287. nvmath/sparse/_internal/cudss_utils.py +506 -0
  288. nvmath/sparse/_internal/cusparse_utils.py +382 -0
  289. nvmath/sparse/_internal/sparse_bsc_ifc.py +303 -0
  290. nvmath/sparse/_internal/sparse_bsr_ifc.py +305 -0
  291. nvmath/sparse/_internal/sparse_coo_ifc.py +256 -0
  292. nvmath/sparse/_internal/sparse_csc_ifc.py +268 -0
  293. nvmath/sparse/_internal/sparse_csr_ifc.py +288 -0
  294. nvmath/sparse/_internal/sparse_dia_ifc.py +242 -0
  295. nvmath/sparse/_internal/sparse_format_helpers.py +601 -0
  296. nvmath/sparse/_internal/sparse_tensor_ifc.py +133 -0
  297. nvmath/sparse/_internal/sparse_ust_ifc.py +141 -0
  298. nvmath/sparse/_internal/utils.py +56 -0
  299. nvmath/sparse/advanced/__init__.py +7 -0
  300. nvmath/sparse/advanced/_configuration.py +227 -0
  301. nvmath/sparse/advanced/direct_solver.py +2069 -0
  302. nvmath/sparse/generic/__init__.py +7 -0
  303. nvmath/sparse/generic/_configuration.py +129 -0
  304. nvmath/sparse/generic/_helpers.py +137 -0
  305. nvmath/sparse/generic/_thunks.py +21 -0
  306. nvmath/sparse/generic/matmulmod.py +2353 -0
  307. nvmath/sparse/ust/__init__.py +7 -0
  308. nvmath/sparse/ust/_converters.py +422 -0
  309. nvmath/sparse/ust/_cpp.py +28 -0
  310. nvmath/sparse/ust/_drawer.py +565 -0
  311. nvmath/sparse/ust/_emitter.py +1033 -0
  312. nvmath/sparse/ust/_jit.py +188 -0
  313. nvmath/sparse/ust/_kernel.py +282 -0
  314. nvmath/sparse/ust/_utils.py +149 -0
  315. nvmath/sparse/ust/interfaces/__init__.py +0 -0
  316. nvmath/sparse/ust/interfaces/torch_interface.py +476 -0
  317. nvmath/sparse/ust/tensor.py +1016 -0
  318. nvmath/sparse/ust/tensor_format.py +957 -0
  319. nvmath/tensor/__init__.py +6 -0
  320. nvmath/tensor/_configuration.py +120 -0
  321. nvmath/tensor/_internal/__init__.py +3 -0
  322. nvmath/tensor/_internal/cutensor_config_ifc.py +279 -0
  323. nvmath/tensor/_internal/cutensor_utils.py +230 -0
  324. nvmath/tensor/_internal/data.py +43 -0
  325. nvmath/tensor/_internal/einsum_parser.py +444 -0
  326. nvmath/tensor/_internal/typemaps.py +96 -0
  327. nvmath/tensor/contract.py +1900 -0
  328. nvmath_python-1.0.0.dist-info/METADATA +134 -0
  329. nvmath_python-1.0.0.dist-info/RECORD +332 -0
  330. nvmath_python-1.0.0.dist-info/WHEEL +5 -0
  331. nvmath_python-1.0.0.dist-info/licenses/LICENSE +177 -0
  332. nvmath_python-1.0.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,1024 @@
1
+ # Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from __future__ import annotations # allows typehint of class methods to return the self class
6
+
7
+ import copy
8
+ import math
9
+ from abc import ABC, abstractmethod
10
+ from collections.abc import Sequence
11
+ from enum import IntEnum
12
+ from typing import cast
13
+
14
+ import numpy as np
15
+
16
+ import nvmath.distributed as dist
17
+ from nvmath.bindings import cufftMp # type: ignore
18
+
19
+ __all__ = [
20
+ "ProcessGrid",
21
+ "Distribution",
22
+ "Slab",
23
+ "Box",
24
+ "BlockCyclic",
25
+ "BlockNonCyclic",
26
+ "BindDistributionError",
27
+ "ConvertDistributionError",
28
+ ]
29
+
30
+
31
+ class ProcessGrid:
32
+ """
33
+ N-dimensional grid of processes used by some distributions like the PBLAS block-cyclic
34
+ distribution.
35
+
36
+ Example 2D process grid for 4 processes, with processes arranged in column-major order::
37
+
38
+ ---------
39
+ | 0 | 2 |
40
+ ---------
41
+ | 1 | 3 |
42
+ ---------
43
+ """
44
+
45
+ class Layout(IntEnum):
46
+ """Process-grid layout used by distributed nvmath routines."""
47
+
48
+ COL_MAJOR = 0
49
+ ROW_MAJOR = 1
50
+
51
+ _layout: Layout | None
52
+
53
+ def __init__(
54
+ self,
55
+ *,
56
+ shape: Sequence[int] | None = None,
57
+ layout: ProcessGrid.Layout | None = None,
58
+ process_array=None,
59
+ ):
60
+ """
61
+ Create a new ProcessGrid object.
62
+
63
+ Args:
64
+ shape: Shape of the process grid.
65
+
66
+ layout: Layout of the process grid (column-major or row-major). This is optional
67
+ for 1D grid or when a custom grid is provided.
68
+
69
+ process_array: optional ndarray specifying custom arrangement of processes.
70
+ """
71
+ self._nranks = _get_process_group().nranks
72
+
73
+ if process_array is None:
74
+ if shape is None:
75
+ raise ValueError("shape must be provided when process_array=None")
76
+ self._shape = tuple(shape)
77
+ if layout is None:
78
+ if self._is_1d_distribution():
79
+ layout = ProcessGrid.Layout.ROW_MAJOR # layout doesn't matter in this case.
80
+ else:
81
+ raise ValueError("layout must be provided when process_array=None and partitioning on multiple dimensions")
82
+ if not isinstance(layout, ProcessGrid.Layout):
83
+ raise TypeError(f"layout must be of type ProcessGrid.Layout, got {layout}")
84
+ self._layout = layout
85
+ self._process_array = None
86
+ else:
87
+ self._shape = tuple(process_array.shape)
88
+ if shape is not None and self._shape != tuple(shape):
89
+ raise ValueError(f"shape {shape} and process_array.shape ({process_array.shape}) don't match")
90
+ # TODO: Can set layout to COL_MAJOR or ROW_MAJOR automatically if the
91
+ # process_array matches.
92
+ if layout is not None:
93
+ raise NotImplementedError
94
+ self._layout = None
95
+ self._process_array = process_array
96
+
97
+ if math.prod(self._shape) != self._nranks:
98
+ raise ValueError(
99
+ f"Number of grid elements ({math.prod(self._shape)}) must equal the number of processes ({self._nranks})"
100
+ )
101
+
102
+ @property
103
+ def shape(self) -> tuple[int, ...]:
104
+ """Shape of process grid."""
105
+ return self._shape
106
+
107
+ @property
108
+ def layout(self) -> ProcessGrid.Layout | None:
109
+ """Layout of process grid if row-major or column-major, otherwise None."""
110
+ return self._layout
111
+
112
+ @property
113
+ def process_array(self):
114
+ return self._process_array
115
+
116
+ def __str__(self):
117
+ return f"ProcessGrid(shape={self._shape}, layout={self._layout.name}, process_array={self._process_array})"
118
+
119
+ def __hash__(self):
120
+ # NOTE: The layout isn't considered for the hash, to allow process grids
121
+ # partitioned on a single dimension with the same shape but different layout
122
+ # to be the same dictionary key.
123
+ return hash(self._shape)
124
+
125
+ def __eq__(self, other):
126
+ if self._process_array is None and other._process_array is None:
127
+ if self._shape == other._shape:
128
+ return self._is_1d_distribution() or self._layout == other._layout
129
+ return False
130
+ raise NotImplementedError
131
+
132
+ def _is_1d_distribution(self) -> bool:
133
+ """True if process grid partitions on a single dimension."""
134
+ return self._nranks in self._shape
135
+
136
+ def _is_row_wise(self) -> bool:
137
+ """True if 2D process grid partitioned only on rows."""
138
+ return self._shape == (self._nranks, 1)
139
+
140
+ def _is_col_wise(self) -> bool:
141
+ """True if 2D process grid partitioned only on columns."""
142
+ return self._shape == (1, self._nranks)
143
+
144
+
145
+ class BindDistributionError(Exception):
146
+ pass
147
+
148
+
149
+ class ConvertDistributionError(Exception):
150
+ """Errors converting a distribution instance to another distribution type"""
151
+
152
+ pass
153
+
154
+
155
+ class Distribution(ABC):
156
+ """Specifies how a tensor is distributed across processes."""
157
+
158
+ def __init__(self):
159
+ self._bound = False
160
+
161
+ @property
162
+ @abstractmethod
163
+ def ndim(self) -> int | None:
164
+ """The number of dimensions of a distributed tensor for which this distribution
165
+ applies; None if it doesn't apply to any specific number of dimensions."""
166
+ raise NotImplementedError
167
+
168
+ @abstractmethod
169
+ def shape(self, rank: int, global_shape: Sequence[int] | None = None) -> tuple[int, ...]:
170
+ """Get the local shape of data on the given rank according to this distribution.
171
+
172
+ Args:
173
+ rank: the process rank for which to calculate the local shape.
174
+
175
+ global_shape: Global shape of data. Required if the distribution is
176
+ not bound to a global shape, otherwise not required.
177
+ """
178
+ raise NotImplementedError
179
+
180
+ @abstractmethod
181
+ def to(
182
+ self,
183
+ cls: type[Distribution],
184
+ /,
185
+ *,
186
+ ndim: int | None = None,
187
+ copy: bool = False,
188
+ ) -> Distribution:
189
+ """Convert this distribution object to an equivalent distribution of the given type.
190
+
191
+ Args:
192
+ cls: the target distribution type.
193
+
194
+ ndim: dimensionality of the target distribution. It must be greater than or
195
+ equal to the source dimensionality; it can never be smaller. When it is
196
+ larger, the distribution is promoted by appending trailing size-1
197
+ dimensions. Providing ``ndim`` is required when the
198
+ source distribution has no associated dimensionality of its own.
199
+
200
+ copy: Returns a copy if the source and target type are the same.
201
+
202
+ Raises:
203
+ ConvertDistributionError: if the conversion is not possible.
204
+ """
205
+ raise NotImplementedError
206
+
207
+ def _to_checks(self, cls: type[Distribution], ndim: int | None) -> None:
208
+ if cls is Distribution or not issubclass(cls, Distribution):
209
+ raise ValueError(f"{cls} is not a valid distribution")
210
+ if ndim is not None and self.ndim is not None and ndim < self.ndim:
211
+ raise ValueError(f"ndim argument ({ndim}) must be >= the distribution's dimensionality ({self.ndim})")
212
+
213
+ @abstractmethod
214
+ def _bind(
215
+ self,
216
+ global_shape: Sequence[int],
217
+ *,
218
+ shape: Sequence[int] | None = None,
219
+ ) -> Distribution:
220
+ """Binds this distribution object to a global shape, which determines how a
221
+ distributed tensor with that shape must be partitioned among processes (the local
222
+ shape on each process). You can also provide the local shape on this process to
223
+ check if it fits the distribution (the function will raise an exception if not).
224
+ **The exception may be raised on some ranks but not others (it's up to the caller
225
+ to handle this)**.
226
+
227
+ Args:
228
+ global_shape: global shape of the data.
229
+
230
+ shape: shape of the data on this process.
231
+
232
+ Returns:
233
+ self
234
+
235
+ Raises:
236
+ BindDistributionError: if distribution is already bound or local shape doesn't
237
+ fit the distribution.
238
+ """
239
+ raise NotImplementedError
240
+
241
+ def _binding_str(self):
242
+ return f"[bound: global_shape={self._data_global_shape}, shape={self._data_shape}]" if self._bound else ""
243
+
244
+ def copy(self) -> Distribution:
245
+ """This is a common implementation for those distributions that only require
246
+ a shallow copy."""
247
+ return copy.copy(self)
248
+
249
+ def _local_shape_checks(self, rank: int, nranks: int, global_shape: Sequence[int] | None = None):
250
+ if not isinstance(rank, int):
251
+ raise ValueError(f"rank must be an integer, got {rank}")
252
+
253
+ if rank < 0 or rank > nranks - 1:
254
+ raise ValueError(f"This is not a valid process rank: got rank={rank} with nranks={nranks}")
255
+
256
+ if global_shape is None and not self._bound:
257
+ raise RuntimeError("This distribution is unbound: please specify a global shape")
258
+
259
+ if global_shape is not None and self._bound and tuple(global_shape) != self._data_global_shape: # type: ignore
260
+ raise ValueError(
261
+ "This distribution is already bound to a different global shape: provided "
262
+ f"{global_shape}, bound to {self._data_global_shape}" # type: ignore
263
+ )
264
+
265
+ # ------------------------------------------------------------------------------------
266
+ # Methods to infer global shape of an operand that is distributed according
267
+ # to this distribution. This is intended to be used in a ProcessGroup.allreduce_object()
268
+ # reduction (or equivalent type of reduction) in the following manner:
269
+ # - self refers to the distribution instance that is part of the reduction.
270
+ # - self._global_shape_reduction_prologue() is called at the start of the reduction on
271
+ # each process (in other words, at the reduction tree leaves).
272
+ # - self._global_shape_reduction() is called at every step of the reduction.
273
+ # - _global_shape_reduction_epilogue() is called after the reduction completes, and
274
+ # returns the global shape of the operand.
275
+ # IMPORTANT: This only calculates the global shape of *one operand*. If the
276
+ # operation has multiple operands, you must provide a separate distribution
277
+ # instance for each operand (even if the distribution characteristics are the same).
278
+
279
+ @abstractmethod
280
+ def _global_shape_reduction_prologue(self, rank: int, local_shape: Sequence[int]) -> None:
281
+ """Initialize the global shape reduction.
282
+
283
+ Args:
284
+ rank: rank of this process
285
+
286
+ local_shape: local shape of operand on this process.
287
+ """
288
+ raise NotImplementedError
289
+
290
+ @abstractmethod
291
+ def _global_shape_reduction(self, other: Distribution) -> None:
292
+ raise NotImplementedError
293
+
294
+ @abstractmethod
295
+ def _global_shape_reduction_epilogue(self) -> tuple[int, ...]:
296
+ """Return inferred global shape (to be called after the reduction completes"""
297
+ raise NotImplementedError
298
+
299
+
300
+ def _calculate_local_box(global_shape, partition_dim, rank, nranks):
301
+ """Given a global shape of data that is partitioned across ranks along the
302
+ `partition_dim` dimension according to the Slab distribution, return
303
+ the local box of this rank (as lower and upper coordinates in the global shape).
304
+ """
305
+ lower = [0] * len(global_shape)
306
+ N = nranks
307
+ S = global_shape[partition_dim]
308
+
309
+ num_ranks_with_remainder = S % N
310
+ num_preceding_ranks_with_remainder = min(num_ranks_with_remainder, rank)
311
+ num_preceding_ranks_without_remainder = rank - num_preceding_ranks_with_remainder
312
+
313
+ lower[partition_dim] = num_preceding_ranks_with_remainder * (S // N + 1) + num_preceding_ranks_without_remainder * (S // N)
314
+ upper = list(global_shape)
315
+ upper[partition_dim] = lower[partition_dim] + (S // N + 1 if rank < num_ranks_with_remainder else S // N)
316
+
317
+ return lower, upper
318
+
319
+
320
+ class Slab(Distribution):
321
+ """
322
+ Slab distribution
323
+
324
+ Data is partitioned across processes on a single axis, such that:
325
+
326
+ - The shape of the slab on the first s_p % P processes is
327
+ (s_0, ..., s_p // P + 1, ..., s_{n-1})
328
+ - The shape of the slab on the remaining processes is (s_0, ..., s_p // P, ..., s_{n-1})
329
+ - Process 0 owns the first slab according to the global index order, process 1 owns
330
+ the second slab and so on.
331
+
332
+ where:
333
+
334
+ - s_i is the size of dimension i of the global array
335
+ - p is the partition dimension
336
+ - n is the number of dimensions of the array
337
+ - P is the number of processes
338
+ """
339
+
340
+ X: Slab
341
+ """Slab distribution on axis 0."""
342
+
343
+ Y: Slab
344
+ """Slab distribution on axis 1."""
345
+
346
+ Z: Slab
347
+ """Slab distribution on axis 2."""
348
+
349
+ def __init__(
350
+ self,
351
+ partition_dim: int,
352
+ ndim: int | None = None,
353
+ ):
354
+ super().__init__()
355
+ if not isinstance(partition_dim, int) or partition_dim < 0:
356
+ raise ValueError(f"partition_dim must be integer >= 0, got {partition_dim}")
357
+
358
+ if ndim is not None:
359
+ if not isinstance(ndim, int) or ndim < 1:
360
+ raise ValueError(f"ndim must be integer >= 1, got {ndim}")
361
+ if partition_dim >= ndim:
362
+ raise ValueError("partition_dim must be < ndim")
363
+
364
+ self._partition_dim = partition_dim
365
+ self._ndim = ndim
366
+
367
+ def __eq__(self, other):
368
+ if not isinstance(other, Slab):
369
+ return False
370
+ if self._partition_dim != other._partition_dim:
371
+ return False
372
+ if self._ndim is None or other._ndim is None:
373
+ return True
374
+ return self._ndim == other._ndim
375
+
376
+ def __hash__(self):
377
+ return self._partition_dim
378
+
379
+ def __str__(self):
380
+ return f"Slab(partition_dim={self._partition_dim}, ndim={self._ndim})" + self._binding_str()
381
+
382
+ @property
383
+ def name(self) -> str:
384
+ match self._partition_dim:
385
+ case 0:
386
+ return "Slab.X"
387
+ case 1:
388
+ return "Slab.Y"
389
+ case 2:
390
+ return "Slab.Z"
391
+ case _:
392
+ return "Slab"
393
+
394
+ @property
395
+ def partition_dim(self) -> int:
396
+ """Slab partition dimension"""
397
+ return self._partition_dim
398
+
399
+ @property
400
+ def ndim(self) -> int | None:
401
+ return self._ndim
402
+
403
+ def shape(self, rank: int, global_shape: Sequence[int] | None = None) -> tuple[int, ...]:
404
+ n = _get_process_group().nranks
405
+ self._local_shape_checks(rank, n, global_shape)
406
+ if global_shape is None:
407
+ global_shape = self._data_global_shape
408
+
409
+ S = global_shape[self._partition_dim]
410
+ partition_dim_local_size = S // n + bool(rank < S % n)
411
+ slab_shape = list(global_shape)
412
+ slab_shape[self._partition_dim] = partition_dim_local_size
413
+ return tuple(slab_shape)
414
+
415
+ @property
416
+ def _cufftmp_value(self):
417
+ if self._partition_dim not in (0, 1):
418
+ raise TypeError(f"Unsupported distribution {self} for cuFFTMp: partition dimension must be X or Y")
419
+ return cufftMp.XtSubFormat.FORMAT_INPLACE if self._partition_dim == 0 else cufftMp.XtSubFormat.FORMAT_INPLACE_SHUFFLED
420
+
421
+ def _bind(self, global_shape, *, shape=None) -> Slab:
422
+ if self._bound:
423
+ raise BindDistributionError(f"{self} is already bound")
424
+
425
+ if self._ndim is not None and len(global_shape) != self._ndim:
426
+ raise ValueError(f"The given shape doesn't have the same dimensionality as this {self} distribution")
427
+
428
+ if self._partition_dim >= len(global_shape):
429
+ raise ValueError("partition_dim must be < ndim")
430
+
431
+ rank = _get_process_group().rank
432
+ slab_shape = self.shape(rank, global_shape)
433
+
434
+ if shape is not None and tuple(shape) != slab_shape:
435
+ raise BindDistributionError(
436
+ f"The given shapes (global_shape={global_shape}, shape={shape}) don't fit distribution {str(self)}"
437
+ )
438
+
439
+ self._ndim = len(global_shape)
440
+ self._data_global_shape = tuple(global_shape)
441
+ self._data_shape = slab_shape
442
+ self._bound = True
443
+ return self
444
+
445
+ def to(self, cls, /, *, ndim=None, copy=False):
446
+ super()._to_checks(cls, ndim)
447
+ process_group = _get_process_group()
448
+ rank, nranks = process_group.rank, process_group.nranks
449
+
450
+ if ndim is None:
451
+ ndim = self._ndim
452
+ elif self._partition_dim >= ndim:
453
+ raise ValueError(f"ndim ({ndim}) must be greater than the partition dimension ({self._partition_dim})")
454
+
455
+ if cls is Slab:
456
+ if ndim is not None and ndim != self._ndim:
457
+ d = Slab(partition_dim=self._partition_dim, ndim=ndim)
458
+ if self._bound:
459
+ d._bind(self._data_global_shape + (1,) * (ndim - self._ndim))
460
+ return d
461
+ elif copy:
462
+ d = cast(Slab, self.copy())
463
+ d._ndim = ndim
464
+ return d
465
+ return self
466
+ elif issubclass(cls, BlockCyclic):
467
+ if ndim is None:
468
+ raise ConvertDistributionError("Can't convert Slab distribution to BlockCyclic: unknown dimensionality")
469
+
470
+ process_grid_shape = tuple(1 if x != self._partition_dim else nranks for x in range(ndim))
471
+ # layout doesn't matter when partitioning on a single axis.
472
+ process_grid = ProcessGrid(shape=process_grid_shape, layout=ProcessGrid.Layout.ROW_MAJOR)
473
+ if not self._bound:
474
+ return BlockNonCyclic(process_grid)
475
+ else:
476
+ data_global_shape = self._data_global_shape + (1,) * (ndim - self._ndim)
477
+ data_shape = self._data_shape + (1,) * (ndim - self._ndim)
478
+ b = BlockNonCyclic(process_grid)
479
+ return b._bind(data_global_shape, shape=data_shape)
480
+ elif cls is Box:
481
+ if not self._bound:
482
+ raise ConvertDistributionError("Unbound Slab distribution can't be converted to Box.")
483
+
484
+ data_global_shape = self._data_global_shape + (1,) * (ndim - self._ndim)
485
+ lower, upper = _calculate_local_box(data_global_shape, self._partition_dim, rank, nranks)
486
+ return Box(lower, upper)._bind(data_global_shape)
487
+
488
+ def _global_shape_reduction_prologue(self, rank: int, local_shape: Sequence[int]) -> None:
489
+ if self.ndim is not None and self.ndim != len(local_shape):
490
+ raise ValueError(
491
+ f"The dimensionality of {self} doesn't match the dimensionality of the local operand ({len(local_shape)})"
492
+ )
493
+ self._ndim = len(local_shape)
494
+ self._global_shape = list(local_shape) # this is the actual reduction variable
495
+
496
+ def _global_shape_reduction(self, other: Distribution) -> None:
497
+ if self is other:
498
+ return
499
+
500
+ if self != other: # partition_dim and dimensionality must match
501
+ raise ValueError(f"The distribution is inconsistent across processes: {self} != {other}")
502
+
503
+ ndim = self.ndim
504
+ assert ndim is not None, "Internal error"
505
+ if any(self._global_shape[i] != other._global_shape[i] for i in range(ndim) if i != self._partition_dim): # type: ignore[attr-defined]
506
+ raise ValueError("The problem size is inconsistent across processes")
507
+
508
+ self._global_shape[self._partition_dim] += other._global_shape[self._partition_dim] # type: ignore[attr-defined]
509
+
510
+ def _global_shape_reduction_epilogue(self) -> tuple[int, ...]:
511
+ return tuple(self._global_shape)
512
+
513
+
514
+ # Define alternate forms for the user to specify Slab on X, Y or Z.
515
+ # NOTE: dimensionality is left unspecified when using these (but will be set when the
516
+ # distribution is bound to data).
517
+ Slab.X = Slab(0)
518
+ Slab.Y = Slab(1)
519
+ Slab.Z = Slab(2)
520
+
521
+
522
+ class Box(Distribution):
523
+ """Box distribution"""
524
+
525
+ def __init__(
526
+ self,
527
+ lower: Sequence[int],
528
+ upper: Sequence[int],
529
+ ):
530
+ super().__init__()
531
+ if len(lower) != len(upper):
532
+ raise ValueError("lower and upper coordinates must have the same dimensionality")
533
+ for coords in (lower, upper):
534
+ if not all(isinstance(x, int) for x in coords):
535
+ raise ValueError("lower and upper coordinates must be integer")
536
+ if not all(upper[i] > lower[i] for i in range(len(upper))):
537
+ raise ValueError(
538
+ f"The upper coordinates must be larger than the lower coordinates, but got lower={lower} upper={upper}"
539
+ )
540
+ self._lower = tuple(lower)
541
+ self._upper = tuple(upper)
542
+
543
+ @property
544
+ def lower(self) -> tuple[int, ...]:
545
+ """Box lower coordinates"""
546
+ return self._lower
547
+
548
+ @property
549
+ def upper(self) -> tuple[int, ...]:
550
+ """Box upper coordinates"""
551
+ return self._upper
552
+
553
+ @property
554
+ def ndim(self) -> int:
555
+ return len(self._lower)
556
+
557
+ @property
558
+ def box_shape(self) -> tuple[int, ...]:
559
+ return tuple(self._upper[i] - self._lower[i] for i in range(self.ndim))
560
+
561
+ def shape(self, rank: int, global_shape: Sequence[int] | None = None) -> tuple[int, ...]:
562
+ process_group = _get_process_group()
563
+ if rank != process_group.rank:
564
+ raise RuntimeError("Can't calculate local shape of peer process with Box distribution")
565
+ nranks = process_group.nranks
566
+ self._local_shape_checks(rank, nranks, global_shape)
567
+ return tuple(self._upper[i] - self._lower[i] for i in range(self.ndim))
568
+
569
+ def __str__(self):
570
+ return f"Box(lower={self._lower}, upper={self._upper})" + self._binding_str()
571
+
572
+ def __eq__(self, other):
573
+ if not isinstance(other, Box):
574
+ return False
575
+ return self._lower == other._lower and self._upper == other._upper
576
+
577
+ def __hash__(self):
578
+ return hash((self._lower, self._upper))
579
+
580
+ def __iter__(self):
581
+ # To allow unpacking
582
+ yield self._lower
583
+ yield self._upper
584
+
585
+ def __getitem__(self, index):
586
+ if index not in (0, 1):
587
+ return IndexError(f"Index must be 0 or 1, got {index}")
588
+ return self._lower if index == 0 else self._upper
589
+
590
+ def _bind(self, global_shape, *, shape=None) -> Distribution:
591
+ if self._bound:
592
+ raise BindDistributionError(f"{self} is already bound")
593
+ my_shape = tuple(self._upper[i] - self._lower[i] for i in range(self.ndim))
594
+ if shape is not None and tuple(shape) != my_shape:
595
+ raise BindDistributionError(
596
+ f"The given shapes don't fit this Box distribution: {global_shape} "
597
+ f"and {shape}, lower={self._lower} upper={self._upper}"
598
+ )
599
+ self._data_global_shape = tuple(global_shape)
600
+ self._data_shape = my_shape
601
+ self._bound = True
602
+ return self
603
+
604
+ def to(self, cls, /, *, ndim=None, copy=False):
605
+ super()._to_checks(cls, ndim)
606
+ if cls is Box:
607
+ if ndim is not None and ndim > self.ndim:
608
+ # Promote to higher dimensionality, assuming that the size of appended
609
+ # dimensions is 1.
610
+ new_lower = self._lower + (0,) * (ndim - self.ndim)
611
+ new_upper = self._upper + (1,) * (ndim - self.ndim)
612
+ d = Box(new_lower, new_upper)
613
+ if self._bound:
614
+ d._bind(self._data_global_shape + (1,) * (ndim - self.ndim))
615
+ return d
616
+ return self.copy() if copy else self
617
+ raise NotImplementedError
618
+
619
+ def _global_shape_reduction_prologue(self, rank: int, local_shape: Sequence[int]) -> None:
620
+ if self.ndim != len(local_shape):
621
+ raise ValueError(
622
+ f"The dimensionality of {self} doesn't match the dimensionality of the local operand ({len(local_shape)})"
623
+ )
624
+ if self.box_shape != tuple(local_shape):
625
+ raise ValueError(f"The operand shape {local_shape} does not match the box shape {self.box_shape}")
626
+
627
+ def _global_shape_reduction(self, other: Distribution) -> None:
628
+ if type(other) is not Box:
629
+ raise ValueError(f"Inconsistent distribution type across processes: {type(self)} != {type(other)}")
630
+ # This calculates the smallest box that encompasses the box of `self` and the
631
+ # box of `other`.
632
+ self._lower = tuple(np.minimum(np.array(self._lower), np.array(other._lower)).tolist()) # type: ignore
633
+ self._upper = tuple(np.maximum(np.array(self._upper), np.array(other._upper)).tolist()) # type: ignore
634
+
635
+ def _global_shape_reduction_epilogue(self) -> tuple[int, ...]:
636
+ global_shape = self.box_shape
637
+ return global_shape
638
+
639
+
640
+ def _numroc(n: int, nb: int, iproc: int, isrcproc: int, nprocs: int) -> int:
641
+ """Computes the number of rows or columns of a matrix distributed with BlockCyclic
642
+ distribution, owned by the process indicated by iproc argument."""
643
+
644
+ # Figure PROC's distance from source process
645
+ mydist = (nprocs + iproc - isrcproc) % nprocs
646
+
647
+ # Figure the total number of whole NB blocks N is split up into
648
+ nblocks = n // nb
649
+
650
+ # Figure the minimum number of rows/cols a process can have
651
+ numroc = (nblocks // nprocs) * nb
652
+
653
+ # See if there are any extra blocks
654
+ extrablks = nblocks % nprocs
655
+
656
+ if mydist < extrablks:
657
+ # If I have extra blocks
658
+ return numroc + nb
659
+ elif mydist == extrablks:
660
+ # If I have the last block, it may be a partial block
661
+ return numroc + (n % nb)
662
+ else:
663
+ # first M-1 blocks are full blocks
664
+ return numroc
665
+
666
+
667
+ class BlockCyclic(Distribution):
668
+ """Block-cyclic distribution"""
669
+
670
+ def __init__(
671
+ self,
672
+ process_grid: ProcessGrid,
673
+ block_sizes: Sequence[int],
674
+ *,
675
+ first_process: Sequence[int] | None = None,
676
+ ):
677
+ super().__init__()
678
+ if block_sizes is None or not all(isinstance(x, int) for x in block_sizes):
679
+ raise ValueError(f"Must provide a sequence of integer block sizes, got {block_sizes}")
680
+ if len(block_sizes) != len(process_grid.shape):
681
+ raise ValueError(
682
+ f"Number of block sizes ({len(block_sizes)}) doesn't match dimensionality ({len(process_grid.shape)})"
683
+ )
684
+ self._process_grid = process_grid
685
+ self._block_sizes = tuple(block_sizes)
686
+ if first_process is None:
687
+ self._first_process = (0,) * self.ndim
688
+ else:
689
+ if not all(isinstance(x, int) for x in first_process):
690
+ raise ValueError(f"first_process must be a sequence of integer coordinates, got {first_process}")
691
+ for i, x in enumerate(first_process):
692
+ if x < 0 or x >= process_grid.shape[i]:
693
+ raise ValueError(
694
+ f"first_process {first_process} is not a valid index into the process grid of "
695
+ f"shape {process_grid.shape}"
696
+ )
697
+ self._first_process = tuple(first_process)
698
+
699
+ @property
700
+ def process_grid(self) -> ProcessGrid:
701
+ """The process grid of this BlockCyclic distribution"""
702
+ return self._process_grid
703
+
704
+ @property
705
+ def ndim(self) -> int:
706
+ return len(self._process_grid.shape)
707
+
708
+ @property
709
+ def block_sizes(self) -> tuple[int, ...]:
710
+ """The block sizes of this BlockCyclic distribution"""
711
+ return self._block_sizes
712
+
713
+ @property
714
+ def first_process(self) -> tuple[int, ...]:
715
+ """Index in the process grid of the process who owns the first block of the
716
+ distributed tensor."""
717
+ return self._first_process
718
+
719
+ def __str__(self):
720
+ return (
721
+ f"{self.__class__.__name__}(process_grid={self._process_grid}, block_sizes={self._block_sizes})"
722
+ + self._binding_str()
723
+ )
724
+
725
+ def __eq__(self, other):
726
+ if not isinstance(other, BlockCyclic):
727
+ return False
728
+ return (
729
+ self._process_grid == other._process_grid
730
+ and self._block_sizes == other._block_sizes
731
+ and self._first_process == other._first_process
732
+ )
733
+
734
+ def _is_1d_distribution(self) -> bool:
735
+ """True if process grid partitions on a single dimension."""
736
+ return self._process_grid._is_1d_distribution()
737
+
738
+ def _is_row_wise(self) -> bool:
739
+ """True if 2D process grid partitioned only on rows."""
740
+ return self._process_grid._is_row_wise()
741
+
742
+ def _is_col_wise(self) -> bool:
743
+ """True if 2D process grid partitioned only on columns."""
744
+ return self._process_grid._is_col_wise()
745
+
746
+ def shape(self, rank: int, global_shape: Sequence[int] | None = None) -> tuple[int, ...]:
747
+ self._local_shape_checks(rank, self._process_grid._nranks, global_shape)
748
+ if global_shape is None:
749
+ global_shape = self._data_global_shape
750
+ return self._calc_local_shape(rank, self._block_sizes, global_shape)
751
+
752
+ def _calc_local_shape(self, rank, block_sizes, global_shape):
753
+ nprow, npcol = self._process_grid._shape
754
+ layout = self._process_grid._layout
755
+ if layout is not None:
756
+ myprow = rank % nprow if layout == ProcessGrid.Layout.COL_MAJOR else rank // npcol
757
+ mypcol = rank // nprow if layout == ProcessGrid.Layout.COL_MAJOR else rank % npcol
758
+ index = (myprow, mypcol)
759
+ else:
760
+ raise NotImplementedError
761
+ nrows = _numroc(global_shape[0], block_sizes[0], index[0], self._first_process[0], nprow)
762
+ ncols = _numroc(global_shape[1], block_sizes[1], index[1], self._first_process[1], npcol)
763
+ return (nrows, ncols)
764
+
765
+ def _bind(self, global_shape, *, shape=None) -> Distribution:
766
+ if self._bound:
767
+ raise BindDistributionError(f"{self} is already bound")
768
+
769
+ if self.ndim != 2:
770
+ raise NotImplementedError
771
+
772
+ if len(global_shape) != self.ndim:
773
+ raise ValueError(
774
+ f"Dimensionality of shapes ({len(global_shape)}) doesn't match dimensionality "
775
+ f"of this distribution ({self.ndim})"
776
+ )
777
+
778
+ rank = _get_process_group().rank
779
+ nrows, ncols = self._calc_local_shape(rank, self._block_sizes, global_shape)
780
+ if shape is not None and tuple(shape) != (nrows, ncols):
781
+ raise BindDistributionError(
782
+ f"The local shape {shape} on process {rank} is not the expected one based "
783
+ f"on the global shape {global_shape}, process grid {self._process_grid} and "
784
+ f"block sizes {self._block_sizes}: expected shape is {(nrows, ncols)}"
785
+ )
786
+
787
+ self._data_global_shape = global_shape
788
+ self._data_shape = (nrows, ncols)
789
+ self._bound = True
790
+ return self
791
+
792
+ def to(self, cls, /, *, ndim=None, copy=False):
793
+ super()._to_checks(cls, ndim)
794
+ nranks = _get_process_group().nranks
795
+ if ndim is None:
796
+ ndim = self.ndim
797
+ if issubclass(cls, BlockCyclic):
798
+ if type(self) is BlockCyclic and cls is BlockNonCyclic:
799
+ # TODO: BlockCyclic to BlockNonCyclic
800
+ raise NotImplementedError
801
+ if ndim > self.ndim:
802
+ if self._process_grid.process_array is not None:
803
+ raise NotImplementedError
804
+ d = self.copy()
805
+ d._process_grid = ProcessGrid(
806
+ shape=self._process_grid.shape + (1,) * (ndim - self.ndim),
807
+ layout=self._process_grid.layout,
808
+ )
809
+ d._first_process += (0,) * (ndim - self.ndim)
810
+ d._block_sizes += (1,) * (ndim - self.ndim)
811
+ if self._bound:
812
+ d._bound = False
813
+ d._bind(self._data_global_shape + (1,) * (ndim - self.ndim))
814
+ return d
815
+ return self.copy() if copy else self
816
+ elif cls is Slab:
817
+ if not self._bound:
818
+ # Without binding, it's a stretch to assume that this is compatible with
819
+ # Slab (the "cyclic" nature means that it's much more likely that it isn't).
820
+ raise ConvertDistributionError(
821
+ "Unbound BlockCyclic distribution can't be converted to Slab. "
822
+ "Consider using BlockNonCyclic if there is no cyclic distribution of blocks."
823
+ )
824
+
825
+ if not self._is_1d_distribution():
826
+ raise ConvertDistributionError(
827
+ "Can't convert this block distribution to Slab: partitioning must be on a single dimension"
828
+ )
829
+
830
+ # Data must be divisible on partition_dim and the block size must correspond
831
+ # to the slab size.
832
+ partition_dim = self._process_grid.shape.index(nranks)
833
+
834
+ if self._data_global_shape[partition_dim] % nranks != 0:
835
+ raise ConvertDistributionError(
836
+ "Can't convert this distribution to Slab: data doesn't divide evenly on partition dimension."
837
+ f" Global shape is {self._data_global_shape}, partition dimension is "
838
+ f"{partition_dim} and number of processes is {nranks}."
839
+ )
840
+
841
+ if self._block_sizes[partition_dim] != self._data_global_shape[partition_dim] // nranks:
842
+ raise ConvertDistributionError(
843
+ "This distribution can't be converted to Slab, because the block size in the "
844
+ f"partition dimension {partition_dim} is not a factor of the global extent in "
845
+ f"that dimension {self._data_global_shape[partition_dim]}"
846
+ )
847
+
848
+ data_global_shape = self._data_global_shape + (1,) * (ndim - self.ndim)
849
+ data_shape = self._data_shape + (1,) * (ndim - self.ndim)
850
+ d = Slab(partition_dim, ndim=ndim)
851
+ return d._bind(data_global_shape, shape=data_shape)
852
+ elif cls is Box:
853
+ # TODO: conversion to Box for non-1D distribution, and non-uniform partition
854
+ # sizes.
855
+ # Note that Box doesn't support cyclic.
856
+ if self._bound:
857
+ try:
858
+ slab = self.to(Slab, ndim=ndim)
859
+ except ConvertDistributionError:
860
+ raise NotImplementedError from None
861
+ return slab.to(Box)
862
+ raise NotImplementedError
863
+
864
+ def _global_shape_reduction_prologue(self, rank: int, local_shape: Sequence[int]) -> None:
865
+ if self.ndim != 2:
866
+ raise NotImplementedError("Global shape reduction for BlockCyclic with number of dimensions != 2 not implemented")
867
+
868
+ # To calculate the global shape when using 2D block distribution, we
869
+ # ignore the rows of processes that aren't in column 0 of the process
870
+ # grid, and the columns of processes that aren't in row 0 of the process
871
+ # grid (by setting the rows/columns to 0). We could do the same for 1D,
872
+ # but by preserving the shape info for 1D we can do some extra checks
873
+ # in _global_shape_reduction.
874
+ self._global_shape = list(local_shape)
875
+ if not self._process_grid._is_1d_distribution():
876
+ nprow, npcol = self._process_grid.shape
877
+ myprow = rank % nprow if self._process_grid.layout == ProcessGrid.Layout.COL_MAJOR else rank // npcol
878
+ mypcol = rank // nprow if self._process_grid.layout == ProcessGrid.Layout.COL_MAJOR else rank % npcol
879
+ if myprow != 0:
880
+ # Ignore my ncols in global shape calculation (because of overlap with
881
+ # ncols of process row 0).
882
+ self._global_shape[1] = 0
883
+ if mypcol != 0:
884
+ # Ignore my nrows in global shape calculation (because of overlap with
885
+ # nrows of process col 0).
886
+ self._global_shape[0] = 0
887
+
888
+ def _global_shape_reduction(self, other: Distribution) -> None:
889
+ if self is other:
890
+ return
891
+
892
+ if self != other:
893
+ raise ValueError(f"The distribution is inconsistent across processes: {self} != {other}")
894
+
895
+ partitioned_dims = (0,) if self._process_grid._is_row_wise() else (1,) if self._process_grid._is_col_wise() else (0, 1)
896
+
897
+ if len(partitioned_dims) == 1 and any(
898
+ self._global_shape[i] != other._global_shape[i] # type: ignore[attr-defined]
899
+ for i in (0, 1)
900
+ if i != partitioned_dims[0]
901
+ ):
902
+ raise ValueError(
903
+ "The problem size is inconsistent across processes:"
904
+ + str(self._global_shape)
905
+ + " vs "
906
+ + str(other._global_shape) # type: ignore[attr-defined]
907
+ )
908
+
909
+ # Reduce the partitioned dimensions to get the global size.
910
+ for dim in partitioned_dims:
911
+ self._global_shape[dim] += other._global_shape[dim] # type: ignore[attr-defined]
912
+
913
+ def _global_shape_reduction_epilogue(self) -> tuple[int, ...]:
914
+ return tuple(self._global_shape)
915
+
916
+
917
+ class BlockNonCyclic(BlockCyclic):
918
+ """Block distribution without cycles"""
919
+
920
+ def __init__(
921
+ self,
922
+ process_grid: ProcessGrid,
923
+ *,
924
+ first_process: Sequence[int] | None = None,
925
+ ):
926
+ super().__init__(process_grid, (-1,) * len(process_grid.shape), first_process=first_process)
927
+
928
+ def __eq__(self, other):
929
+ if isinstance(other, BlockNonCyclic) and (not other._bound or not self._bound):
930
+ # Don't compare block sizes since one of them is not bound.
931
+ return self._process_grid == other._process_grid and self._first_process == other._first_process
932
+ return super().__eq__(other)
933
+
934
+ def shape(self, rank: int, global_shape: Sequence[int] | None = None) -> tuple[int, ...]:
935
+ self._local_shape_checks(rank, self._process_grid._nranks, global_shape)
936
+ if global_shape is None:
937
+ global_shape = self._data_global_shape
938
+ if self._bound:
939
+ block_sizes = self._block_sizes
940
+ else:
941
+ block_sizes = self._infer_block_sizes(global_shape)
942
+ return self._calc_local_shape(rank, block_sizes, global_shape)
943
+
944
+ def _infer_block_sizes(self, global_shape):
945
+ if all(x % self._process_grid._shape[i] == 0 for i, x in enumerate(global_shape)):
946
+ block_sizes = tuple(x // self._process_grid._shape[i] for i, x in enumerate(global_shape))
947
+ else:
948
+ # The logic to bind this global shape to this distribution isn't implemented yet
949
+ # (which doesn't necessarily mean that it isn't possible to fit the data to this
950
+ # distribution).
951
+ raise NotImplementedError(
952
+ "BlockNonCyclic is currently only implemented for uniform partition sizes. "
953
+ "Use BlockCyclic with explicit block sizes instead."
954
+ )
955
+ return block_sizes
956
+
957
+ def _bind(self, global_shape, *, shape=None) -> Distribution:
958
+ if self._bound:
959
+ raise BindDistributionError(f"{self} is already bound")
960
+
961
+ if len(global_shape) != self.ndim:
962
+ raise ValueError(
963
+ f"Dimensionality of shapes ({len(global_shape)}) doesn't match dimensionality "
964
+ f"of this distribution ({self.ndim})"
965
+ )
966
+
967
+ # Now we assign block sizes to this distribution based on the shape of the data.
968
+ # NOTE: For dimensions that aren't partitioned, there are multiple blocks sizes that
969
+ # are valid and still fit the Block[Non]Cyclic model. This is because, in addition
970
+ # to block size being the full length L of the dimension, any L//N is also a valid
971
+ # block size (so a single block in that dimension is equivalent to N contiguous
972
+ # blocks in that dimension). The importance of this is that cuBLASMp actually
973
+ # requires configurations with these block sizes, since block sizes have to match
974
+ # across matrices A, B and C but they might be distributed differently.
975
+ # In other words, for distributed matmul with BlockNonCyclic, block sizes have to be
976
+ # inferred jointly with matrices A, B and C, but that is outside the scope of this
977
+ # method.
978
+
979
+ block_sizes = self._infer_block_sizes(global_shape)
980
+ if shape is not None and block_sizes != tuple(shape):
981
+ raise BindDistributionError("Data doesn't fit BlockNonCyclic distribution")
982
+
983
+ self._block_sizes = block_sizes
984
+ return super()._bind(global_shape, shape=shape)
985
+
986
+ def to(self, cls, /, *, ndim=None, copy=False):
987
+ super()._to_checks(cls, ndim)
988
+ nranks = _get_process_group().nranks
989
+ if ndim is None:
990
+ ndim = self.ndim
991
+ if issubclass(cls, BlockCyclic):
992
+ return super().to(cls, ndim=ndim, copy=copy)
993
+ elif cls is Slab:
994
+ if not self._is_1d_distribution():
995
+ raise ConvertDistributionError(
996
+ "Can't convert this block distribution to Slab: partitioning must be on a single dimension"
997
+ )
998
+
999
+ partition_dim = self._process_grid.shape.index(nranks)
1000
+ d = Slab(partition_dim, ndim=ndim)
1001
+ # For bound=False, we can allow the conversion and let Slab._bind() catch
1002
+ # any potential errors later.
1003
+ if self._bound:
1004
+ data_global_shape = self._data_global_shape + (1,) * (ndim - self.ndim)
1005
+ data_shape = self._data_shape + (1,) * (ndim - self.ndim)
1006
+ if data_global_shape[partition_dim] % nranks != 0:
1007
+ raise ConvertDistributionError(
1008
+ "Can't convert this distribution to Slab: data doesn't divide evenly on partition dimension"
1009
+ )
1010
+ d._bind(data_global_shape, shape=data_shape)
1011
+ return d
1012
+ elif cls is Box:
1013
+ return super().to(Box, ndim=ndim, copy=copy)
1014
+
1015
+
1016
+ def _get_process_group():
1017
+ distributed_ctx = dist.get_context()
1018
+ if distributed_ctx is None:
1019
+ raise RuntimeError(
1020
+ "nvmath.distributed has not been initialized. Refer to "
1021
+ "https://docs.nvidia.com/cuda/nvmath-python/latest/distributed-apis/runtime.html"
1022
+ " for more information."
1023
+ )
1024
+ return distributed_ctx.process_group