Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / core / tensor_parallel / style.py: 98%
312 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-07-13 05:07 +0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-07-13 05:07 +0800
1# Copyright 2026 Huawei Technologies Co., Ltd
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14# ============================================================================
15"""Parallel styles for declarative tensor-parallel module sharding.
17Provides :class:`ParallelStyle` (ABC) and concrete implementations
18:class:`ColwiseParallel`, :class:`RowwiseParallel`, :class:`SequenceParallel`,
19:class:`PrepareModuleInput`, :class:`PrepareModuleInputOutput`, and
20:class:`PrepareModuleOutput` aligned with ``torch.distributed.tensor.parallel.style``.
21"""
22from abc import ABC, abstractmethod
23from typing import Any, Dict, Optional, Tuple, Union
25from hyper_parallel.core.dtensor.device_mesh import DeviceMesh
26from hyper_parallel.core.dtensor.dtensor import (
27 DTensor,
28 distribute_module,
29 distribute_tensor,
30 _distribute_module_iter_params,
31 _distribute_module_new_parameter,
32 _distribute_module_param_source,
33 _distribute_module_set_param,
34)
35from hyper_parallel.core.dtensor.placement_types import Partial, Placement, Replicate, Shard
36from hyper_parallel.platform import get_platform
38platform = get_platform()
39Module = platform.Module
42def _src_data_rank_for_tensor(tensor: Any, src_data_rank: Optional[int]) -> Optional[int]:
43 """Disable source-rank communication while sharding meta tensors."""
44 if getattr(tensor, "is_meta", False):
45 return None
46 return src_data_rank
49__all__ = [
50 "ParallelStyle",
51 "ColwiseParallel",
52 "RowwiseParallel",
53 "SequenceParallel",
54 "PrepareModuleInput",
55 "PrepareModuleInputOutput",
56 "PrepareModuleOutput",
57 "NoParallel",
58]
61class ParallelStyle(ABC):
62 """Abstract base class for parallel styles applied to nn.Module submodules.
64 Subclasses implement ``apply`` to wrap a module with the desired
65 parallel communication behaviour (e.g. all-to-all for context parallel).
67 ``src_data_rank`` mirrors PyTorch's tensor-parallel contract: it can be set by
68 :func:`parallelize_module` for styles that scatter/broadcast global tensors.
69 HyperParallel styles may ignore it until they integrate ``distribute_tensor``.
70 """
72 src_data_rank: Optional[int] = 0
74 @abstractmethod
75 def apply(self, module: Module, device_mesh: DeviceMesh) -> Module:
76 """Apply this parallel style to *module* in-place and return it.
78 Args:
79 module: The submodule to be parallelised.
80 device_mesh: The device mesh describing the cluster topology.
82 Returns:
83 The (possibly wrapped) module with parallelism applied.
84 """
87class ColwiseParallel(ParallelStyle):
88 """Partition a compatible module in a column-wise fashion.
90 Currently supports Linear and Embedding modules (framework-agnostic via
91 ``platform.is_linear_module`` / ``platform.is_embedding_module``).
92 Compose with :class:`RowwiseParallel` to shard MLP or Attention blocks.
94 Keyword Args:
95 input_layouts (Placement, optional):
96 DTensor layout for the module input. Used to annotate the input
97 tensor as a DTensor. Defaults to ``Replicate()``.
98 output_layouts (Placement, optional):
99 Desired DTensor layout of the module output. Defaults to
100 ``Shard(-1)`` (sharded on the last dimension).
101 use_local_output (bool, optional):
102 If ``True`` (default), convert the output DTensor back to a local
103 tensor via ``to_local()``.
105 Returns:
106 A :class:`ParallelStyle` that applies column-wise sharding.
108 Example::
110 >>> from hyper_parallel import parallelize_module, ColwiseParallel, init_device_mesh
111 >>> m = Model(...)
112 >>> tp_mesh = init_device_mesh("npu", (8,), mesh_dim_names=("tp",))
113 >>> parallelize_module(m, tp_mesh, {"linear1": ColwiseParallel()})
114 """
116 def __init__(
117 self,
118 *,
119 input_layouts: Optional[Placement] = None,
120 output_layouts: Optional[Placement] = None,
121 use_local_output: Optional[bool] = None,
122 ) -> None:
123 super().__init__()
124 self._input_layouts_arg = input_layouts
125 self._output_layouts_arg = output_layouts
126 self._use_local_output_arg = use_local_output
128 self.input_layouts: Tuple[Placement, ...] = (input_layouts or Replicate(),)
129 self.output_layouts: Tuple[Placement, ...] = (output_layouts or Shard(-1),)
130 self.desired_input_layouts: Tuple[Placement, ...] = (Replicate(),)
131 self.use_local_output = use_local_output if use_local_output is not None else True
133 def __repr__(self) -> str:
134 return (
135 f"{self.__class__.__name__}("
136 f"input_layouts={self.input_layouts}, "
137 f"output_layouts={self.output_layouts}, "
138 f"use_local_output={self.use_local_output})"
139 )
141 @staticmethod
142 def _prepare_input_fn(
143 input_layouts: Tuple[Placement, ...],
144 desired_input_layouts: Tuple[Placement, ...],
145 inputs: Any,
146 device_mesh: DeviceMesh,
147 ) -> Any:
148 """Annotate or redistribute the first positional input."""
149 input_tensor = inputs[0]
150 if not isinstance(input_tensor, DTensor):
151 input_tensor = DTensor.from_local(
152 input_tensor, device_mesh, input_layouts,
153 )
155 if input_layouts != desired_input_layouts:
156 input_tensor = input_tensor.redistribute(
157 device_mesh, desired_input_layouts,
158 )
159 # MindSpore requires tuple return from pre-hook
160 return (input_tensor,)
162 def _partition_linear_fn(self, module: Any, device_mesh: DeviceMesh) -> None:
163 """Shard Linear weight/bias along ``Shard(0)`` (column-wise)."""
164 for key, param in _distribute_module_iter_params(module):
165 if param is None:
166 continue
167 src = _distribute_module_param_source(param)
168 requires_grad = bool(getattr(param, "requires_grad", True))
169 dt = distribute_tensor(
170 src,
171 device_mesh,
172 [Shard(0)],
173 src_data_rank=_src_data_rank_for_tensor(src, self.src_data_rank),
174 )
175 new_param = _distribute_module_new_parameter(key, dt, requires_grad)
176 _distribute_module_set_param(module, key, new_param)
178 def _partition_embedding_fn(self, module: Any, device_mesh: DeviceMesh) -> None:
179 """Shard Embedding weight along ``Shard(1)`` (column-wise)."""
180 for key, param in _distribute_module_iter_params(module):
181 if param is None:
182 continue
183 src = _distribute_module_param_source(param)
184 requires_grad = bool(getattr(param, "requires_grad", True))
185 dt = distribute_tensor(
186 src,
187 device_mesh,
188 [Shard(1)],
189 src_data_rank=_src_data_rank_for_tensor(src, self.src_data_rank),
190 )
191 new_param = _distribute_module_new_parameter(key, dt, requires_grad)
192 _distribute_module_set_param(module, key, new_param)
194 @staticmethod
195 def _prepare_output_fn(
196 output_layouts: Tuple[Placement, ...],
197 use_local_output: bool,
198 outputs: Any,
199 device_mesh: DeviceMesh,
200 ) -> Any:
201 """Redistribute output to desired layout and optionally convert to local."""
202 if outputs.placements != output_layouts:
203 outputs = outputs.redistribute(device_mesh, output_layouts)
204 if use_local_output:
205 return outputs.to_local()
206 return outputs
208 def apply(self, module: Module, device_mesh: DeviceMesh) -> Module:
209 """Apply column-wise parallelism to *module*.
211 Args:
212 module: A Linear or Embedding module to be sharded.
213 device_mesh: 1-D device mesh for tensor parallelism.
215 Returns:
216 The module with distributed parameters and I/O hooks attached.
218 Raises:
219 NotImplementedError: If *module* is not a supported type.
220 """
221 if platform.is_linear_module(module):
223 def partition_fn(submodule_path, submodule, device_mesh):
224 self._partition_linear_fn(submodule, device_mesh)
226 elif platform.is_embedding_module(module):
228 def partition_fn(submodule_path, submodule, device_mesh):
229 self._partition_embedding_fn(submodule, device_mesh)
231 else:
232 raise NotImplementedError(
233 "ColwiseParallel currently only supports Linear and Embedding modules!"
234 )
236 def input_fn(forward_module, forward_inputs, device_mesh):
237 return self._prepare_input_fn(
238 self.input_layouts,
239 self.desired_input_layouts,
240 forward_inputs,
241 device_mesh,
242 )
244 def output_fn(forward_module, forward_outputs, device_mesh):
245 return self._prepare_output_fn(
246 self.output_layouts,
247 self.use_local_output,
248 forward_outputs,
249 device_mesh,
250 )
252 return distribute_module(
253 module,
254 device_mesh,
255 partition_fn,
256 input_fn,
257 output_fn,
258 )
261class RowwiseParallel(ParallelStyle):
262 """Partition a compatible module in a row-wise fashion.
264 Currently supports Linear and Embedding modules (framework-agnostic via
265 ``platform.is_linear_module`` / ``platform.is_embedding_module``).
266 Compose with :class:`ColwiseParallel` to shard MLP or Attention blocks.
268 Keyword Args:
269 input_layouts (Placement, optional):
270 DTensor layout for the module input. Defaults to ``Shard(-1)``
271 (sharded on the last dimension).
272 output_layouts (Placement, optional):
273 Desired DTensor layout of the module output. Defaults to
274 ``Replicate()`` (all-reduce / reduce-scatter from partial).
275 reduce_dtype (dtype, optional):
276 Floating-point dtype used for reducing Partial outputs; the reduced
277 output remains in this dtype. Defaults to ``None`` (reduce in the
278 output dtype).
279 use_local_output (bool, optional):
280 If ``True`` (default), convert the output DTensor back to a local
281 tensor via ``to_local()``.
283 Returns:
284 A :class:`ParallelStyle` that applies row-wise sharding.
286 Example::
287 >>> from hyper_parallel import parallelize_module, RowwiseParallel, init_device_mesh
288 >>> m = Model(...)
289 >>> tp_mesh = init_device_mesh("npu", (8,), mesh_dim_names=("tp",))
290 >>> parallelize_module(m, tp_mesh, {"linear2": RowwiseParallel()})
291 """
293 def __init__(
294 self,
295 *,
296 input_layouts: Optional[Placement] = None,
297 output_layouts: Optional[Placement] = None,
298 reduce_dtype: Optional[Any] = None,
299 use_local_output: bool = True,
300 ) -> None:
301 super().__init__()
302 self.input_layouts: Tuple[Placement, ...] = (input_layouts or Shard(-1),)
303 self.output_layouts: Tuple[Placement, ...] = (output_layouts or Replicate(),)
304 self.desired_input_layouts: Tuple[Placement, ...] = (Shard(-1),)
305 self.reduce_dtype = reduce_dtype
306 self.use_local_output = use_local_output
308 def __repr__(self) -> str:
309 return (
310 f"{self.__class__.__name__}("
311 f"input_layouts={self.input_layouts}, "
312 f"output_layouts={self.output_layouts}, "
313 f"reduce_dtype={self.reduce_dtype}, "
314 f"use_local_output={self.use_local_output})"
315 )
317 @staticmethod
318 def _prepare_input_fn(
319 input_layouts: Tuple[Placement, ...],
320 desired_input_layouts: Tuple[Placement, ...],
321 inputs: Any,
322 device_mesh: DeviceMesh,
323 ) -> Any:
324 """Annotate or redistribute the first positional input."""
325 input_tensor = inputs[0]
326 if not isinstance(input_tensor, DTensor):
327 input_tensor = DTensor.from_local(
328 input_tensor, device_mesh, input_layouts,
329 )
331 if input_layouts != desired_input_layouts:
332 input_tensor = input_tensor.redistribute(
333 device_mesh, desired_input_layouts,
334 )
335 # MindSpore requires tuple return from pre-hook
336 return (input_tensor,)
338 def _partition_linear_fn(self, module: Any, device_mesh: DeviceMesh) -> None:
339 """Shard Linear weight along ``Shard(1)`` (row-wise); bias to ``Replicate()``."""
340 for key, param in _distribute_module_iter_params(module):
341 if param is None:
342 continue
343 src = _distribute_module_param_source(param)
344 requires_grad = bool(getattr(param, "requires_grad", True))
345 placement = [Shard(1)] if key == "weight" else [Replicate()]
346 dt = distribute_tensor(
347 src,
348 device_mesh,
349 placement,
350 src_data_rank=_src_data_rank_for_tensor(src, self.src_data_rank),
351 )
352 new_param = _distribute_module_new_parameter(key, dt, requires_grad)
353 _distribute_module_set_param(module, key, new_param)
355 def _partition_embedding_fn(self, module: Any, device_mesh: DeviceMesh) -> None:
356 """Shard Embedding weight along ``Shard(0)`` (row-wise)."""
357 for key, param in _distribute_module_iter_params(module):
358 if param is None:
359 continue
360 src = _distribute_module_param_source(param)
361 requires_grad = bool(getattr(param, "requires_grad", True))
362 dt = distribute_tensor(
363 src,
364 device_mesh,
365 [Shard(0)],
366 src_data_rank=_src_data_rank_for_tensor(src, self.src_data_rank),
367 )
368 new_param = _distribute_module_new_parameter(key, dt, requires_grad)
369 _distribute_module_set_param(module, key, new_param)
371 @staticmethod
372 def _prepare_output_fn(
373 output_layouts: Tuple[Placement, ...],
374 use_local_output: bool,
375 outputs: Any,
376 device_mesh: DeviceMesh,
377 module: Optional[Module] = None,
378 reduce_dtype: Optional[Any] = None,
379 ) -> Any:
380 """Redistribute partial output and optionally convert to local."""
381 if not isinstance(outputs, DTensor):
382 # ``nn.Embedding.forward`` returns a plain tensor even when weight is sharded;
383 # treat the local values as partial along the TP mesh (sum) before redistributing.
384 if module is not None and platform.is_embedding_module(module):
385 outputs = DTensor.from_local(outputs, device_mesh, [Partial("sum")])
386 else:
387 raise TypeError(
388 "RowwiseParallel expects a DTensor from Linear outputs; "
389 f"got {type(outputs)}. If this is an unsupported module, extend I/O hooks."
390 )
391 if reduce_dtype is not None and tuple(outputs.placements) != tuple(output_layouts):
392 local_output = platform.cast_fp_tensor(reduce_dtype, outputs.to_local())
393 outputs = DTensor.from_local(local_output, outputs.device_mesh, outputs.placements)
394 if tuple(outputs.placements) != tuple(output_layouts):
395 outputs = outputs.redistribute(device_mesh, output_layouts)
396 if use_local_output:
397 return outputs.to_local()
398 return outputs
400 def apply(self, module: Module, device_mesh: DeviceMesh) -> Module:
401 """Apply row-wise parallelism to *module*.
403 Args:
404 module: A Linear or Embedding module to be sharded.
405 device_mesh: 1-D device mesh for tensor parallelism.
407 Returns:
408 The module with distributed parameters and I/O hooks attached.
410 Raises:
411 NotImplementedError: If *module* is not a supported type.
412 """
413 if platform.is_linear_module(module):
415 def partition_fn(submodule_path, submodule, device_mesh):
416 self._partition_linear_fn(submodule, device_mesh)
418 self.desired_input_layouts = (Shard(-1),)
419 elif platform.is_embedding_module(module):
421 def partition_fn(submodule_path, submodule, device_mesh):
422 self._partition_embedding_fn(submodule, device_mesh)
424 self.desired_input_layouts = (Replicate(),)
425 else:
426 raise NotImplementedError(
427 "RowwiseParallel currently only supports Linear and Embedding modules!"
428 )
430 def input_fn(forward_module, forward_inputs, device_mesh):
431 return self._prepare_input_fn(
432 self.input_layouts,
433 self.desired_input_layouts,
434 forward_inputs,
435 device_mesh,
436 )
438 def output_fn(forward_module, forward_outputs, device_mesh):
439 return self._prepare_output_fn(
440 self.output_layouts,
441 self.use_local_output,
442 forward_outputs,
443 device_mesh,
444 forward_module,
445 self.reduce_dtype,
446 )
448 return distribute_module(
449 module,
450 device_mesh,
451 partition_fn,
452 input_fn,
453 output_fn,
454 )
457class SequenceParallel(ParallelStyle):
458 """Replicate module parameters and run forward with the sequence axis sharded.
460 Matches ``torch.distributed.tensor.parallel.SequenceParallel``: activations are
461 sharded on the sequence dimension while weights stay fully replicated. Typical
462 targets are normalization and dropout layers used after row-wise / scatter
463 projections in tensor-parallel transformers (`Reducing Activation Recomputation
464 in Large Transformer Models <https://arxiv.org/abs/2205.05198>`__).
466 If the first positional input is a plain tensor, it is treated as the local
467 shard along ``sequence_dim`` and wrapped as a :class:`DTensor`. If it is already
468 a :class:`DTensor` but not sharded on that dimension, it is redistributed.
470 Keyword Args:
471 sequence_dim (int, optional):
472 Tensor dimension index for the sequence axis (e.g. ``1`` for ``(B, S, H)``).
473 Default: ``1``.
474 use_local_output (bool, optional):
475 If ``True``, return a local tensor via ``to_local()``; otherwise keep a
476 :class:`DTensor`. Default: ``False`` (PyTorch default).
478 Note:
479 Like PyTorch, this assumes sensible defaults for norm weights (e.g. ones).
480 Custom initializations should be broadcast so every rank agrees before or
481 after parallelization.
483 Example::
485 >>> from hyper_parallel import parallelize_module, SequenceParallel, init_device_mesh
486 >>> m = Model(...)
487 >>> tp_mesh = init_device_mesh("npu", (8,), mesh_dim_names=("tp",))
488 >>> parallelize_module(m, tp_mesh, {"norm": SequenceParallel()})
489 """
491 def __init__(self, *, sequence_dim: int = 1, use_local_output: bool = False) -> None:
492 super().__init__()
493 self.sequence_sharding: Tuple[Placement, ...] = (Shard(sequence_dim),)
494 self.use_local_output = use_local_output
496 def __repr__(self) -> str:
497 dim = self.sequence_sharding[0].dim
498 return (
499 f"{self.__class__.__name__}("
500 f"sequence_dim={dim}, "
501 f"use_local_output={self.use_local_output})"
502 )
504 @staticmethod
505 def _prepare_input_fn(
506 sequence_sharding: Tuple[Placement, ...],
507 mod: Module,
508 inputs: Any,
509 device_mesh: DeviceMesh,
510 ) -> Any:
511 """Ensure the first input is a :class:`DTensor` sharded on the sequence dim."""
512 input_tensor = inputs[0]
513 if isinstance(input_tensor, DTensor):
514 if tuple(input_tensor.placements) != tuple(sequence_sharding):
515 input_tensor = input_tensor.redistribute(device_mesh, sequence_sharding)
516 elif platform.is_tensor(input_tensor):
517 input_tensor = DTensor.from_local(input_tensor, device_mesh, sequence_sharding)
518 else:
519 raise ValueError(
520 f"expecting input of {mod} to be a tensor or DTensor, but got {type(input_tensor)}"
521 )
522 return (input_tensor,)
524 @staticmethod
525 def _prepare_output_fn(use_local_output: bool, outputs: Any) -> Any:
526 if use_local_output:
527 return outputs.to_local()
528 return outputs
530 def apply(self, module: Module, device_mesh: DeviceMesh) -> Module:
531 """Apply sequence-parallel hooks and replicate parameters via ``distribute_module``.
533 Args:
534 module: Submodule to parallelize (for example ``LayerNorm`` or ``Dropout``).
535 device_mesh: One-dimensional tensor-parallel device mesh.
537 Returns:
538 The same ``module`` instance with forward hooks attached and parameters
539 converted to replicated DTensors where applicable.
540 """
542 def partition_fn(_submodule_path, _submodule, _mesh):
543 return None
545 def input_fn(forward_module, forward_inputs, mesh):
546 return self._prepare_input_fn(
547 self.sequence_sharding,
548 forward_module,
549 forward_inputs,
550 mesh,
551 )
553 def output_fn(_forward_module, forward_outputs, _mesh):
554 return self._prepare_output_fn(self.use_local_output, forward_outputs)
556 return distribute_module(
557 module,
558 device_mesh,
559 partition_fn,
560 input_fn,
561 output_fn,
562 )
565class PrepareModuleInput(ParallelStyle):
566 """Prepare module forward *args* (and optional *kwargs*) as :class:`DTensor` layouts.
568 At forward time, converts each annotated positional (or keyword) tensor from local
569 to :class:`DTensor` using ``input_layouts``, then redistributes to
570 ``desired_input_layouts`` when they differ. ``None`` in a layout tuple means
571 “leave this input unchanged”.
573 Mirrors ``torch.distributed.tensor.parallel.style.PrepareModuleInput``.
575 Keyword Args:
576 input_layouts: Placements per positional arg, or a single :class:`Placement`
577 wrapped as a one-tuple. ``None`` entries skip conversion for that arg.
578 desired_input_layouts: Target placements; must match ``input_layouts`` length.
579 input_kwarg_layouts: Optional mapping kwarg name → placement for conversion.
580 desired_input_kwarg_layouts: Target placements for those kwargs (same keys).
581 use_local_output: If ``True``, convert prepared inputs back to local tensors
582 before the module runs (PyTorch names this flag ``use_local_output`` on
583 :class:`PrepareModuleInput`).
584 """
586 def __init__(
587 self,
588 *,
589 input_layouts: Optional[Union[Placement, Tuple[Optional[Placement], ...]]] = None,
590 desired_input_layouts: Optional[
591 Union[Placement, Tuple[Optional[Placement], ...]]
592 ] = None,
593 input_kwarg_layouts: Optional[Dict[str, Placement]] = None,
594 desired_input_kwarg_layouts: Optional[Dict[str, Placement]] = None,
595 use_local_output: bool = False,
596 ) -> None:
597 super().__init__()
598 self.input_layouts = (
599 (input_layouts,) if isinstance(input_layouts, Placement) else input_layouts
600 )
601 self.desired_input_layouts = (
602 (desired_input_layouts,)
603 if isinstance(desired_input_layouts, Placement)
604 else desired_input_layouts
605 )
606 self.use_local_output = use_local_output
607 if self.input_layouts is not None:
608 if self.desired_input_layouts is None:
609 raise AssertionError("desired module inputs should not be None!")
610 if len(self.input_layouts) != len(self.desired_input_layouts):
611 raise AssertionError(
612 "input_layouts and desired_input_layouts should have same length!"
613 )
614 self.with_kwargs = input_kwarg_layouts is not None
615 self.input_kwarg_layouts = input_kwarg_layouts or {}
616 self.desired_input_kwarg_layouts = desired_input_kwarg_layouts or {}
617 if self.with_kwargs:
618 if len(self.input_kwarg_layouts) != len(self.desired_input_kwarg_layouts):
619 raise AssertionError(
620 "input_kwarg_layouts and desired_input_kwarg_layouts should have "
621 "same length!"
622 )
624 def _prepare_input_arg(
625 self,
626 input_obj: Any,
627 mesh: DeviceMesh,
628 input_layout: Optional[Placement],
629 desired_layout: Optional[Placement],
630 ) -> Any:
631 """Convert one input to DTensor, redistribute if needed, optionally to_local."""
632 if input_layout is not None:
633 if isinstance(input_obj, DTensor):
634 dt_inp = input_obj
635 else:
636 if not platform.is_tensor(input_obj):
637 raise AssertionError("expecting input to be a framework tensor!")
638 dt_inp = DTensor.from_local(input_obj, mesh, (input_layout,))
640 if desired_layout is not None and input_layout != desired_layout:
641 dt_inp = dt_inp.redistribute(mesh, (desired_layout,))
643 return dt_inp.to_local() if self.use_local_output else dt_inp
644 return input_obj
646 def _prepare_input_fn(self, inputs: Any, device_mesh: DeviceMesh) -> Any:
647 """Prepare positional ``inputs`` tuple per ``input_layouts`` / ``desired_input_layouts``."""
648 if self.input_layouts is None:
649 return inputs
650 if not isinstance(inputs, tuple):
651 inputs = (inputs,)
652 if len(inputs) != len(self.input_layouts):
653 raise ValueError("module inputs and input_layouts should have same length!")
654 if self.desired_input_layouts is None:
655 raise AssertionError("desired module inputs should not be None!")
656 prepared_inputs = [
657 self._prepare_input_arg(inp, device_mesh, il, dl)
658 for inp, il, dl in zip(inputs, self.input_layouts, self.desired_input_layouts)
659 ]
660 return tuple(prepared_inputs)
662 def _prepare_input_kwarg_fn(
663 self,
664 inputs: Any,
665 kwarg_inputs: Dict[str, Any],
666 device_mesh: DeviceMesh,
667 ) -> Tuple[Any, Dict[str, Any]]:
668 """Prepare positional and keyword tensor inputs; returns ``(args, kwargs)`` for the hook."""
669 prepared_arg_inputs = self._prepare_input_fn(inputs, device_mesh)
670 prepared_kwarg_inputs: Dict[str, Any] = {}
671 for kwarg_key in kwarg_inputs:
672 kwarg_val = kwarg_inputs[kwarg_key]
673 input_layout = self.input_kwarg_layouts.get(kwarg_key)
674 desired_input_layout = self.desired_input_kwarg_layouts.get(kwarg_key)
675 prepared_kwarg_inputs[kwarg_key] = self._prepare_input_arg(
676 kwarg_val, device_mesh, input_layout, desired_input_layout
677 )
678 return (prepared_arg_inputs, prepared_kwarg_inputs)
680 def apply(self, module: Module, device_mesh: DeviceMesh) -> Module:
681 if self.with_kwargs:
683 def _pre_hook(_mod, inputs, kwargs):
684 return self._prepare_input_kwarg_fn(inputs, kwargs, device_mesh)
686 platform.register_forward_pre_hook(
687 module, _pre_hook, prepend=False, with_kwargs=True,
688 )
689 else:
691 def _pre_hook(_mod, inputs):
692 return self._prepare_input_fn(inputs, device_mesh)
694 platform.register_forward_pre_hook(module, _pre_hook, prepend=False)
695 return module
697 def __repr__(self) -> str:
698 return (
699 f"{self.__class__.__name__}("
700 f"input_layouts={self.input_layouts}, "
701 f"desired_input_layouts={self.desired_input_layouts}, "
702 f"input_kwarg_layouts={self.input_kwarg_layouts}, "
703 f"desired_input_kwarg_layouts={self.desired_input_kwarg_layouts}, "
704 f"use_local_output={self.use_local_output})"
705 )
708class PrepareModuleOutput(ParallelStyle):
709 """Prepare module forward outputs as :class:`DTensor` and redistribute layouts.
711 Registers a forward hook that treats each return value like
712 ``torch.distributed.tensor.parallel.style.PrepareModuleOutput``: optional
713 ``None`` slots in ``output_layouts`` pass that output through unchanged.
715 Keyword Args:
716 output_layouts: Current or assumed placement per output tensor.
717 desired_output_layouts: Target placements; length must match ``output_layouts``.
718 reduce_dtype: Floating-point dtype used before reducing Partial outputs;
719 the reduced output remains in this dtype.
720 use_local_output: If ``True`` (default), return local shards after redistribution.
721 """
723 def __init__(
724 self,
725 *,
726 output_layouts: Union[Placement, Tuple[Optional[Placement], ...]],
727 desired_output_layouts: Union[Placement, Tuple[Optional[Placement], ...]],
728 reduce_dtype: Optional[Any] = None,
729 use_local_output: bool = True,
730 ) -> None:
731 super().__init__()
732 self.output_layouts = (
733 (output_layouts,) if isinstance(output_layouts, Placement) else output_layouts
734 )
735 self.desired_output_layouts = (
736 (desired_output_layouts,)
737 if isinstance(desired_output_layouts, Placement)
738 else desired_output_layouts
739 )
740 self.reduce_dtype = reduce_dtype
741 self.use_local_output = use_local_output
742 if len(self.output_layouts) != len(self.desired_output_layouts):
743 raise AssertionError(
744 "output_layouts and desired_output_layouts should have same length!"
745 )
747 def _prepare_out_fn(self, outputs: Any, device_mesh: DeviceMesh) -> Any:
748 """Redistribute each output tensor per ``output_layouts`` / ``desired_output_layouts``."""
749 prepared_outputs: list = []
750 if not isinstance(outputs, tuple):
751 outputs = (outputs,)
752 if len(outputs) != len(self.output_layouts):
753 raise ValueError("module outputs and output_layouts should have same length!")
754 for out, out_layout, desired_out_layout in zip(
755 outputs, self.output_layouts, self.desired_output_layouts,
756 ):
757 if out_layout is not None:
758 if isinstance(out, DTensor):
759 dt_out = out
760 else:
761 dt_out = DTensor.from_local(out, device_mesh, (out_layout,))
762 has_partial_output = any(
763 placement.is_partial() for placement in dt_out.placements
764 )
765 if (
766 self.reduce_dtype is not None
767 and has_partial_output
768 and out_layout != desired_out_layout
769 ):
770 local_out = platform.cast_fp_tensor(self.reduce_dtype, dt_out.to_local())
771 dt_out = DTensor.from_local(local_out, dt_out.device_mesh, dt_out.placements)
772 if out_layout != desired_out_layout:
773 dt_out = dt_out.redistribute(device_mesh, (desired_out_layout,))
774 prepared_outputs.append(
775 dt_out.to_local() if self.use_local_output else dt_out
776 )
777 else:
778 prepared_outputs.append(out)
779 if len(prepared_outputs) == 1:
780 return prepared_outputs[0]
781 return tuple(prepared_outputs)
783 def apply(self, module: Module, device_mesh: DeviceMesh) -> Module:
785 def _hook(_mod, _inputs, outputs):
786 return self._prepare_out_fn(outputs, device_mesh)
788 module.register_forward_hook(_hook)
789 return module
791 def __repr__(self) -> str:
792 return (
793 f"{self.__class__.__name__}("
794 f"output_layouts={self.output_layouts}, "
795 f"desired_output_layouts={self.desired_output_layouts}, "
796 f"reduce_dtype={self.reduce_dtype}, "
797 f"use_local_output={self.use_local_output})"
798 )
801class PrepareModuleInputOutput(ParallelStyle):
802 """Combine :class:`PrepareModuleInput` and :class:`PrepareModuleOutput` on one module.
804 Same keyword arguments as the two styles, with ``use_local_input`` mapping to
805 ``PrepareModuleInput(..., use_local_output=use_local_input)`` for PyTorch parity.
806 """
808 def __init__(
809 self,
810 *,
811 input_layouts: Optional[Union[Placement, Tuple[Optional[Placement], ...]]] = None,
812 desired_input_layouts: Optional[
813 Union[Placement, Tuple[Optional[Placement], ...]]
814 ] = None,
815 input_kwarg_layouts: Optional[Dict[str, Placement]] = None,
816 desired_input_kwarg_layouts: Optional[Dict[str, Placement]] = None,
817 use_local_input: bool = False,
818 output_layouts: Union[Placement, Tuple[Optional[Placement], ...]],
819 desired_output_layouts: Union[Placement, Tuple[Optional[Placement], ...]],
820 reduce_dtype: Optional[Any] = None,
821 use_local_output: bool = True,
822 ) -> None:
823 super().__init__()
824 self.prepare_module_input = PrepareModuleInput(
825 input_layouts=input_layouts,
826 desired_input_layouts=desired_input_layouts,
827 input_kwarg_layouts=input_kwarg_layouts,
828 desired_input_kwarg_layouts=desired_input_kwarg_layouts,
829 use_local_output=use_local_input,
830 )
831 self.prepare_module_output = PrepareModuleOutput(
832 output_layouts=output_layouts,
833 desired_output_layouts=desired_output_layouts,
834 reduce_dtype=reduce_dtype,
835 use_local_output=use_local_output,
836 )
838 def apply(self, module: Module, device_mesh: DeviceMesh) -> Module:
839 self.prepare_module_input.apply(module, device_mesh)
840 self.prepare_module_output.apply(module, device_mesh)
841 return module
843 def __repr__(self) -> str:
844 p_in = self.prepare_module_input
845 p_out = self.prepare_module_output
846 return (
847 f"{self.__class__.__name__}("
848 f"input_layouts={p_in.input_layouts}, "
849 f"desired_input_layouts={p_in.desired_input_layouts}, "
850 f"input_kwarg_layouts={p_in.input_kwarg_layouts}, "
851 f"desired_input_kwarg_layouts={p_in.desired_input_kwarg_layouts}, "
852 f"use_local_input={p_in.use_local_output}, "
853 f"output_layouts={p_out.output_layouts}, "
854 f"desired_output_layouts={p_out.desired_output_layouts}, "
855 f"reduce_dtype={p_out.reduce_dtype}, "
856 f"use_local_output={p_out.use_local_output})"
857 )
860class NoParallel(ParallelStyle):
861 """Replicate module parameters without sharding, while maintaining DTensor semantics.
863 Parameters and buffers are converted to fully replicated :class:`DTensor`, and I/O
864 hooks ensure tensor conversion and layout alignment at module boundaries.
866 Use this style for modules that must perform identical computations across TP ranks,
867 such as:
869 - MoE Router/Gate modules
870 - Normalization layers when sequence parallel is disabled
871 - Any module that should not shard weights but needs DTensor compatibility
873 Keyword Args:
874 input_layout (Placement, optional):
875 Layout used to annotate the first positional input if it is a plain tensor.
876 Defaults to ``Replicate()``.
877 output_layout (Placement, optional):
878 Target layout for the module output. If the output :class:`DTensor` has a
879 different placement, it will be redistributed. Defaults to ``Replicate()``.
880 desired_input_layout (Placement, optional):
881 Final layout for the first input after annotation/redistribution.
882 Defaults to ``Replicate()``. If different from ``input_layout``, a
883 redistribution is performed.
884 use_local_output (bool, optional):
885 If ``True``, convert the output :class:`DTensor` back to a local
886 tensor via ``to_local()``. Defaults to ``True``.
888 Example::
890 >>> from hyper_parallel import parallelize_module, NoParallel, init_device_mesh
891 >>> model = Transformer(...)
892 >>> tp_mesh = init_device_mesh("npu", (8,), mesh_dim_names=("tp",))
893 >>> parallelize_module(model, tp_mesh, {
894 ... "router": NoParallel(),
895 ... "norm": SequenceParallel() if use_sp else NoParallel(),
896 ... })
897 """
899 def __init__(
900 self,
901 *,
902 input_layout: Optional[Placement] = None,
903 output_layout: Optional[Placement] = None,
904 desired_input_layout: Optional[Placement] = None,
905 use_local_output: bool = True,
906 ) -> None:
907 super().__init__()
908 self.input_layout = input_layout or Replicate()
909 self.output_layout = output_layout or Replicate()
910 self.desired_input_layout = desired_input_layout or Replicate()
911 self.use_local_output = use_local_output
913 def __repr__(self) -> str:
914 return (
915 f"{self.__class__.__name__}("
916 f"input_layout={self.input_layout}, "
917 f"output_layout={self.output_layout}, "
918 f"desired_input_layout={self.desired_input_layout}, "
919 f"use_local_output={self.use_local_output})"
920 )
922 @staticmethod
923 def _prepare_input_fn(
924 input_layout: Placement,
925 desired_input_layout: Placement,
926 inputs: Tuple[Any, ...],
927 device_mesh: DeviceMesh,
928 ) -> Any:
929 """Annotate and redistribute the first positional input.
931 If the first input is a plain tensor, wrap it as a :class:`DTensor` with
932 ``input_layout``. If the resulting :class:`DTensor` placements differ from
933 ``(desired_input_layout,)``, redistribute to the desired layout.
935 Args:
936 input_layout: Layout for :meth:`DTensor.from_local` annotation.
937 desired_input_layout: Target layout after redistribution.
938 inputs: Tuple of module forward inputs.
939 device_mesh: Device mesh for tensor distribution.
941 Returns:
942 Tuple of the first input (possibly converted/redistributed) followed by
943 any remaining positional inputs.
944 """
945 input_tensor = inputs[0]
946 if not isinstance(input_tensor, DTensor):
947 input_tensor = DTensor.from_local(
948 input_tensor, device_mesh, (input_layout,)
949 )
951 if tuple(input_tensor.placements) != (desired_input_layout,):
952 input_tensor = input_tensor.redistribute(
953 device_mesh, (desired_input_layout,)
954 )
956 return (input_tensor, *inputs[1:])
958 @staticmethod
959 def _prepare_output_fn(
960 output_layout: Placement,
961 use_local_output: bool,
962 outputs: DTensor,
963 device_mesh: DeviceMesh,
964 ) -> Any:
965 """Redistribute output and optionally convert to local tensor.
967 If the output :class:`DTensor` placement differs from ``output_layout``,
968 redistribute it. If ``use_local_output`` is ``True``, convert the output
969 to a local tensor via ``to_local()``.
971 Args:
972 output_layout: Target output layout.
973 use_local_output: If ``True``, convert output to local tensor.
974 outputs: Module forward output (:class:`DTensor`).
975 device_mesh: Device mesh for redistribution.
977 Returns:
978 The output (possibly redistributed and/or converted to local tensor).
979 """
980 if tuple(outputs.placements) != (output_layout,):
981 outputs = outputs.redistribute(device_mesh, (output_layout,))
983 if use_local_output:
984 return outputs.to_local()
985 return outputs
987 def apply(self, module: Module, device_mesh: DeviceMesh) -> Module:
988 """Apply no-parallel style: replicate params/buffers and attach I/O hooks.
990 Args:
991 module: Any ``nn.Module`` or MindSpore ``Cell`` to wrap.
992 device_mesh: 1-D device mesh for tensor parallelism.
994 Returns:
995 The module with replicated :class:`DTensor` parameters and I/O hooks attached.
996 """
998 def input_fn(forward_module, forward_inputs, mesh):
999 return self._prepare_input_fn(
1000 self.input_layout,
1001 self.desired_input_layout,
1002 forward_inputs,
1003 mesh,
1004 )
1006 def output_fn(forward_module, forward_outputs, mesh):
1007 return self._prepare_output_fn(
1008 self.output_layout,
1009 self.use_local_output,
1010 forward_outputs,
1011 mesh,
1012 )
1014 return distribute_module(
1015 module,
1016 device_mesh,
1017 partition_fn=None,
1018 input_fn=input_fn,
1019 output_fn=output_fn,
1020 )