nvmath-python 0.5.0__cp313-cp313-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. nvmath/__init__.pxd +0 -0
  2. nvmath/__init__.py +37 -0
  3. nvmath/_internal/__init__.py +0 -0
  4. nvmath/_internal/layout.py +58 -0
  5. nvmath/_utils.py +363 -0
  6. nvmath/bindings/__init__.pxd +0 -0
  7. nvmath/bindings/__init__.py +44 -0
  8. nvmath/bindings/_internal/__init__.pxd +0 -0
  9. nvmath/bindings/_internal/__init__.py +0 -0
  10. nvmath/bindings/_internal/cublas.cp313-win_amd64.pyd +0 -0
  11. nvmath/bindings/_internal/cublas.pxd +518 -0
  12. nvmath/bindings/_internal/cublasLt.cp313-win_amd64.pyd +0 -0
  13. nvmath/bindings/_internal/cublasLt.pxd +54 -0
  14. nvmath/bindings/_internal/cudss.cp313-win_amd64.pyd +0 -0
  15. nvmath/bindings/_internal/cudss.pxd +44 -0
  16. nvmath/bindings/_internal/cufft.cp313-win_amd64.pyd +0 -0
  17. nvmath/bindings/_internal/cufft.pxd +69 -0
  18. nvmath/bindings/_internal/cufftMp.pxd +77 -0
  19. nvmath/bindings/_internal/curand.cp313-win_amd64.pyd +0 -0
  20. nvmath/bindings/_internal/curand.pxd +42 -0
  21. nvmath/bindings/_internal/cusolver.cp313-win_amd64.pyd +0 -0
  22. nvmath/bindings/_internal/cusolver.pxd +15 -0
  23. nvmath/bindings/_internal/cusolverDn.cp313-win_amd64.pyd +0 -0
  24. nvmath/bindings/_internal/cusolverDn.pxd +386 -0
  25. nvmath/bindings/_internal/cusparse.cp313-win_amd64.pyd +0 -0
  26. nvmath/bindings/_internal/cusparse.pxd +270 -0
  27. nvmath/bindings/_internal/mathdx.cp313-win_amd64.pyd +0 -0
  28. nvmath/bindings/_internal/mathdx.pxd +79 -0
  29. nvmath/bindings/_internal/nvshmem.pxd +29 -0
  30. nvmath/bindings/_internal/utils.cp313-win_amd64.pyd +0 -0
  31. nvmath/bindings/_internal/utils.pxd +186 -0
  32. nvmath/bindings/cublas.cp313-win_amd64.pyd +0 -0
  33. nvmath/bindings/cublas.pxd +545 -0
  34. nvmath/bindings/cublas.pyi +18 -0
  35. nvmath/bindings/cublasLt.cp313-win_amd64.pyd +0 -0
  36. nvmath/bindings/cublasLt.pxd +99 -0
  37. nvmath/bindings/cudss.cp313-win_amd64.pyd +0 -0
  38. nvmath/bindings/cudss.pxd +83 -0
  39. nvmath/bindings/cudss.pyi +249 -0
  40. nvmath/bindings/cufft.cp313-win_amd64.pyd +0 -0
  41. nvmath/bindings/cufft.pxd +114 -0
  42. nvmath/bindings/cufftMp.pxd +124 -0
  43. nvmath/bindings/curand.cp313-win_amd64.pyd +0 -0
  44. nvmath/bindings/curand.pxd +71 -0
  45. nvmath/bindings/cusolver.cp313-win_amd64.pyd +0 -0
  46. nvmath/bindings/cusolver.pxd +60 -0
  47. nvmath/bindings/cusolverDn.cp313-win_amd64.pyd +0 -0
  48. nvmath/bindings/cusolverDn.pxd +410 -0
  49. nvmath/bindings/cusparse.cp313-win_amd64.pyd +0 -0
  50. nvmath/bindings/cusparse.pxd +329 -0
  51. nvmath/bindings/cycublas.cp313-win_amd64.pyd +0 -0
  52. nvmath/bindings/cycublas.pxd +657 -0
  53. nvmath/bindings/cycublasLt.cp313-win_amd64.pyd +0 -0
  54. nvmath/bindings/cycublasLt.pxd +1010 -0
  55. nvmath/bindings/cycudss.cp313-win_amd64.pyd +0 -0
  56. nvmath/bindings/cycudss.pxd +191 -0
  57. nvmath/bindings/cycufft.cp313-win_amd64.pyd +0 -0
  58. nvmath/bindings/cycufft.pxd +320 -0
  59. nvmath/bindings/cycufftMp.pxd +333 -0
  60. nvmath/bindings/cycurand.cp313-win_amd64.pyd +0 -0
  61. nvmath/bindings/cycurand.pxd +146 -0
  62. nvmath/bindings/cycusolver.cp313-win_amd64.pyd +0 -0
  63. nvmath/bindings/cycusolver.pxd +143 -0
  64. nvmath/bindings/cycusolverDn.cp313-win_amd64.pyd +0 -0
  65. nvmath/bindings/cycusolverDn.pxd +414 -0
  66. nvmath/bindings/cycusparse.cp313-win_amd64.pyd +0 -0
  67. nvmath/bindings/cycusparse.pxd +476 -0
  68. nvmath/bindings/cymathdx.cp313-win_amd64.pyd +0 -0
  69. nvmath/bindings/cymathdx.pxd +363 -0
  70. nvmath/bindings/cynvshmem.pxd +126 -0
  71. nvmath/bindings/mathdx.cp313-win_amd64.pyd +0 -0
  72. nvmath/bindings/mathdx.pxd +131 -0
  73. nvmath/bindings/mathdx.pyi +686 -0
  74. nvmath/bindings/nvpl/__init__.pxd +0 -0
  75. nvmath/bindings/nvpl/__init__.py +11 -0
  76. nvmath/bindings/nvpl/_internal/__init__.pxd +0 -0
  77. nvmath/bindings/nvpl/_internal/__init__.py +0 -0
  78. nvmath/bindings/nvpl/_internal/fft.pxd +36 -0
  79. nvmath/bindings/nvpl/cyfft.pxd +87 -0
  80. nvmath/bindings/nvpl/fft.pxd +100 -0
  81. nvmath/bindings/nvshmem.pxd +54 -0
  82. nvmath/device/__init__.py +23 -0
  83. nvmath/device/_deprecated.py +33 -0
  84. nvmath/device/caching.py +51 -0
  85. nvmath/device/common.py +131 -0
  86. nvmath/device/common_backend.py +125 -0
  87. nvmath/device/common_cuda.py +156 -0
  88. nvmath/device/common_mathdx.py +97 -0
  89. nvmath/device/common_numba.py +159 -0
  90. nvmath/device/common_opaque_tensor.py +128 -0
  91. nvmath/device/cublasdx.py +1085 -0
  92. nvmath/device/cublasdx_backend.py +552 -0
  93. nvmath/device/cublasdx_numba.py +528 -0
  94. nvmath/device/cufftdx.py +551 -0
  95. nvmath/device/cufftdx_backend.py +194 -0
  96. nvmath/device/cufftdx_numba.py +123 -0
  97. nvmath/device/curand_kernel.py +9147 -0
  98. nvmath/device/nvrtc.py +95 -0
  99. nvmath/device/patch.py +35 -0
  100. nvmath/device/random.py +417 -0
  101. nvmath/device/random_helpers.py +23 -0
  102. nvmath/device/random_states.py +189 -0
  103. nvmath/device/types.py +11 -0
  104. nvmath/device/vector_types_numba.py +203 -0
  105. nvmath/distributed/__init__.py +117 -0
  106. nvmath/distributed/_internal/__init__.py +0 -0
  107. nvmath/distributed/_internal/nvshmem.py +243 -0
  108. nvmath/distributed/_internal/tensor_ifc_cupy.py +111 -0
  109. nvmath/distributed/_internal/tensor_ifc_numpy.py +53 -0
  110. nvmath/distributed/_internal/tensor_ifc_torch.py +108 -0
  111. nvmath/distributed/_internal/tensor_wrapper.py +59 -0
  112. nvmath/distributed/_utils.py +152 -0
  113. nvmath/distributed/fft/__init__.py +6 -0
  114. nvmath/distributed/fft/_configuration.py +86 -0
  115. nvmath/distributed/fft/fft.py +1737 -0
  116. nvmath/distributed/reshape/__init__.py +6 -0
  117. nvmath/distributed/reshape/_configuration.py +39 -0
  118. nvmath/distributed/reshape/reshape.py +1249 -0
  119. nvmath/fft/__init__.py +7 -0
  120. nvmath/fft/_configuration.py +208 -0
  121. nvmath/fft/_exec_utils.py +116 -0
  122. nvmath/fft/_helpers.py +260 -0
  123. nvmath/fft/fft.py +2346 -0
  124. nvmath/internal/__init__.py +5 -0
  125. nvmath/internal/enum_utils.py +142 -0
  126. nvmath/internal/formatters.py +87 -0
  127. nvmath/internal/mem_limit.py +51 -0
  128. nvmath/internal/package_ifc.py +119 -0
  129. nvmath/internal/package_ifc_cuda.py +43 -0
  130. nvmath/internal/package_ifc_cupy.py +49 -0
  131. nvmath/internal/package_ifc_torch.py +31 -0
  132. nvmath/internal/package_wrapper.py +14 -0
  133. nvmath/internal/tensor_ifc.py +143 -0
  134. nvmath/internal/tensor_ifc_cupy.py +185 -0
  135. nvmath/internal/tensor_ifc_numpy.py +157 -0
  136. nvmath/internal/tensor_ifc_torch.py +115 -0
  137. nvmath/internal/tensor_wrapper.py +160 -0
  138. nvmath/internal/typemaps.py +154 -0
  139. nvmath/internal/utils.py +634 -0
  140. nvmath/linalg/__init__.py +11 -0
  141. nvmath/linalg/_internal/__init__.py +3 -0
  142. nvmath/linalg/_internal/algo_cap_ifc.py +86 -0
  143. nvmath/linalg/_internal/algo_config_ifc.py +87 -0
  144. nvmath/linalg/_internal/enum_to_tuples.py +64 -0
  145. nvmath/linalg/_internal/epilog_protocol.py +738 -0
  146. nvmath/linalg/_internal/matmul_desc_ifc.py +72 -0
  147. nvmath/linalg/_internal/matmul_pref_ifc.py +65 -0
  148. nvmath/linalg/_internal/matrix_layout_ifc.py +59 -0
  149. nvmath/linalg/_internal/typemaps.py +135 -0
  150. nvmath/linalg/_internal/utils.py +96 -0
  151. nvmath/linalg/advanced/__init__.py +8 -0
  152. nvmath/linalg/advanced/_algorithmmod.py +142 -0
  153. nvmath/linalg/advanced/_configuration.py +320 -0
  154. nvmath/linalg/advanced/helpers/__init__.py +5 -0
  155. nvmath/linalg/advanced/helpers/matmul.py +173 -0
  156. nvmath/linalg/advanced/matmulmod.py +2855 -0
  157. nvmath/memory.py +266 -0
  158. nvmath/sparse/__init__.py +18 -0
  159. nvmath/sparse/_internal/__init__.py +21 -0
  160. nvmath/sparse/_internal/common_utils.py +81 -0
  161. nvmath/sparse/_internal/cudss_config_ifc.py +584 -0
  162. nvmath/sparse/_internal/cudss_data_ifc.py +291 -0
  163. nvmath/sparse/_internal/cudss_utils.py +398 -0
  164. nvmath/sparse/_internal/sparse_csr_ifc.py +154 -0
  165. nvmath/sparse/_internal/sparse_format_helpers.py +88 -0
  166. nvmath/sparse/_internal/sparse_tensor_ifc.py +80 -0
  167. nvmath/sparse/advanced/__init__.py +7 -0
  168. nvmath/sparse/advanced/_configuration.py +150 -0
  169. nvmath/sparse/advanced/direct_solver.py +1705 -0
  170. nvmath_python-0.5.0.dist-info/METADATA +122 -0
  171. nvmath_python-0.5.0.dist-info/RECORD +174 -0
  172. nvmath_python-0.5.0.dist-info/WHEEL +5 -0
  173. nvmath_python-0.5.0.dist-info/licenses/LICENSE +177 -0
  174. nvmath_python-0.5.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1249 @@
