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,770 @@
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 typing import TypeAlias, cast
12
+
13
+ import nvmath.distributed as dist
14
+ from nvmath.bindings import (
15
+ cublasMp, # type: ignore
16
+ cufftMp, # type: ignore
17
+ )
18
+
19
+ __all__ = ["ProcessGrid", "Distribution", "Slab", "Box", "BlockCyclic", "BlockNonCyclic"]
20
+
21
+
22
+ class ProcessGrid:
23
+ """
24
+ N-dimensional grid of processes used by some distributions like the PBLAS block-cyclic
25
+ distribution.
26
+
27
+ Example 2D process grid for 4 processes, with processes arranged in column-major order::
28
+
29
+ ---------
30
+ | 0 | 2 |
31
+ ---------
32
+ | 1 | 3 |
33
+ ---------
34
+ """
35
+
36
+ Layout: TypeAlias = cublasMp.GridLayout
37
+
38
+ def __init__(
39
+ self,
40
+ *,
41
+ shape: Sequence[int] | None = None,
42
+ layout: ProcessGrid.Layout | None = None,
43
+ process_array=None,
44
+ ):
45
+ """
46
+ Create a new ProcessGrid object.
47
+
48
+ Args:
49
+ shape: Shape of the process grid.
50
+
51
+ layout: Layout of the process grid (column-major or row-major). This is optional
52
+ for 1D grid or when a custom grid is provided.
53
+
54
+ process_array: optional ndarray specifying custom arrangement of processes.
55
+ """
56
+ self._nranks = _get_process_group().nranks
57
+
58
+ if process_array is None:
59
+ if shape is None:
60
+ raise ValueError("shape must be provided when process_array=None")
61
+ self._shape = tuple(shape)
62
+ if layout is None:
63
+ if self._is_1d_distribution():
64
+ layout = ProcessGrid.Layout.ROW_MAJOR # layout doesn't matter in this case.
65
+ else:
66
+ raise ValueError("layout must be provided when process_array=None and partitioning on multiple dimensions")
67
+ if not isinstance(layout, ProcessGrid.Layout):
68
+ raise TypeError(f"layout must be of type ProcessGrid.Layout, got {layout}")
69
+ self._layout = layout
70
+ self._process_array = None
71
+ else:
72
+ self._shape = tuple(process_array.shape)
73
+ if shape is not None and self._shape != tuple(shape):
74
+ raise ValueError(f"shape {shape} and process_array.shape ({process_array.shape}) don't match")
75
+ # TODO: Can set layout to COL_MAJOR or ROW_MAJOR automatically if the
76
+ # process_array matches.
77
+ if layout is not None:
78
+ raise NotImplementedError
79
+ self._layout = None
80
+ self._process_array = process_array
81
+
82
+ if math.prod(self._shape) != self._nranks:
83
+ raise ValueError(
84
+ f"Number of grid elements ({math.prod(self._shape)}) must equal the number of processes ({self._nranks})"
85
+ )
86
+
87
+ @property
88
+ def shape(self) -> tuple[int, ...]:
89
+ """Shape of process grid."""
90
+ return self._shape
91
+
92
+ @property
93
+ def layout(self) -> ProcessGrid.Layout | None:
94
+ """Layout of process grid if row-major or column-major, otherwise None."""
95
+ return self._layout
96
+
97
+ @property
98
+ def process_array(self):
99
+ return self._process_array
100
+
101
+ def __str__(self):
102
+ return f"ProcessGrid(shape={self._shape}, layout={self._layout.name}, process_array={self._process_array})"
103
+
104
+ def __hash__(self):
105
+ # NOTE: The layout isn't considered for the hash, to allow process grids
106
+ # partitioned on a single dimension with the same shape but different layout
107
+ # to be the same dictionary key.
108
+ return hash(self._shape)
109
+
110
+ def __eq__(self, other):
111
+ if self._process_array is None and other._process_array is None:
112
+ if self._shape == other._shape:
113
+ return self._is_1d_distribution() or self._layout == other._layout
114
+ return False
115
+ raise NotImplementedError
116
+
117
+ def _is_1d_distribution(self) -> bool:
118
+ """True if process grid partitions on a single dimension."""
119
+ return self._nranks in self._shape
120
+
121
+ def _is_row_wise(self) -> bool:
122
+ """True if 2D process grid partitioned only on rows."""
123
+ return self._shape == (self._nranks, 1)
124
+
125
+ def _is_col_wise(self) -> bool:
126
+ """True if 2D process grid partitioned only on columns."""
127
+ return self._shape == (1, self._nranks)
128
+
129
+
130
+ class BindDistributionError(Exception):
131
+ pass
132
+
133
+
134
+ class ConvertDistributionError(Exception):
135
+ """Errors converting a distribution instance to another distribution type"""
136
+
137
+ pass
138
+
139
+
140
+ class Distribution(ABC):
141
+ """Specifies how a tensor is distributed across processes."""
142
+
143
+ def __init__(self):
144
+ self._bound = False
145
+
146
+ @property
147
+ @abstractmethod
148
+ def ndim(self) -> int | None:
149
+ """The number of dimensions of a distributed tensor for which this distribution
150
+ applies; None if it doesn't apply to any specific number of dimensions."""
151
+ raise NotImplementedError
152
+
153
+ @abstractmethod
154
+ def shape(self, rank: int, global_shape: Sequence[int] | None = None) -> tuple[int, ...]:
155
+ """Get the local shape of data on the given rank according to this distribution.
156
+
157
+ Args:
158
+ rank: the process rank for which to calculate the local shape.
159
+
160
+ global_shape: Global shape of data. Required if the distribution is
161
+ not bound to a global shape, otherwise not required.
162
+ """
163
+ raise NotImplementedError
164
+
165
+ @abstractmethod
166
+ def to(
167
+ self,
168
+ cls: type[Distribution],
169
+ /,
170
+ *,
171
+ ndim: int | None = None,
172
+ copy: bool = False,
173
+ ) -> Distribution:
174
+ """Convert this distribution object to an equivalent distribution of the given type.
175
+
176
+ Args:
177
+ cls: the target distribution type.
178
+
179
+ ndim: dimensionality of the target distribution. Must be compatible with the
180
+ dimensionality of the source distribution. This may be required if the
181
+ source distribution doesn't have associated dimensionality.
182
+
183
+ copy: Returns a copy if the source and target type are the same.
184
+
185
+ Raises:
186
+ ConvertDistributionError: if the conversion is not possible.
187
+ """
188
+ raise NotImplementedError
189
+
190
+ def _to_checks(self, cls: type[Distribution], ndim: int | None) -> None:
191
+ if cls is Distribution or not issubclass(cls, Distribution):
192
+ raise ValueError(f"{cls} is not a valid distribution")
193
+ if ndim is not None and self.ndim is not None and ndim != self.ndim:
194
+ raise ValueError(f"ndim argument ({ndim}) doesn't match this distribution's dimensionality ({self.ndim})")
195
+
196
+ @abstractmethod
197
+ def _bind(
198
+ self,
199
+ global_shape: Sequence[int],
200
+ *,
201
+ shape: Sequence[int] | None = None,
202
+ ) -> Distribution:
203
+ """Binds this distribution object to a global shape, which determines how a
204
+ distributed tensor with that shape must be partitioned among processes (the local
205
+ shape on each process). You can also provide the local shape on this process to
206
+ check if it fits the distribution (the function will raise an exception if not).
207
+ **The exception may be raised on some ranks but not others (it's up to the caller
208
+ to handle this)**.
209
+
210
+ Args:
211
+ global_shape: global shape of the data.
212
+
213
+ shape: shape of the data on this process.
214
+
215
+ Returns:
216
+ self
217
+
218
+ Raises:
219
+ BindDistributionError: if distribution is already bound or local shape doesn't
220
+ fit the distribution.
221
+ """
222
+ raise NotImplementedError
223
+
224
+ def _binding_str(self):
225
+ return f"[bound: global_shape={self._data_global_shape}, shape={self._data_shape}]" if self._bound else ""
226
+
227
+ def copy(self) -> Distribution:
228
+ """This is a common implementation for those distributions that only require
229
+ a shallow copy."""
230
+ return copy.copy(self)
231
+
232
+ def _local_shape_checks(self, rank: int, nranks: int, global_shape: Sequence[int] | None = None):
233
+ if not isinstance(rank, int):
234
+ raise ValueError(f"rank must be an integer, got {rank}")
235
+
236
+ if rank < 0 or rank > nranks - 1:
237
+ raise ValueError(f"This is not a valid process rank: got rank={rank} with nranks={nranks}")
238
+
239
+ if global_shape is None and not self._bound:
240
+ raise RuntimeError("This distribution is unbound: please specify a global shape")
241
+
242
+ if global_shape is not None and self._bound and tuple(global_shape) != self._data_global_shape: # type: ignore
243
+ raise ValueError(
244
+ "This distribution is already bound to a different global shape: provided "
245
+ f"{global_shape}, bound to {self._data_global_shape}" # type: ignore
246
+ )
247
+
248
+
249
+ class Slab(Distribution):
250
+ """
251
+ Slab distribution
252
+
253
+ Data is partitioned across processes on a single axis, such that:
254
+
255
+ - The shape of the slab on the first s_p % P processes is
256
+ (s_0, ..., s_p // P + 1, ..., s_{n-1})
257
+ - The shape of the slab on the remaining processes is (s_0, ..., s_p // P, ..., s_{n-1})
258
+ - Process 0 owns the first slab according to the global index order, process 1 owns
259
+ the second slab and so on.
260
+
261
+ where:
262
+
263
+ - s_i is the size of dimension i of the global array
264
+ - p is the partition dimension
265
+ - n is the number of dimensions of the array
266
+ - P is the number of processes
267
+ """
268
+
269
+ X: Slab
270
+ """Slab distribution on axis 0."""
271
+
272
+ Y: Slab
273
+ """Slab distribution on axis 1."""
274
+
275
+ Z: Slab
276
+ """Slab distribution on axis 2."""
277
+
278
+ def __init__(
279
+ self,
280
+ partition_dim: int,
281
+ ndim: int | None = None,
282
+ ):
283
+ super().__init__()
284
+ if not isinstance(partition_dim, int) or partition_dim < 0:
285
+ raise ValueError(f"partition_dim must be integer >= 0, got {partition_dim}")
286
+
287
+ if ndim is not None:
288
+ if not isinstance(ndim, int) or ndim < 1:
289
+ raise ValueError(f"ndim must be integer >= 1, got {ndim}")
290
+ if partition_dim >= ndim:
291
+ raise ValueError("partition_dim must be < ndim")
292
+
293
+ self._partition_dim = partition_dim
294
+ self._ndim = ndim
295
+
296
+ def __eq__(self, other):
297
+ if not isinstance(other, Slab):
298
+ return False
299
+ if self._partition_dim != other._partition_dim:
300
+ return False
301
+ if self._ndim is None or other._ndim is None:
302
+ return True
303
+ return self._ndim == other._ndim
304
+
305
+ def __hash__(self):
306
+ return self._partition_dim
307
+
308
+ def __str__(self):
309
+ return f"Slab(partition_dim={self._partition_dim}, ndim={self._ndim})" + self._binding_str()
310
+
311
+ @property
312
+ def name(self) -> str:
313
+ match self._partition_dim:
314
+ case 0:
315
+ return "Slab.X"
316
+ case 1:
317
+ return "Slab.Y"
318
+ case 2:
319
+ return "Slab.Z"
320
+ case _:
321
+ return "Slab"
322
+
323
+ @property
324
+ def partition_dim(self) -> int:
325
+ """Slab partition dimension"""
326
+ return self._partition_dim
327
+
328
+ @property
329
+ def ndim(self) -> int | None:
330
+ return self._ndim
331
+
332
+ def shape(self, rank: int, global_shape: Sequence[int] | None = None) -> tuple[int, ...]:
333
+ n = _get_process_group().nranks
334
+ self._local_shape_checks(rank, n, global_shape)
335
+ if global_shape is None:
336
+ global_shape = self._data_global_shape
337
+
338
+ S = global_shape[self._partition_dim]
339
+ partition_dim_local_size = S // n + bool(rank < S % n)
340
+ slab_shape = list(global_shape)
341
+ slab_shape[self._partition_dim] = partition_dim_local_size
342
+ return tuple(slab_shape)
343
+
344
+ @property
345
+ def _cufftmp_value(self):
346
+ if self._partition_dim not in (0, 1):
347
+ raise TypeError(f"Unsupported distribution {self} for cuFFTMp: partition dimension must be X or Y")
348
+ return cufftMp.XtSubFormat.FORMAT_INPLACE if self._partition_dim == 0 else cufftMp.XtSubFormat.FORMAT_INPLACE_SHUFFLED
349
+
350
+ def _bind(self, global_shape, *, shape=None) -> Slab:
351
+ if self._bound:
352
+ raise BindDistributionError(f"{self} is already bound")
353
+
354
+ if self._ndim is not None and len(global_shape) != self._ndim:
355
+ raise ValueError(f"The given shape doesn't have the same dimensionality as this {self} distribution")
356
+
357
+ if self._partition_dim >= len(global_shape):
358
+ raise ValueError("partition_dim must be < ndim")
359
+
360
+ rank = _get_process_group().rank
361
+ slab_shape = self.shape(rank, global_shape)
362
+
363
+ if shape is not None and tuple(shape) != slab_shape:
364
+ raise BindDistributionError(
365
+ f"The given shapes (global_shape={global_shape}, shape={shape}) don't fit distribution {str(self)}"
366
+ )
367
+
368
+ self._ndim = len(global_shape)
369
+ self._data_global_shape = tuple(global_shape)
370
+ self._data_shape = slab_shape
371
+ self._bound = True
372
+ return self
373
+
374
+ def to(self, cls, /, *, ndim=None, copy=False):
375
+ super()._to_checks(cls, ndim)
376
+ nranks = _get_process_group().nranks
377
+
378
+ if ndim is None:
379
+ ndim = self._ndim
380
+ elif self._partition_dim >= ndim:
381
+ raise ValueError(f"ndim ({ndim}) must be greater than the partition dimension ({self._partition_dim})")
382
+
383
+ if cls is Slab:
384
+ if copy or (ndim is not None and self._ndim is None):
385
+ d = cast(Slab, self.copy())
386
+ d._ndim = ndim
387
+ return d
388
+ return self
389
+ elif issubclass(cls, BlockCyclic):
390
+ if ndim is None:
391
+ raise ConvertDistributionError("Can't convert Slab distribution to BlockCyclic: unknown dimensionality")
392
+
393
+ process_grid_shape = tuple(1 if x != self._partition_dim else nranks for x in range(ndim))
394
+ # layout doesn't matter when partitioning on a single axis.
395
+ process_grid = ProcessGrid(shape=process_grid_shape, layout=ProcessGrid.Layout.ROW_MAJOR)
396
+ if not self._bound:
397
+ return BlockNonCyclic(process_grid)
398
+ else:
399
+ b = BlockNonCyclic(process_grid)
400
+ return b._bind(self._data_global_shape, shape=self._data_shape)
401
+ elif cls is Box:
402
+ raise NotImplementedError
403
+
404
+
405
+ # Define alternate forms for the user to specify Slab on X, Y or Z.
406
+ # NOTE: dimensionality is left unspecified when using these (but will be set when the
407
+ # distribution is bound to data).
408
+ Slab.X = Slab(0)
409
+ Slab.Y = Slab(1)
410
+ Slab.Z = Slab(2)
411
+
412
+
413
+ class Box(Distribution):
414
+ """Box distribution"""
415
+
416
+ def __init__(
417
+ self,
418
+ lower: Sequence[int],
419
+ upper: Sequence[int],
420
+ ):
421
+ super().__init__()
422
+ if len(lower) != len(upper):
423
+ raise ValueError("lower and upper coordinates must have the same dimensionality")
424
+ for coords in (lower, upper):
425
+ if not all(isinstance(x, int) for x in coords):
426
+ raise ValueError("lower and upper coordinates must be integer")
427
+ if not all(upper[i] > lower[i] for i in range(len(upper))):
428
+ raise ValueError(
429
+ f"The upper coordinates must be larger than the lower coordinates, but got lower={lower} upper={upper}"
430
+ )
431
+ self._lower = tuple(lower)
432
+ self._upper = tuple(upper)
433
+
434
+ @property
435
+ def lower(self) -> tuple[int, ...]:
436
+ """Box lower coordinates"""
437
+ return self._lower
438
+
439
+ @property
440
+ def upper(self) -> tuple[int, ...]:
441
+ """Box upper coordinates"""
442
+ return self._upper
443
+
444
+ @property
445
+ def ndim(self) -> int:
446
+ return len(self._lower)
447
+
448
+ def shape(self, rank: int, global_shape: Sequence[int] | None = None) -> tuple[int, ...]:
449
+ process_group = _get_process_group()
450
+ if rank != process_group.rank:
451
+ raise RuntimeError("Can't calculate local shape of peer process with Box distribution")
452
+ nranks = process_group.nranks
453
+ self._local_shape_checks(rank, nranks, global_shape)
454
+ return tuple(self._upper[i] - self._lower[i] for i in range(self.ndim))
455
+
456
+ def __str__(self):
457
+ return f"Box(lower={self._lower}, upper={self._upper})" + self._binding_str()
458
+
459
+ def __eq__(self, other):
460
+ if not isinstance(other, Box):
461
+ return False
462
+ return self._lower == other._lower and self._upper == other._upper
463
+
464
+ def __hash__(self):
465
+ return hash((self._lower, self._upper))
466
+
467
+ def __iter__(self):
468
+ # To allow unpacking
469
+ yield self._lower
470
+ yield self._upper
471
+
472
+ def __getitem__(self, index):
473
+ if index not in (0, 1):
474
+ return IndexError(f"Index must be 0 or 1, got {index}")
475
+ return self._lower if index == 0 else self._upper
476
+
477
+ def _bind(self, global_shape, *, shape=None) -> Distribution:
478
+ if self._bound:
479
+ raise BindDistributionError(f"{self} is already bound")
480
+ my_shape = tuple(self._upper[i] - self._lower[i] for i in range(self.ndim))
481
+ if shape is not None and tuple(shape) != my_shape:
482
+ raise BindDistributionError(
483
+ f"The given shapes don't fit this Box distribution: {global_shape} "
484
+ f"and {shape}, lower={self._lower} upper={self._upper}"
485
+ )
486
+ self._data_global_shape = tuple(global_shape)
487
+ self._data_shape = my_shape
488
+ self._bound = True
489
+ return self
490
+
491
+ def to(self, cls, /, *, ndim=None, copy=False):
492
+ super()._to_checks(cls, ndim)
493
+ if cls is Box:
494
+ return self.copy() if copy else self
495
+ raise NotImplementedError
496
+
497
+
498
+ class BlockCyclic(Distribution):
499
+ """Block-cyclic distribution"""
500
+
501
+ def __init__(
502
+ self,
503
+ process_grid: ProcessGrid,
504
+ block_sizes: Sequence[int],
505
+ *,
506
+ first_process: Sequence[int] | None = None,
507
+ ):
508
+ super().__init__()
509
+ if block_sizes is None or not all(isinstance(x, int) for x in block_sizes):
510
+ raise ValueError(f"Must provide a sequence of integer block sizes, got {block_sizes}")
511
+ if len(block_sizes) != len(process_grid.shape):
512
+ raise ValueError(
513
+ f"Number of block sizes ({len(block_sizes)}) doesn't match dimensionality ({len(process_grid.shape)})"
514
+ )
515
+ self._process_grid = process_grid
516
+ self._block_sizes = tuple(block_sizes)
517
+ if first_process is None:
518
+ self._first_process = (0,) * self.ndim
519
+ else:
520
+ if not all(isinstance(x, int) for x in first_process):
521
+ raise ValueError(f"first_process must be a sequence of integer coordinates, got {first_process}")
522
+ for i, x in enumerate(first_process):
523
+ if x < 0 or x >= process_grid.shape[i]:
524
+ raise ValueError(
525
+ f"first_process {first_process} is not a valid index into the process grid of "
526
+ f"shape {process_grid.shape}"
527
+ )
528
+ self._first_process = tuple(first_process)
529
+
530
+ @property
531
+ def process_grid(self) -> ProcessGrid:
532
+ """The process grid of this BlockCyclic distribution"""
533
+ return self._process_grid
534
+
535
+ @property
536
+ def ndim(self) -> int:
537
+ return len(self._process_grid.shape)
538
+
539
+ @property
540
+ def block_sizes(self) -> tuple[int, ...]:
541
+ """The block sizes of this BlockCyclic distribution"""
542
+ return self._block_sizes
543
+
544
+ @property
545
+ def first_process(self) -> tuple[int, ...]:
546
+ """Index in the process grid of the process who owns the first block of the
547
+ distributed tensor."""
548
+ return self._first_process
549
+
550
+ def __str__(self):
551
+ return (
552
+ f"{self.__class__.__name__}(process_grid={self._process_grid}, block_sizes={self._block_sizes})"
553
+ + self._binding_str()
554
+ )
555
+
556
+ def __eq__(self, other):
557
+ if not isinstance(other, BlockCyclic):
558
+ return False
559
+ return (
560
+ self._process_grid == other._process_grid
561
+ and self._block_sizes == other._block_sizes
562
+ and self._first_process == other._first_process
563
+ )
564
+
565
+ def _is_1d_distribution(self) -> bool:
566
+ """True if process grid partitions on a single dimension."""
567
+ return self._process_grid._is_1d_distribution()
568
+
569
+ def _is_row_wise(self) -> bool:
570
+ """True if 2D process grid partitioned only on rows."""
571
+ return self._process_grid._is_row_wise()
572
+
573
+ def _is_col_wise(self) -> bool:
574
+ """True if 2D process grid partitioned only on columns."""
575
+ return self._process_grid._is_col_wise()
576
+
577
+ def shape(self, rank: int, global_shape: Sequence[int] | None = None) -> tuple[int, ...]:
578
+ self._local_shape_checks(rank, self._process_grid._nranks, global_shape)
579
+ if global_shape is None:
580
+ global_shape = self._data_global_shape
581
+ return self._calc_local_shape(rank, self._block_sizes, global_shape)
582
+
583
+ def _calc_local_shape(self, rank, block_sizes, global_shape):
584
+ nprow, npcol = self._process_grid._shape
585
+ layout = self._process_grid._layout
586
+ if layout is not None:
587
+ myprow = rank % nprow if layout == ProcessGrid.Layout.COL_MAJOR else rank // npcol
588
+ mypcol = rank // nprow if layout == ProcessGrid.Layout.COL_MAJOR else rank % npcol
589
+ index = (myprow, mypcol)
590
+ else:
591
+ raise NotImplementedError
592
+ nrows = cublasMp.numroc(global_shape[0], block_sizes[0], index[0], self._first_process[0], nprow)
593
+ ncols = cublasMp.numroc(global_shape[1], block_sizes[1], index[1], self._first_process[1], npcol)
594
+ return (nrows, ncols)
595
+
596
+ def _bind(self, global_shape, *, shape=None) -> Distribution:
597
+ if self._bound:
598
+ raise BindDistributionError(f"{self} is already bound")
599
+
600
+ if self.ndim != 2:
601
+ raise NotImplementedError
602
+
603
+ if len(global_shape) != self.ndim:
604
+ raise ValueError(
605
+ f"Dimensionality of shapes ({len(global_shape)}) doesn't match dimensionality "
606
+ f"of this distribution ({self.ndim})"
607
+ )
608
+
609
+ rank = _get_process_group().rank
610
+ nrows, ncols = self._calc_local_shape(rank, self._block_sizes, global_shape)
611
+ if shape is not None and tuple(shape) != (nrows, ncols):
612
+ raise BindDistributionError(
613
+ f"The local shape {shape} on process {rank} is not the expected one based "
614
+ f"on the global shape {global_shape}, process grid {self._process_grid} and "
615
+ f"block sizes {self._block_sizes}: expected shape is {(nrows, ncols)}"
616
+ )
617
+
618
+ self._data_global_shape = global_shape
619
+ self._data_shape = (nrows, ncols)
620
+ self._bound = True
621
+ return self
622
+
623
+ def to(self, cls, /, *, ndim=None, copy=False):
624
+ super()._to_checks(cls, ndim)
625
+ nranks = _get_process_group().nranks
626
+ if issubclass(cls, BlockCyclic):
627
+ return self.copy() if copy else self
628
+ elif cls is Slab:
629
+ if not self._bound:
630
+ # Without binding, it's a stretch to assume that this is compatible with
631
+ # Slab (the "cyclic" nature means that it's much more likely that it isn't).
632
+ raise ConvertDistributionError(
633
+ "Unbound BlockCyclic distribution can't be converted to Slab. "
634
+ "Consider using BlockNonCyclic if there is no cyclic distribution of blocks."
635
+ )
636
+
637
+ if not self._is_1d_distribution():
638
+ raise ConvertDistributionError(
639
+ "Can't convert this block distribution to Slab: partitioning must be on a single dimension"
640
+ )
641
+
642
+ # Data must be divisible on partition_dim and the block size must correspond
643
+ # to the slab size.
644
+ partition_dim = self._process_grid.shape.index(nranks)
645
+
646
+ if self._data_global_shape[partition_dim] % nranks != 0:
647
+ raise ConvertDistributionError(
648
+ "Can't convert this distribution to Slab: data doesn't divide evenly on partition dimension."
649
+ f" Global shape is {self._data_global_shape}, partition dimension is "
650
+ f"{partition_dim} and number of processes is {nranks}."
651
+ )
652
+
653
+ if self._block_sizes[partition_dim] != self._data_global_shape[partition_dim] // nranks:
654
+ raise ConvertDistributionError(
655
+ "This distribution can't be converted to Slab, because the block size in the "
656
+ f"partition dimension {partition_dim} is not a factor of the global extent in "
657
+ f"that dimension {self._data_global_shape[partition_dim]}"
658
+ )
659
+ d = Slab(partition_dim, ndim=self.ndim)
660
+ return d._bind(self._data_global_shape, self._data_shape)
661
+ elif cls is Box:
662
+ raise NotImplementedError
663
+
664
+
665
+ class BlockNonCyclic(BlockCyclic):
666
+ """Block distribution without cycles"""
667
+
668
+ def __init__(
669
+ self,
670
+ process_grid: ProcessGrid,
671
+ *,
672
+ first_process: Sequence[int] | None = None,
673
+ ):
674
+ super().__init__(process_grid, (-1,) * len(process_grid.shape), first_process=first_process)
675
+
676
+ def __eq__(self, other):
677
+ if isinstance(other, BlockNonCyclic) and (not other._bound or not self._bound):
678
+ # Don't compare block sizes since one of them is not bound.
679
+ return self._process_grid == other._process_grid and self._first_process == other._first_process
680
+ return super().__eq__(other)
681
+
682
+ def shape(self, rank: int, global_shape: Sequence[int] | None = None) -> tuple[int, ...]:
683
+ self._local_shape_checks(rank, self._process_grid._nranks, global_shape)
684
+ if global_shape is None:
685
+ global_shape = self._data_global_shape
686
+ if self._bound:
687
+ block_sizes = self._block_sizes
688
+ else:
689
+ block_sizes = self._infer_block_sizes(global_shape)
690
+ return self._calc_local_shape(rank, block_sizes, global_shape)
691
+
692
+ def _infer_block_sizes(self, global_shape):
693
+ if all(x % self._process_grid._shape[i] == 0 for i, x in enumerate(global_shape)):
694
+ block_sizes = tuple(x // self._process_grid._shape[i] for i, x in enumerate(global_shape))
695
+ else:
696
+ # The logic to bind this global shape to this distribution isn't implemented yet
697
+ # (which doesn't necessarily mean that it isn't possible to fit the data to this
698
+ # distribution).
699
+ raise NotImplementedError(
700
+ "BlockNonCyclic is currently only implemented for uniform partition sizes. "
701
+ "Use BlockCyclic with explicit block sizes instead."
702
+ )
703
+ return block_sizes
704
+
705
+ def _bind(self, global_shape, *, shape=None) -> Distribution:
706
+ if self._bound:
707
+ raise BindDistributionError(f"{self} is already bound")
708
+
709
+ assert all(x == -1 for x in self._block_sizes)
710
+
711
+ if len(global_shape) != self.ndim:
712
+ raise ValueError(
713
+ f"Dimensionality of shapes ({len(global_shape)}) doesn't match dimensionality "
714
+ f"of this distribution ({self.ndim})"
715
+ )
716
+
717
+ # Now we assign block sizes to this distribution based on the shape of the data.
718
+ # NOTE: For dimensions that aren't partitioned, there are multiple blocks sizes that
719
+ # are valid and still fit the Block[Non]Cyclic model. This is because, in addition
720
+ # to block size being the full length L of the dimension, any L//N is also a valid
721
+ # block size (so a single block in that dimension is equivalent to N contiguous
722
+ # blocks in that dimension). The importance of this is that cuBLASMp actually
723
+ # requires configurations with these block sizes, since block sizes have to match
724
+ # across matrices A, B and C but they might be distributed differently.
725
+ # In other words, for distributed matmul with BlockNonCyclic, block sizes have to be
726
+ # inferred jointly with matrices A, B and C, but that is outside the scope of this
727
+ # method.
728
+
729
+ block_sizes = self._infer_block_sizes(global_shape)
730
+ if shape is not None and block_sizes != tuple(shape):
731
+ raise BindDistributionError("Data doesn't fit BlockNonCyclic distribution")
732
+
733
+ self._block_sizes = block_sizes
734
+ return super()._bind(global_shape, shape=shape)
735
+
736
+ def to(self, cls, /, *, ndim=None, copy=False):
737
+ super()._to_checks(cls, ndim)
738
+ nranks = _get_process_group().nranks
739
+ if issubclass(cls, BlockCyclic):
740
+ return self.copy() if copy else self
741
+ elif cls is Slab:
742
+ if not self._is_1d_distribution():
743
+ raise ConvertDistributionError(
744
+ "Can't convert this block distribution to Slab: partitioning must be on a single dimension"
745
+ )
746
+
747
+ partition_dim = self._process_grid.shape.index(nranks)
748
+ d = Slab(partition_dim, ndim=self.ndim)
749
+ # For bound=False, we can allow the conversion and let Slab._bind() catch
750
+ # any potential errors later.
751
+ if self._bound:
752
+ if self._data_global_shape[partition_dim] % nranks != 0:
753
+ raise ConvertDistributionError(
754
+ "Can't convert this distribution to Slab: data doesn't divide evenly on partition dimension"
755
+ )
756
+ d._bind(self._data_global_shape, shape=self._data_shape)
757
+ return d
758
+ elif cls is Box:
759
+ raise NotImplementedError
760
+
761
+
762
+ def _get_process_group():
763
+ distributed_ctx = dist.get_context()
764
+ if distributed_ctx is None:
765
+ raise RuntimeError(
766
+ "nvmath.distributed has not been initialized. Refer to "
767
+ "https://docs.nvidia.com/cuda/nvmath-python/latest/distributed-apis/runtime.html"
768
+ " for more information."
769
+ )
770
+ return distributed_ctx.process_group