1
+ # Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ __all__ = ["Reshape", "reshape"]
6
+
7
+ import logging
8
+ from collections.abc import Sequence
9
+ from dataclasses import dataclass
10
+ from typing import Literal, cast, TYPE_CHECKING, Final
11
+ import math
12
+ import numpy as np
13
+
14
+ import nvmath.distributed
15
+ from nvmath.internal import formatters, utils
16
+ from nvmath.internal.tensor_wrapper import maybe_register_package
17
+ from nvmath.internal.package_wrapper import StreamHolder, AnyStream
18
+ from nvmath.bindings import cufftMp # type: ignore
19
+ from nvmath.bindings import nvshmem # type: ignore
20
+ from nvmath.distributed._internal import tensor_wrapper
21
+ from nvmath.distributed._internal.nvshmem import NvshmemMemoryManager
22
+ from nvmath.distributed._internal.nvshmem import free as nvshmem_free_wrapper
23
+
24
+ if TYPE_CHECKING:
25
+ from nvmath.distributed._internal.tensor_ifc_cupy import CupyDistributedTensor
26
+ from nvmath.distributed._internal.tensor_ifc_torch import TorchDistributedTensor
27
+
28
+ from ._configuration import ReshapeOptions
29
+
30
+
31
+ @dataclass
32
+ class TensorLayout:
33
+ """An internal data class for capturing the tensor layout."""
34
+
35
+ shape: Sequence[int]
36
+ strides: Sequence[int]
37
+
38
+
39
+ # Box contains lower and upper coordinates, so it must be of length 2 in practice.
40
+ Box = Sequence[Sequence[int]]
41
+
42
+
43
+ @dataclass
44
+ class _ProblemSpec:
45
+ """This is used in a custom MPI reduction to check that the Reshape problem
46
+ specification is consistent across processes, and to infer global information
47
+ (e.g shape and memory layout)."""
48
+
49
+ @dataclass
50
+ class Options:
51
+ """
52
+ This is used for _ProblemSpec instead of ._configuration.ReshapeOptions
53
+ because it's going to be serialized as part of the custom reduction of the
54
+ _ProblemSpec, and we want to control which fields are included (for example
55
+ we don't need the logger).
56
+ """
57
+
58
+ def __init__(self, options: ReshapeOptions):
59
+ self.blocking = options.blocking
60
+
61
+ blocking: Literal[True, "auto"]
62
+
63
+ shape: tuple[int] # operand shape
64
+ is_F: bool # Is Fortran memory layout
65
+ is_C: bool # Is C memory layout
66
+ operand_dtype: str # str because TensorHolder.dtype returns str
67
+ package: Literal["numpy", "cupy", "torch"] # operand package
68
+ memory_space: Literal["cuda", "cpu"] # operand memory space
69
+ boxes: Sequence[Box] # Reshape input and output box
70
+ options: Options # Reshape options
71
+
72
+ # Global number of elements in the operand (calculated as part of the reduction).
73
+ global_size: int = 0
74
+ # is_leaf=True means that this is the _ProblemSpec of a process before reducing
75
+ # with that of another process.
76
+ is_leaf: bool = True
77
+
78
+
79
+ SHARED_RESHAPE_DOCUMENTATION = utils.COMMON_SHARED_DOC_MAP.copy()
80
+ SHARED_RESHAPE_DOCUMENTATION.update(
81
+ {
82
+ "operand": SHARED_RESHAPE_DOCUMENTATION["operand"],
83
+ #
84
+ "operand_admonitions": """
85
+ .. important::
86
+ GPU operands must be on the symmetric heap (for example, allocated with
87
+ ``nvmath.distributed.allocate_symmetric_memory()``).
88
+ """,
89
+ #
90
+ "options": """\
91
+ Specify options for the Reshape as a :class:`ReshapeOptions` object. Alternatively, a `dict` containing the parameters for the
92
+ ``ReshapeOptions`` constructor can also be provided. If not specified, the value will be set to the default-constructed
93
+ ``ReshapeOptions`` object.""".replace("\n", " "),
94
+ #
95
+ "input_box": """\
96
+ The box specifying the distribution of the input operand across processes, where each process
97
+ specifies which portion of the global array it holds. A box is a pair of coordinates specifying
98
+ the lower and upper extent for each dimension.""".replace("\n", " "),
99
+ #
100
+ "output_box": """\
101
+ The box specifying the distribution of the result across processes, where each process
102
+ specifies which portion of the global array it will hold after reshaping. A box is a pair
103
+ of coordinates specifying the lower and upper extent for each dimension.""".replace("\n", " "),
104
+ #
105
+ "sync_symmetric_memory": """\
106
+ Indicates whether to issue a symmetric memory synchronization operation on the execute stream
107
+ before the reshape operation. Note that before the Reshape starts executing, it is
108
+ required that the source operand be ready on all processes. A symmetric memory synchronization
109
+ ensures completion and visibility by all processes of previously issued local stores to
110
+ symmetric memory. Advanced users who choose to manage the synchronization on their own using
111
+ the appropriate NVSHMEM API, or who know that GPUs are already synchronized on the source
112
+ operand, can set this to False.""".replace("\n", " "),
113
+ #
114
+ "function_signature": """\
115
+ operand,
116
+ input_box: Box,
117
+ output_box: Box,
118
+ sync_symmetric_memory: bool = True,
119
+ options: ReshapeOptions | None = None,
120
+ stream: AnyStream | None = None
121
+ """.replace("\n", " "),
122
+ }
123
+ )
124
+
125
+
126
+ def _calculate_strides(shape, axis_order):
127
+ """
128
+ Calculate the strides for the provided shape and axis order.
129
+ """
130
+ strides = [None] * len(shape)
131
+
132
+ stride = 1
133
+ for axis in axis_order:
134
+ strides[axis] = stride
135
+ stride *= shape[axis]
136
+
137
+ return strides
138
+
139
+
140
+ def _copy_operand_perhaps(
141
+ operand,
142
+ stream_holder,
143
+ execution_space,
144
+ memory_space,
145
+ device_id: int | Literal["cpu"],
146
+ ):
147
+ if execution_space == memory_space:
148
+ return operand, None
149
+ else:
150
+ # Copy the `operand` to memory that matches the exec space.
151
+ # Currently, reshape only runs on GPU.
152
+ assert execution_space == "cuda"
153
+ exec_space_copy = operand.to(device_id, stream_holder)
154
+ return exec_space_copy, operand
155
+
156
+
157
+ def _problem_spec_reducer(p1: _ProblemSpec, p2: _ProblemSpec):
158
+ try:
159
+ if isinstance(p1, Exception):
160
+ return p1 # propagate exception
161
+
162
+ if isinstance(p2, Exception):
163
+ return p2 # propagate exception
164
+
165
+ if len(p1.shape) != len(p2.shape):
166
+ return ValueError("The number of dimensions of the input operand is inconsistent across processes")
167
+
168
+ if len(p1.shape) <= 0 or len(p1.shape) > 3:
169
+ return ValueError(
170
+ "Reshape currently only supports 1-D, 2-D and 3-D tensors."
171
+ f" The number of dimensions of the operand is {len(p1.shape)}."
172
+ )
173
+
174
+ if p1.operand_dtype != p2.operand_dtype:
175
+ return ValueError("The operand dtype is inconsistent across processes")
176
+
177
+ if p1.package != p2.package:
178
+ return ValueError("operand doesn't belong to the same package on all processes")
179
+
180
+ if p1.memory_space != p2.memory_space:
181
+ return ValueError('operand is not on the same memory space ("cpu", "cuda") on all processes')
182
+
183
+ if p1.options != p2.options:
184
+ return ValueError(f"options are inconsistent across processes: {p1.options} != {p2.options}")
185
+
186
+ # Determine the memory layout shared by all processes. Note that it's possible for
187
+ # a process to have both C and F layout (e.g. (2, 1) input shape).
188
+ p1.is_C &= p2.is_C
189
+ p1.is_F &= p2.is_F
190
+ if not p1.is_F and not p1.is_C:
191
+ return ValueError("The input memory layout is not C or Fortran, or is inconsistent across processes")
192
+
193
+ if len(p1.boxes) != 2 or len(p2.boxes) != 2:
194
+ return ValueError("Must provide input and output boxes on all processes")
195
+ input_box1, output_box1 = p1.boxes
196
+ input_box2, output_box2 = p2.boxes
197
+ for box in (input_box1, output_box1, input_box2, output_box2):
198
+ if len(box) != 2:
199
+ return ValueError(f"Box {box} must have lower and upper coordinates")
200
+ lower, upper = box
201
+ if len(lower) != len(p1.shape) or len(upper) != len(p1.shape):
202
+ return ValueError(
203
+ f"The number of coordinates in each coordinate pair of box {box} must "
204
+ f"match the number of operand dimensions {len(p1.shape)}."
205
+ )
206
+ if not all(upper[i] > lower[i] for i in range(len(p1.shape))):
207
+ return ValueError(
208
+ f"The upper coordinates must be larger than the lower coordinates, but got lower={lower} upper={upper}"
209
+ )
210
+
211
+ for p_spec in (p1, p2):
212
+ if p_spec.is_leaf:
213
+ # Check that the input box shape of this process matches the shape of the
214
+ # input operand.
215
+ input_lower, input_upper = p_spec.boxes[0]
216
+ input_box_shape = tuple(input_upper[i] - input_lower[i] for i in range(len(p_spec.shape)))
217
+ if input_box_shape != tuple(p_spec.shape):
218
+ return ValueError(f"The operand shape {p_spec.shape} does not match the input box shape {input_box_shape}")
219
+
220
+ if p1 is not p2: # with nranks=1 p1 is p2
221
+ p1.global_size += p2.global_size
222
+
223
+ def reduce_boxes(box1, box2):
224
+ """This function returns the smallest box that encompasses `box1` and `box2`"""
225
+ lower1, upper1 = box1
226
+ lower2, upper2 = box2
227
+ lower = np.minimum(np.array(lower1), np.array(lower2)).tolist()
228
+ upper = np.maximum(np.array(upper1), np.array(upper2)).tolist()
229
+ return lower, upper
230
+
231
+ # Merge the boxes to get the global operand shape. Note that this is applied
232
+ # progressively throughout the MPI reduction, starting with the local boxes.
233
+ p1.boxes = (reduce_boxes(input_box1, input_box2), reduce_boxes(output_box1, output_box2))
234
+
235
+ except Exception as e:
236
+ return e
237
+ p1.is_leaf = False
238
+ return p1
239
+
240
+
241
+ class InvalidReshapeState(Exception):
242
+ pass
243
+
244
+
245
+ @utils.docstring_decorator(SHARED_RESHAPE_DOCUMENTATION, skip_missing=False)
246
+ class Reshape:
247
+ """
248
+ Create a stateful object that encapsulates the specified distributed Reshape
249
+ and required resources. This object ensures the validity of resources during use and
250
+ releases them when they are no longer needed to prevent misuse.
251
+
252
+ This object encompasses all functionalities of the function-form API :func:`reshape`,
253
+ which is a convenience wrapper around it. The stateful object also allows for the
254
+ amortization of preparatory costs when the same Reshape operation is to be performed
255
+ on multiple operands with the same problem specification (see :meth:`reset_operand`
256
+ for more details).
257
+
258
+ Using the stateful object typically involves the following steps:
259
+
260
+ 1. **Problem Specification**: Initialize the object with a defined operation and
261
+ options.
262
+ 2. **Preparation**: Use :meth:`plan` to determine the best distributed algorithmic
263
+ implementation for this specific Reshape operation.
264
+ 3. **Execution**: Perform the Reshape with :meth:`execute`.
265
+ 4. **Resource Management**: Ensure all resources are released either by explicitly
266
+ calling :meth:`free` or by managing the stateful object within a context manager.
267
+
268
+ Detailed information on each step described above can be obtained by passing in a
269
+ :class:`logging.Logger` object to :class:`ReshapeOptions` or by setting the appropriate
270
+ options in the root logger object, which is used by default:
271
+
272
+ >>> import logging
273
+ >>> logging.basicConfig(
274
+ ... level=logging.INFO,
275
+ ... format="%(asctime)s %(levelname)-8s %(message)s",
276
+ ... datefmt="%m-%d %H:%M:%S",
277
+ ... )
278
+
279
+ Args:
280
+ operand: {operand}
281
+ {operand_admonitions}
282
+
283
+ input_box: {input_box}
284
+
285
+ output_box: {output_box}
286
+
287
+ options: {options}
288
+
289
+ stream: {stream}
290
+
291
+ See Also:
292
+ :meth:`plan`, :meth:`reset_operand`, :meth:`execute`
293
+
294
+ Examples:
295
+
296
+ >>> import cupy as cp
297
+ >>> import nvmath.distributed
298
+
299
+ Get MPI communicator used to initialize nvmath.distributed (for information on
300
+ initializing nvmath.distributed, you can refer to the documentation or to the
301
+ Reshape examples in `nvmath/examples/distributed/reshape
302
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/reshape>`_):
303
+
304
+ >>> comm = nvmath.distributed.get_context().communicator
305
+
306
+ Let's create a 3D floating-point ndarray on GPU, distributed across a certain number
307
+ of processes, with each holding a portion of the ndarray. As an example, process 0
308
+ holds a 3D box of the global 3D array of shape (4, 4, 4).
309
+
310
+ >>> shape = 4, 4, 4
311
+
312
+ Reshape uses the NVSHMEM PGAS model, which requires GPU operands to be on the
313
+ symmetric heap:
314
+
315
+ >>> if comm.Get_rank() == 0:
316
+ ... a[:] = cp.random.rand(*shape)
317
+ ... else:
318
+ ... a = ... # each process holds a different section of the global array.
319
+ ... a = nvmath.distributed.allocate_symmetric_memory(shape, cp)
320
+
321
+ With Reshape, we will change how the ndarray is distributed, by having each
322
+ process specify the input and output section of the global array. For process 0,
323
+ let's assume that it holds the 3D box that goes from the lower corner given by
324
+ coordinates (0, 0, 0) to the upper corner (4, 4, 4).
325
+
326
+ NOTE: each process has its own input and output boxes which are different to those
327
+ of other processes, as each holds a different section of the global array.
328
+
329
+ >>> if comm.Get_rank() == 0:
330
+ ... input_lower = (0, 0, 0)
331
+ ... input_upper = (4, 4, 4)
332
+ ... input_box = [input_lower, input_upper]
333
+ ... output_box = ...
334
+ ... else:
335
+ ... input_box = ... # the input box depends on the process.
336
+ ... output_box = ... # the output box depends on the process.
337
+
338
+ Create a Reshape object encapsulating the problem specification above:
339
+
340
+ >>> r = nvmath.distributed.reshape.Reshape(a, input_box, output_box)
341
+
342
+ Options can be provided above to control the behavior of the operation using the
343
+ `options` argument (see :class:`ReshapeOptions`).
344
+
345
+ Next, plan the Reshape:
346
+
347
+ >>> r.plan()
348
+
349
+ Now execute the Reshape, and obtain the result `b` as a CuPy ndarray. Reshape always
350
+ performs the distributed operation on the GPU.
351
+
352
+ >>> b = r.execute()
353
+
354
+ Finally, free the Reshape object's resources. To avoid this explicit call, it's
355
+ recommended to use the Reshape object as a context manager as shown below, if
356
+ possible.
357
+
358
+ >>> r.free()
359
+
360
+ Any symmetric memory that is owned by the user must be deleted explicitly (this is
361
+ a collective call and must be called by all processes):
362
+
363
+ >>> nvmath.distributed.free_symmetric_memory(a, b)
364
+
365
+ Note that all :class:`Reshape` methods execute on the current stream by default.
366
+ Alternatively, the `stream` argument can be used to run a method on a specified
367
+ stream.
368
+
369
+ Let's now look at the same problem with NumPy ndarrays on the CPU.
370
+
371
+ >>> import numpy as np
372
+ >>> a = np.random.rand(*shape) # each process holds a different section
373
+
374
+ Create a Reshape object encapsulating the problem specification described earlier
375
+ and use it as a context manager.
376
+
377
+ >>> with nvmath.distributed.reshape.Reshape(a, input_box, output_box) as r:
378
+ ... r.plan()
379
+ ...
380
+ ... # Execute the Reshape to redistribute the ndarray.
381
+ ... b = r.execute()
382
+
383
+ All the resources used by the object are released at the end of the block.
384
+
385
+ Reshape always executes on the GPU. In this case, because ``a`` resides in host
386
+ memory, the NumPy array is temporarily copied to device memory (on the symmetric
387
+ memory heap), re-distributed on the GPU, and the result is copied to host memory
388
+ as a NumPy array.
389
+
390
+ Further examples can be found in the `nvmath/examples/distributed/reshape
391
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/reshape>`_
392
+ directory.
393
+ """
394
+
395
+ def _free_internal_sheap(self, exception: Exception | None = None) -> bool:
396
+ # This is a fail-safe to free NVSHMEM internal memory in case of invalid
397
+ # state (Reshape constructor fails). Since we might call nvshmem_free here, we're
398
+ # assuming that all processes equally failed in the ctor, which might not be true,
399
+ # but if it weren't true they would end up in deadlock most likely anyway.
400
+ if (
401
+ hasattr(self, "memory_space")
402
+ and self.memory_space == "cpu"
403
+ and self.operand is not None
404
+ and self.operand.device == "cuda"
405
+ ):
406
+ with utils.device_ctx(self.device_id):
407
+ nvshmem_free_wrapper(self.operand.data_ptr)
408
+ return True
409
+
410
+ @utils.atomic(_free_internal_sheap, method=True)
411
+ def __init__(
412
+ self,
413
+ operand,
414
+ /,
415
+ input_box: Box,
416
+ output_box: Box,
417
+ *,
418
+ options: ReshapeOptions | None = None,
419
+ stream: AnyStream | None = None,
420
+ ):
421
+ distributed_ctx = nvmath.distributed.get_context()
422
+ if distributed_ctx is None:
423
+ # TODO: add a link to the docs section that will discuss initialization
424
+ # and finalization of the distributed operations.
425
+ raise RuntimeError("nvmath.distributed has not been initialized")
426
+ self.communicator = distributed_ctx.communicator
427
+ nranks = self.communicator.Get_size()
428
+
429
+ # For GPU operands, the distributed tensor wrappers check that the memory is in the
430
+ # symmetric heap by calling nvshmem.ptr().
431
+ self.operand = operand = tensor_wrapper.wrap_operand(operand)
432
+ self.options = options = cast(
433
+ ReshapeOptions, utils.check_or_create_options(ReshapeOptions, options, "Distributed Reshape options")
434
+ )
435
+ self.package = operand.name
436
+
437
+ is_C = sorted(operand.strides, reverse=True) == list(operand.strides)
438
+ is_F = sorted(operand.strides) == list(operand.strides)
439
+
440
+ # Merge the problem specification across processes to make sure that there are no
441
+ # inconsistencies and to calculate the global shape. Importantly, this also does
442
+ # collective error checking of the Reshape input parameters, to ensure that all
443
+ # processes fail on error of any one process, thus preventing deadlock.
444
+ problem_spec = _ProblemSpec(
445
+ boxes=[input_box, output_box],
446
+ shape=tuple(operand.shape),
447
+ operand_dtype=operand.dtype,
448
+ options=_ProblemSpec.Options(options),
449
+ package=self.package,
450
+ memory_space=operand.device,
451
+ global_size=math.prod(operand.shape),
452
+ is_C=is_C,
453
+ is_F=is_F,
454
+ )
455
+ if nranks > 1:
456
+ problem_spec = self.communicator.allreduce(problem_spec, op=_problem_spec_reducer)
457
+ else:
458
+ # Ensure we error-check with one rank.
459
+ problem_spec = _problem_spec_reducer(problem_spec, problem_spec)
460
+ if isinstance(problem_spec, Exception):
461
+ # There is an error or inconsistency in the problem spec across processes.
462
+ # Note that since this comes from an allreduce, all processes will have
463
+ # received the same exception.
464
+ raise problem_spec
465
+
466
+ if problem_spec.is_C:
467
+ self.layout: Final = "C"
468
+ else:
469
+ assert problem_spec.is_F, "Internal Error." # The reducer is supposed to have detected this
470
+ self.layout: Final = "F" # type: ignore
471
+
472
+ self.operand_dim = len(operand.shape)
473
+
474
+ self.logger = options.logger if options.logger is not None else logging.getLogger()
475
+
476
+ # The merged (global) input and output box must be the same.
477
+ if problem_spec.boxes[0] != problem_spec.boxes[1]:
478
+ raise ValueError(
479
+ "The global box derived from the input and output boxes doesn't match: "
480
+ f"{problem_spec.boxes[0]} != {problem_spec.boxes[1]}"
481
+ )
482
+
483
+ # Infer the global shape from the global input box. Note that cuFFTMp does not
484
+ # disallow lower coordinates for the merged (global) boxes that are not 0.
485
+ lower, upper = problem_spec.boxes[0]
486
+ global_shape = tuple(int(upper[i] - lower[i]) for i in range(self.operand_dim))
487
+ self.logger.info(f"The global shape of the operand is {global_shape}.")
488
+
489
+ # The global number of elements must be compatible with the global shape.
490
+ if problem_spec.global_size != math.prod(global_shape):
491
+ raise ValueError(f"The global number of elements is incompatible with the inferred global shape {global_shape}")
492
+
493
+ # Store the local input and output box.
494
+ self.input_box = input_box
495
+ self.output_box = output_box
496
+
497
+ self.operand_data_type = operand.dtype
498
+ # TODO: change to `operand.dtype.itemsize` once operand is StridedMemoryView.
499
+ itemsize = operand.tensor.dtype.itemsize
500
+ if itemsize not in (4, 8, 16):
501
+ raise ValueError(
502
+ f"Reshape only supports element sizes in (4, 8, 16) bytes. The operand's element size is {itemsize}"
503
+ )
504
+
505
+ self.logger.info(f"The Reshape data type is {self.operand_data_type}.")
506
+
507
+ # Infer execution and memory space.
508
+ execution_device_id: int = distributed_ctx.device_id
509
+ if operand.device_id != "cpu": # exec space matches the mem space
510
+ self.memory_space = "cuda"
511
+ self.device_id = operand.device_id
512
+ assert operand.device_id == execution_device_id
513
+ else: # we need to move inputs cpu -> gpu and outputs gpu -> cpu
514
+ self.memory_space = "cpu"
515
+ self.device_id = execution_device_id
516
+ self.execution_space = "cuda"
517
+ self.operand_device_id = operand.device_id
518
+ self.internal_op_package = self._internal_operand_package(self.package)
519
+ stream_holder: StreamHolder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
520
+
521
+ self.logger.info(
522
+ f"The input tensor's memory space is {self.memory_space}, and the execution space "
523
+ f"is {self.execution_space}, with device {self.device_id}."
524
+ )
525
+
526
+ self.logger.info(f"The specified stream for the Reshape ctor is {stream_holder and stream_holder.obj}")
527
+
528
+ # Copy the operand to execution_space's device if needed.
529
+ self.operand, self.operand_backup = _copy_operand_perhaps(
530
+ operand,
531
+ stream_holder,
532
+ self.execution_space,
533
+ self.memory_space,
534
+ self.device_id,
535
+ )
536
+ operand = self.operand
537
+ # Capture operand layout for consistency checks when resetting operands.
538
+ self.operand_layout = TensorLayout(shape=operand.shape, strides=operand.strides)
539
+
540
+ self.result_layout: TensorLayout | None = None
541
+ # We'll infer the result layout at plan time.
542
+ self.result_class: CupyDistributedTensor | TorchDistributedTensor = operand.__class__
543
+ self.result_data_type = operand.dtype
544
+
545
+ # Set blocking or non-blocking behavior.
546
+ self.blocking = self.options.blocking is True or self.memory_space == "cpu"
547
+ if self.blocking:
548
+ self.call_prologue = "This call is blocking and will return only after the operation is complete."
549
+ else:
550
+ self.call_prologue = (
551
+ "This call is non-blocking and will return immediately after the operation is launched on the device."
552
+ )
553
+
554
+ # Set memory allocator.
555
+ self.allocator = NvshmemMemoryManager(self.device_id, self.logger)
556
+
557
+ # Create handle.
558
+ with utils.device_ctx(self.device_id):
559
+ self.handle = cufftMp.create_reshape()
560
+
561
+ self.reshape_planned = False
562
+
563
+ # Workspace attributes.
564
+ self.workspace_ptr, self.workspace_size = None, None
565
+ self._workspace_allocated_here = False
566
+
567
+ # Attributes to establish stream ordering.
568
+ self.workspace_stream = None
569
+ self.last_compute_event = None
570
+
571
+ self.valid_state = True
572
+ self.logger.info("The distributed Reshape operation has been created.")
573
+
574
+ def __enter__(self):
575
+ return self
576
+
577
+ def __exit__(self, exc_type, exc_value, traceback):
578
+ self.free()
579
+
580
+ def _check_valid_reshape(self, *args, **kwargs):
581
+ """
582
+ Check if Reshape object is alive and well.
583
+ """
584
+ if not self.valid_state:
585
+ raise InvalidReshapeState("The Reshape object cannot be used after resources are free'd")
586
+
587
+ def _free_plan_resources(self, exception: Exception | None = None) -> bool:
588
+ """
589
+ Free resources allocated in planning.
590
+ """
591
+ self.reshape_planned = False
592
+ return True
593
+
594
+ def _internal_operand_package(self, package_name):
595
+ if package_name == "numpy":
596
+ # TODO: remove this call after cupy is dropped
597
+ maybe_register_package("cupy")
598
+ return package_name if package_name != "numpy" else "cupy"
599
+
600
+ def _allocate_result_operand(self, exec_stream_holder, log_debug):
601
+ if log_debug:
602
+ self.logger.debug("Beginning output (empty) tensor creation...")
603
+ self.logger.debug(
604
+ f"The output tensor shape = {self.result_layout.shape} with strides = "
605
+ f"{self.result_layout.strides} and data type '{self.result_data_type}'."
606
+ )
607
+ result = utils.create_empty_tensor(
608
+ self.result_class,
609
+ self.result_layout.shape,
610
+ self.result_data_type,
611
+ self.device_id,
612
+ exec_stream_holder,
613
+ verify_strides=False,
614
+ strides=self.result_layout.strides,
615
+ make_symmetric=True,
616
+ logger=self.logger,
617
+ )
618
+ if log_debug:
619
+ self.logger.debug("The output (empty) tensor has been created.")
620
+ return result
621
+
622
+ @utils.precondition(_check_valid_reshape)
623
+ @utils.atomic(_free_plan_resources, method=True)
624
+ def plan(self, stream: AnyStream | None = None):
625
+ """Plan the Reshape.
626
+
627
+ Args:
628
+ stream: {stream}
629
+ """
630
+ log_info = self.logger.isEnabledFor(logging.INFO)
631
+
632
+ if self.reshape_planned:
633
+ self.logger.debug("The Reshape has already been planned, and redoing the plan is not supported.")
634
+ return
635
+
636
+ stream_holder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
637
+ self.workspace_stream = stream_holder.obj
638
+
639
+ if log_info:
640
+ self.logger.info("Starting distributed Reshape planning...")
641
+
642
+ lower_input, upper_input = self.input_box
643
+ lower_output, upper_output = self.output_box
644
+
645
+ def calculate_reshape_params(lower, upper, operand, order: Literal["C", "F"]):
646
+ """Calculate shape and strides for input or output of Reshape."""
647
+ local_shape = tuple(upper[i] - lower[i] for i in range(self.operand_dim))
648
+ if operand is not None:
649
+ # Take the strides from the operand.
650
+ strides = operand.strides
651
+ elif order == "F":
652
+ # This is out=None with F layout.
653
+ strides = _calculate_strides(local_shape, range(self.operand_dim))
654
+ else:
655
+ # This is out=None with C layout.
656
+ strides = _calculate_strides(local_shape, reversed(range(self.operand_dim)))
657
+ return local_shape, strides
658
+
659
+ input_local_shape, strides_input = calculate_reshape_params(lower_input, upper_input, self.operand, self.layout)
660
+ output_local_shape, strides_output = calculate_reshape_params(lower_output, upper_output, None, self.layout)
661
+ self.result_layout = TensorLayout(shape=output_local_shape, strides=strides_output)
662
+
663
+ if self.operand_dim < 3:
664
+ # cufftMp.make_reshape only supports 3D, so we broadcast the local operands.
665
+ N = 3 - self.operand_dim
666
+ lower_input = tuple(lower_input) + (0,) * N
667
+ upper_input = tuple(upper_input) + (1,) * N
668
+ lower_output = tuple(lower_output) + (0,) * N
669
+ upper_output = tuple(upper_output) + (1,) * N
670
+
671
+ strides_input = self.operand.reshape(input_local_shape + (1,) * N, copy=False).strides
672
+ if self.layout == "F":
673
+ strides_output = _calculate_strides(output_local_shape + (1,) * N, (0, 1, 2))
674
+ else:
675
+ strides_output = _calculate_strides(output_local_shape + (1,) * N, (2, 1, 0))
676
+
677
+ # cuFFTMp only supports decreasing strides. To support increasing strides, we'll
678
+ # pass the required metadata to cuFFTMp with axes transposed (but won't touch
679
+ # the data).
680
+ if self.layout == "F":
681
+ lower_input = tuple(reversed(lower_input))
682
+ upper_input = tuple(reversed(upper_input))
683
+ strides_input = tuple(reversed(strides_input))
684
+ lower_output = tuple(reversed(lower_output))
685
+ upper_output = tuple(reversed(upper_output))
686
+ strides_output = tuple(reversed(strides_output))
687
+
688
+ with utils.host_call_ctx(timing=log_info) as elapsed, utils.device_ctx(self.device_id):
689
+ nullptr = 0
690
+ cufftMp.make_reshape(
691
+ self.handle,
692
+ # TODO: change to `operand.dtype.itemsize` once operand is StridedMemoryView
693
+ self.operand.tensor.dtype.itemsize,
694
+ 3,
695
+ lower_input,
696
+ upper_input,
697
+ strides_input,
698
+ lower_output,
699
+ upper_output,
700
+ strides_output,
701
+ nullptr,
702
+ cufftMp.MpCommType.COMM_NONE,
703
+ )
704
+ self.workspace_size = cufftMp.get_reshape_size(self.handle)
705
+
706
+ self.reshape_planned = True
707
+
708
+ if log_info and elapsed.data is not None:
709
+ self.logger.info(f"The Reshape planning phase took {elapsed.data:.3f} ms to complete.")
710
+
711
+ @utils.precondition(_check_valid_reshape)
712
+ def reset_operand(self, operand=None, *, stream: AnyStream | None = None):
713
+ """
714
+ Reset the operand held by this :class:`Reshape` instance. This method has two
715
+ use cases:
716
+
717
+ (1) it can be used to provide a new operand for execution
718
+ (2) it can be used to release the internal reference to the previous operand and
719
+ potentially make its memory available for other use by passing
720
+ ``operand=None``.
721
+
722
+ Args:
723
+ operand: A tensor (ndarray-like object) compatible with the previous one or
724
+ `None` (default). A value of `None` will release the internal reference to
725
+ the previous operand and user is expected to set a new operand before again
726
+ calling :meth:`execute`. The new operand is considered compatible if all the
727
+ following properties match with the previous one:
728
+
729
+ - The operand distribution, which must be (input_box, output_box)
730
+ where input_box and output_box are the boxes specified at plan time.
731
+ - The package that the new operand belongs to.
732
+ - The dtype of the new operand.
733
+ - The shape and strides of the new operand.
734
+ - The memory space of the new operand (CPU or GPU).
735
+ - The device that new operand belongs to if it is on GPU.
736
+
737
+ stream: {stream}.
738
+
739
+ Examples:
740
+
741
+ >>> import cupy as cp
742
+ >>> import nvmath.distributed
743
+
744
+ Get MPI communicator used to initialize nvmath.distributed (for information on
745
+ initializing nvmath.distributed, you can refer to the documentation or to the
746
+ Reshape examples in `nvmath/examples/distributed/reshape
747
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/reshape>`_):
748
+
749
+ >>> comm = nvmath.distributed.get_context().communicator
750
+ >>> nranks = comm.Get_size()
751
+
752
+ Create a 3-D complex128 ndarray on GPU symmetric memory, initially partitioned
753
+ on the X axis (the global shape is (128, 128, 128)):
754
+
755
+ >>> shape = 128 // nranks, 128, 128
756
+ >>> dtype = cp.complex128
757
+ >>> a = nvmath.distributed.allocate_symmetric_memory(shape, cp, dtype=dtype)
758
+ >>> a[:] = cp.random.rand(*shape) + 1j * cp.random.rand(*shape)
759
+
760
+ Compute the input and output box for the desired re-distribution:
761
+
762
+ >>> input_box = ...
763
+ >>> output_box = ...
764
+
765
+ Create a Reshape object as a context manager
766
+
767
+ >>> with nvmath.distributed.reshape.Reshape(a, input_box, output_box) as f:
768
+ ... # Plan the Reshape
769
+ ... r.plan()
770
+ ...
771
+ ... # Execute the Reshape to get the first result.
772
+ ... r1 = r.execute()
773
+ ...
774
+ ... # Reset the operand to a new CuPy ndarray.
775
+ ... b = nvmath.distributed.allocate_symmetric_memory(shape, cp, dtype=dtype)
776
+ ... b[:] = cp.random.rand(*shape) + 1j * cp.random.rand(*shape)
777
+ ... f.reset_operand(b)
778
+ ...
779
+ ... # Execute to get the new result corresponding to the updated operand.
780
+ ... r2 = f.execute()
781
+
782
+ With :meth:`reset_operand`, minimal overhead is achieved as problem
783
+ specification and planning are only performed once.
784
+
785
+ For the particular example above, explicitly calling :meth:`reset_operand` is
786
+ equivalent to updating the operand in-place, i.e, replacing
787
+ ``f.reset_operand(b)`` with ``a[:]=b``. Note that updating the operand in-place
788
+ should be adopted with caution as it can only yield the expected result and
789
+ incur no additional copies under the additional constraints below:
790
+
791
+ - The operand's distribution is the same.
792
+
793
+ For more details, please refer to `inplace update example
794
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/reshape/example05_stateful_reset_inplace.py>`_.
795
+ """
796
+
797
+ if operand is None:
798
+ if self.memory_space == "cpu" and self.operand is not None:
799
+ with utils.device_ctx(self.device_id):
800
+ nvshmem_free_wrapper(self.operand.data_ptr)
801
+ self.operand = None # type: ignore
802
+ self.operand_backup = None # type: ignore
803
+ self.logger.info("The operand has been reset to None.")
804
+ return
805
+
806
+ self.logger.info("Resetting operand...")
807
+ # First wrap operand.
808
+ operand = tensor_wrapper.wrap_operand(operand)
809
+
810
+ # Check package match.
811
+ package = utils.infer_object_package(operand.tensor)
812
+ if self.package != package:
813
+ message = f"Library package mismatch: '{self.package}' => '{package}'"
814
+ raise TypeError(message)
815
+
816
+ # Check the dtype.
817
+ utils.check_attribute_match(self.operand_data_type, operand.dtype, "data type")
818
+
819
+ stream_holder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
820
+ self.logger.info(f"The specified stream for reset_operand() is {stream_holder and stream_holder.obj}.")
821
+
822
+ # In principle, we could support memory_space change,
823
+ # but to handle it properly we need to update self.memory_space and
824
+ # some dependent properties, like self.blocking, which may be error-prone
825
+ # from the user perspective. It would prevent inplace optimizations as well.
826
+ operand_device_id = operand.device_id
827
+ if operand_device_id != self.operand_device_id:
828
+
829
+ def device_str(device_id: int | Literal["cpu"]) -> str:
830
+ return f"cuda:{device_id}" if isinstance(device_id, int) else f"{device_id}"
831
+
832
+ raise ValueError(
833
+ f"The new operand must be on the same device as the original one. "
834
+ f"The new operand's device is {device_str(operand_device_id)}, "
835
+ f"the original device is {device_str(self.operand_device_id)}"
836
+ )
837
+
838
+ # The plan was made for a specific input box and strides, so the new operand must
839
+ # match.
840
+ if operand.shape != self.operand_layout.shape:
841
+ raise ValueError(
842
+ f"The shape of the new operand ({operand.shape}) does not match "
843
+ f"the original shape ({self.operand_layout.shape})."
844
+ )
845
+ if operand.strides != self.operand_layout.strides:
846
+ raise ValueError(
847
+ f"The strides of the new operand ({operand.strides}) do not match "
848
+ f"the original strides ({self.operand_layout.strides})."
849
+ )
850
+ self.logger.info(f"The reset operand shape = {self.operand_layout.shape}, and strides = {self.operand_layout.strides}.")
851
+
852
+ if self.memory_space == "cpu":
853
+ if self.operand is not None:
854
+ self.operand.copy_(operand, stream_holder=stream_holder)
855
+ self.operand_backup = operand
856
+ else:
857
+ self.operand, self.operand_backup = _copy_operand_perhaps(
858
+ operand,
859
+ stream_holder,
860
+ self.execution_space,
861
+ self.memory_space,
862
+ self.device_id,
863
+ )
864
+ else:
865
+ self.operand = operand
866
+
867
+ def _check_output(self, out):
868
+ utils.check_attribute_match(self.operand.dtype, out.dtype, "data type")
869
+
870
+ operand = self.operand if self.memory_space == self.execution_space else self.operand_backup
871
+ if operand.data_ptr == out.data_ptr:
872
+ raise ValueError("Reshape does not support inplace operation (operand and output share the same memory address)")
873
+
874
+ out_package = utils.infer_object_package(out.tensor)
875
+ if out_package != self.package:
876
+ raise ValueError(
877
+ f"The package for 'out' ({out_package}) must be the same as that of the operand ({{self.package}})."
878
+ )
879
+
880
+ if len(out.shape) != self.operand_dim:
881
+ raise ValueError(
882
+ "operand and out dimensionality must be the same. The operand number of dimensions is"
883
+ f" {self.operand_dim} and the output number of dimensions is {len(out.shape)}"
884
+ )
885
+
886
+ if out.device_id != self.operand_device_id:
887
+ raise ValueError(
888
+ f"The device ID for 'out' ({out.device_id}) must be the same as that of the operand ({self.operand_device_id})."
889
+ )
890
+
891
+ def _check_planned(self, *args, **kwargs):
892
+ """ """
893
+ what = kwargs["what"]
894
+ if not self.reshape_planned:
895
+ raise RuntimeError(f"{what} cannot be performed before plan() has been called.")
896
+
897
+ def _check_valid_operand(self, *args, **kwargs):
898
+ """ """
899
+ what = kwargs["what"]
900
+ if self.operand is None:
901
+ raise RuntimeError(
902
+ f"{what} cannot be performed if the input operand has been set to None. Use reset_operand() to set the "
903
+ f"desired input before using performing the {what.lower()}."
904
+ )
905
+
906
+ def _free_workspace_memory(self, exception: Exception | None = None) -> bool:
907
+ """
908
+ Free workspace by releasing the MemoryPointer object.
909
+ """
910
+ if self.workspace_ptr is None:
911
+ return True
912
+
913
+ with utils.device_ctx(self.device_id):
914
+ self.workspace_ptr.free()
915
+ self.workspace_ptr = None
916
+ self.logger.debug("[_free_workspace_memory] The workspace has been released.")
917
+
918
+ return True
919
+
920
+ @utils.precondition(_check_valid_reshape)
921
+ @utils.precondition(_check_planned, "Workspace memory allocation")
922
+ @utils.atomic(_free_workspace_memory, method=True)
923
+ def _allocate_workspace_memory(self, stream_holder):
924
+ """
925
+ Allocate workspace memory using the specified allocator.
926
+ """
927
+
928
+ assert self._workspace_allocated_here is False, "Internal Error."
929
+
930
+ self.logger.debug("Allocating workspace for performing the Reshape...")
931
+ with utils.device_ctx(self.device_id), stream_holder.ctx:
932
+ try:
933
+ self.workspace_ptr = self.allocator.memalloc(self.workspace_size)
934
+ self._workspace_allocated_here = True
935
+ except TypeError as e:
936
+ message = (
937
+ "The method 'memalloc' in the allocator object must conform to the interface in the "
938
+ "'BaseCUDAMemoryManager' protocol."
939
+ )
940
+ raise TypeError(message) from e
941
+
942
+ self.workspace_stream = stream_holder.obj
943
+ self.logger.debug(
944
+ f"Finished allocating device workspace of size {formatters.MemoryStr(self.workspace_size)} in the context "
945
+ f"of stream {self.workspace_stream}."
946
+ )
947
+
948
+ def _allocate_workspace_memory_perhaps(self, stream_holder):
949
+ """
950
+ Allocate workspace memory using the specified allocator, if it hasn't already been
951
+ done.
952
+ """
953
+ if self.execution_space != "cuda" or self.workspace_ptr is not None:
954
+ return
955
+
956
+ return self._allocate_workspace_memory(stream_holder)
957
+
958
+ @utils.precondition(_check_valid_reshape)
959
+ def _free_workspace_memory_perhaps(self, release_workspace):
960
+ """
961
+ Free workspace memory if if 'release_workspace' is True.
962
+ """
963
+ if not release_workspace:
964
+ return
965
+
966
+ # Establish ordering wrt the computation and free workspace if it's more than the
967
+ # specified cache limit.
968
+ if self.last_compute_event is not None:
969
+ with utils.device_ctx(self.device_id):
970
+ self.workspace_stream.wait(self.last_compute_event)
971
+ self.logger.debug("Established ordering with respect to the computation before releasing the workspace.")
972
+ self.last_compute_event = None
973
+
974
+ self.logger.debug("[_free_workspace_memory_perhaps] The workspace memory will be released.")
975
+ self._free_workspace_memory()
976
+
977
+ return True
978
+
979
+ def _release_workspace_memory_perhaps(self, exception: Exception | None = None) -> bool:
980
+ """
981
+ Free workspace memory if it was allocated in this call
982
+ (self._workspace_allocated_here == True) when an exception occurs.
983
+ """
984
+ release_workspace = self._workspace_allocated_here
985
+ self.logger.debug(
986
+ f"[_release_workspace_memory_perhaps] The release_workspace flag is set to {release_workspace} based upon "
987
+ "the value of 'workspace_allocated_here'."
988
+ )
989
+ self._free_workspace_memory_perhaps(release_workspace)
990
+ self._workspace_allocated_here = False
991
+ return True
992
+
993
+ @utils.precondition(_check_valid_reshape)
994
+ @utils.precondition(_check_planned, "Execution")
995
+ @utils.precondition(_check_valid_operand, "Execution")
996
+ @utils.atomic(_release_workspace_memory_perhaps, method=True)
997
+ def execute(self, stream: AnyStream | None = None, release_workspace: bool = False, sync_symmetric_memory: bool = True):
998
+ """
999
+ Execute the Reshape operation.
1000
+
1001
+ Args:
1002
+ stream: {stream}
1003
+
1004
+ release_workspace: A value of `True` specifies that the Reshape object
1005
+ should release workspace memory back to the symmetric memory pool on
1006
+ function return, while a value of `False` specifies that the object
1007
+ should retain the memory. This option may be set to `True` if the
1008
+ application performs other operations that consume a lot of memory between
1009
+ successive calls to the (same or different) :meth:`execute` API, but incurs
1010
+ an overhead due to obtaining and releasing workspace memory from and
1011
+ to the symmetric memory pool on every call. The default is `False`.
1012
+ **NOTE: All processes must use the same value or the application can
1013
+ deadlock.**
1014
+
1015
+ sync_symmetric_memory: {sync_symmetric_memory}
1016
+
1017
+ Returns:
1018
+ The reshaped operand, which remains on the same device and utilizes the same
1019
+ package as the input operand. For GPU operands, the result will be in symmetric
1020
+ memory and the user is responsible for explicitly deallocating it (for example,
1021
+ using ``nvmath.distributed.free_symmetric_memory(tensor)``).
1022
+ """
1023
+
1024
+ log_info = self.logger.isEnabledFor(logging.INFO)
1025
+ log_debug = self.logger.isEnabledFor(logging.DEBUG)
1026
+
1027
+ stream_holder: StreamHolder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
1028
+
1029
+ # Allocate workspace if needed.
1030
+ self._allocate_workspace_memory_perhaps(stream_holder)
1031
+
1032
+ # Allocate output operand.
1033
+ with utils.device_ctx(self.device_id):
1034
+ result = self._allocate_result_operand(stream_holder, log_debug)
1035
+
1036
+ if log_info:
1037
+ self.logger.info("Starting distributed Reshape...")
1038
+ self.logger.info(f"{self.call_prologue}")
1039
+
1040
+ with utils.cuda_call_ctx(stream_holder, self.blocking, timing=log_info) as (
1041
+ self.last_compute_event,
1042
+ elapsed,
1043
+ ):
1044
+ raw_workspace_ptr = utils.get_ptr_from_memory_pointer(self.workspace_ptr)
1045
+ if sync_symmetric_memory:
1046
+ nvshmem.sync_all_on_stream(stream_holder.ptr)
1047
+ if log_info:
1048
+ self.logger.info(
1049
+ "sync_symmetric_memory is enabled (this may incur redundant multi-GPU "
1050
+ "synchronization, please refer to the documentation for more information)"
1051
+ )
1052
+ elif log_info:
1053
+ self.logger.info("sync_symmetric_memory is disabled")
1054
+ cufftMp.exec_reshape_async(
1055
+ self.handle, result.data_ptr, self.operand.data_ptr, raw_workspace_ptr, stream_holder.ptr
1056
+ )
1057
+
1058
+ if log_info and elapsed.data is not None:
1059
+ self.logger.info(f"The distributed Reshape took {elapsed.data:.3f} ms to complete.")
1060
+
1061
+ # Establish ordering wrt the computation and free workspace if it's more than the
1062
+ # specified cache limit.
1063
+ self._free_workspace_memory_perhaps(release_workspace)
1064
+
1065
+ # reset workspace allocation tracking to False at the end of the methods where
1066
+ # workspace memory is potentially allocated. This is necessary to prevent any
1067
+ # exceptions raised before method entry from using stale tracking values.
1068
+ self._workspace_allocated_here = False
1069
+
1070
+ if self.memory_space == "cpu":
1071
+ out = result.to("cpu", stream_holder=stream_holder).tensor
1072
+ with utils.device_ctx(self.device_id):
1073
+ nvshmem_free_wrapper(result.data_ptr)
1074
+ else:
1075
+ out = result.tensor
1076
+
1077
+ return out
1078
+
1079
+ def free(self):
1080
+ """Free Reshape resources.
1081
+
1082
+ It is recommended that the :class:`Reshape` object be used within a context, but if
1083
+ it is not possible then this method must be called explicitly to ensure that the
1084
+ Reshape resources (especially internal library objects) are properly cleaned up.
1085
+ """
1086
+
1087
+ if not self.valid_state:
1088
+ return
1089
+
1090
+ try:
1091
+ # Future operations on the workspace stream should be ordered after the
1092
+ # computation.
1093
+ if self.last_compute_event is not None:
1094
+ with utils.device_ctx(self.device_id):
1095
+ self.workspace_stream.wait(self.last_compute_event)
1096
+ self.last_compute_event = None
1097
+
1098
+ self._free_workspace_memory()
1099
+
1100
+ with utils.device_ctx(self.device_id):
1101
+ if self.handle is not None:
1102
+ cufftMp.destroy_reshape(self.handle)
1103
+ self.handle = None
1104
+
1105
+ if self.memory_space == "cpu" and self.operand is not None:
1106
+ # In this case, self.operand is an internal GPU operand owned by Reshape
1107
+ nvshmem_free_wrapper(self.operand.data_ptr)
1108
+ self.operand = None
1109
+ self.operand_backup = None
1110
+
1111
+ except Exception as e:
1112
+ self.logger.critical("Internal error: only part of the Reshape object's resources have been released.")
1113
+ self.logger.critical(str(e))
1114
+ raise e
1115
+ finally:
1116
+ self.valid_state = False
1117
+
1118
+ self.logger.info("The Reshape object's resources have been released.")
1119
+
1120
+
1121
+ @utils.docstring_decorator(SHARED_RESHAPE_DOCUMENTATION, skip_missing=False)
1122
+ def reshape(
1123
+ operand,
1124
+ /,
1125
+ input_box: Box,
1126
+ output_box: Box,
1127
+ *,
1128
+ sync_symmetric_memory: bool = True,
1129
+ options: ReshapeOptions | None = None,
1130
+ stream: AnyStream | None = None,
1131
+ ):
1132
+ r"""
1133
+ reshape({function_signature})
1134
+
1135
+ Perform a distributed reshape on the provided operand to change its distribution across
1136
+ processes.
1137
+
1138
+ Args:
1139
+ operand: {operand}
1140
+ {operand_admonitions}
1141
+
1142
+ input_box: {input_box}
1143
+
1144
+ output_box: {output_box}
1145
+
1146
+ sync_symmetric_memory: {sync_symmetric_memory}
1147
+
1148
+ options: {options}
1149
+
1150
+ stream: {stream}
1151
+
1152
+ Returns:
1153
+ A tensor that remains on the same device and belongs to the same package as
1154
+ the input operand, with shape according to output_box.
1155
+
1156
+ See Also:
1157
+ :class:`Reshape`.
1158
+
1159
+ Examples:
1160
+
1161
+ >>> import cupy as cp
1162
+ >>> import nvmath.distributed
1163
+
1164
+ Get MPI communicator used to initialize nvmath.distributed (for information on
1165
+ initializing nvmath.distributed, you can refer to the documentation or to the
1166
+ Reshape examples in `nvmath/examples/distributed/reshape
1167
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/reshape>`_):
1168
+
1169
+ >>> comm = nvmath.distributed.get_context().communicator
1170
+
1171
+ Let's create a 3D floating-point ndarray on GPU, distributed across a certain number
1172
+ of processes, with each holding a portion of the ndarray. As an example, process 0
1173
+ holds a 3D box of the global 3D array of shape (4, 4, 4).
1174
+
1175
+ >>> shape = 4, 4, 4
1176
+
1177
+ Reshape uses the NVSHMEM PGAS model, which requires GPU operands to be on the
1178
+ symmetric heap:
1179
+
1180
+ >>> if comm.Get_rank() == 0:
1181
+ ... a[:] = cp.random.rand(*shape)
1182
+ ... else:
1183
+ ... a = ... # each process holds a different section of the global array.
1184
+ ... a = nvmath.distributed.allocate_symmetric_memory(shape, cp)
1185
+
1186
+ With Reshape, we will change how the ndarray is distributed, by having each
1187
+ process specify the input and output section of the global array. For process 0,
1188
+ let's assume that it holds the 3D box that goes from the lower corner given by
1189
+ coordinates (0, 0, 0) to the upper corner (4, 4, 4).
1190
+
1191
+ NOTE: each process has its own input and output boxes which are different to those
1192
+ of other processes, as each holds a different section of the global array.
1193
+
1194
+ >>> if comm.Get_rank() == 0:
1195
+ ... input_lower = (0, 0, 0)
1196
+ ... input_upper = (4, 4, 4)
1197
+ ... input_box = [input_lower, input_upper]
1198
+ ... output_box = ...
1199
+ ... else:
1200
+ ... input_box = ... # the input box depends on the process.
1201
+ ... output_box = ... # the output box depends on the process.
1202
+
1203
+ Perform the distributed reshape using :func:`reshape`:
1204
+
1205
+ >>> r = nvmath.distributed.reshape.reshape(a, input_box, output_box)
1206
+
1207
+ See :class:`ReshapeOptions` for the complete list of available options.
1208
+
1209
+ The package current stream is used by default, but a stream can be explicitly
1210
+ provided to the Reshape operation. This can be done if the Reshape operand is
1211
+ computed on a different stream, for example:
1212
+
1213
+ >>> s = cp.cuda.Stream()
1214
+ >>> with s:
1215
+ ... a = nvmath.distributed.allocate_symmetric_memory(shape, cp)
1216
+ ... a[:] = cp.random.rand(*shape)
1217
+ >>> r = nvmath.distributed.reshape.reshape(a, stream=s)
1218
+
1219
+ The operation above runs on stream `s` and is ordered with respect to the input
1220
+ computation.
1221
+
1222
+ Create a NumPy ndarray on the CPU.
1223
+
1224
+ >>> import numpy as np
1225
+ >>> b = np.random.rand(*shape)
1226
+
1227
+ Provide the NumPy ndarray to :func:`reshape`, with the result also being a NumPy
1228
+ ndarray:
1229
+
1230
+ >>> r = nvmath.distributed.reshape.reshape(b, input_box, output_box)
1231
+
1232
+ Notes:
1233
+ - This function is a convenience wrapper around :class:`Reshape` and and is
1234
+ specifically meant for *single* use. The same computation can be performed
1235
+ with the stateful API.
1236
+
1237
+ Further examples can be found in the `nvmath/examples/distributed/reshape
1238
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/distributed/reshape>`_
1239
+ directory.
1240
+ """
1241
+
1242
+ with Reshape(operand, input_box, output_box, options=options, stream=stream) as reshape_obj:
1243
+ # Plan the Reshape.
1244
+ reshape_obj.plan(stream=stream)
1245
+
1246
+ # Execute the Reshape.
1247
+ result = reshape_obj.execute(sync_symmetric_memory=sync_symmetric_memory, stream=stream)
1248
+
1249
+ return result