Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / core / shard / dfunction.py: 88%
34 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-10-25 05:45 +0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-10-25 05:45 +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"""Custom distributed autograd function base class."""
16from __future__ import annotations
18from typing import Any, Callable
20import torch
22from hyper_parallel.core.dtensor.dtensor import DTensor
23from hyper_parallel.core.shard._op_dispatch import _OP_DISPATCHER
26class _LocalCallable:
27 """Named callable wrapper that exposes the op name to Torch dispatch.
29 Torch operation-name resolution inspects ``__name__``. Setting it here lets
30 ``_OP_DISPATCHER`` look up the matching ``DistributedOp``.
32 Args:
33 fn: The underlying callable to invoke.
34 op_name: Canonical op name matching the registered ``DistributedOp``.
35 """
37 def __init__(self, fn: Callable, op_name: str) -> None:
38 """Initialize the wrapper with a callable and its registered op name."""
39 self._fn = fn
40 self.__name__ = op_name
42 def __call__(self, *args: Any, **kwargs: Any) -> Any:
43 """Forward arguments to the wrapped callable."""
44 return self._fn(*args, **kwargs)
47class DFunction(torch.autograd.Function):
48 """Torch base class for user-defined distributed autograd functions.
50 Subclass this class and implement ``forward`` and ``backward`` as
51 ``@staticmethod`` methods that operate on **local** tensors. To enable
52 multi-device support, also set ``_op_name`` to a string that matches the
53 ``op_name`` of a registered ``DistributedOp`` subclass.
55 Dispatch behaviour:
57 * **No DTensor inputs** — calls ``super().apply()`` directly, going straight
58 into Torch autograd (single-device path).
59 * **DTensor inputs + ``_op_name`` set** — delegates to
60 ``_OP_DISPATCHER.dispatch``. The dispatcher extracts local tensors, calls
61 ``DistributedOp.infer_layout`` to derive the output layout, invokes the
62 local callable (which re-enters ``apply`` with plain tensors, triggering
63 the single-device path), and wraps the result as a ``DTensor``.
65 Note on non-tensor positional arguments:
66 Non-tensor positional arguments are preserved via the three-phase dispatch
67 flow (``preprocess()`` → ``infer_layout(cache_values)`` →
68 ``get_expand_impl``). Implement ``preprocess()`` in your ``DistributedOp``
69 to collect non-tensor values into ``cache_values``.
71 Example::
73 class MyAddDistOp(DistributedOp):
74 def __init__(self):
75 super().__init__("MyAdd")
77 def infer_layout(self, layouts, extra_args=None):
78 return layouts[0]
80 MyAddDistOp() # instantiation triggers registration
82 class MyAdd(DFunction):
83 _op_name = "MyAdd"
85 @staticmethod
86 def forward(ctx, x, y):
87 ctx.save_for_backward(x, y)
88 return x + y
90 @staticmethod
91 def backward(ctx, grad):
92 return grad, grad
94 result = MyAdd.apply(x, y) # plain tensors → single-device
95 result = MyAdd.apply(dtensor_x, dtensor_y) # DTensors → distributed
96 """
98 _op_name: str = None
100 @staticmethod
101 def forward(ctx: Any, *args: Any, **kwargs: Any) -> Any:
102 """Override in subclass to define the forward computation on local tensors."""
103 raise NotImplementedError("Subclasses must implement forward()")
105 @staticmethod
106 def backward(ctx: Any, *grad_outputs: Any) -> Any:
107 """Override in subclass to define the backward computation on local tensors."""
108 raise NotImplementedError("Subclasses must implement backward()")
110 @classmethod
111 def apply(cls, *args: Any, **kwargs: Any) -> Any:
112 """Execute the function, routing to distributed dispatch when DTensor inputs are present.
114 Args:
115 *args: Positional arguments forwarded to ``forward``. May mix
116 ``DTensor`` and plain ``Tensor``.
117 **kwargs: Keyword arguments forwarded to ``forward``.
119 Returns:
120 Output of the forward computation. Wrapped as ``DTensor`` when the
121 distributed dispatch path is taken and the ``DistributedOp`` infers a
122 valid output layout.
124 Raises:
125 ValueError: If ``DTensor`` inputs are detected but ``_op_name`` is not
126 set on the subclass.
127 """
128 has_dtensor = any(isinstance(a, DTensor) for a in args)
129 if has_dtensor:
130 if cls._op_name is None:
131 raise ValueError(
132 f"{cls.__name__} received DTensor inputs but '_op_name' is not set. "
133 "Set '_op_name' on the subclass and register a matching DistributedOp."
134 )
135 return _OP_DISPATCHER.dispatch(cls._get_local_callable(), args, kwargs)
136 return super().apply(*args, **kwargs)
138 @classmethod
139 def _get_local_callable(cls) -> _LocalCallable:
140 """Return (and lazily create) the named local callable for this subclass.
142 The callable re-enters ``cls.apply`` with plain tensors so the single-device
143 autograd path is taken without recursion. Cached per subclass in
144 ``cls.__dict__`` to avoid repeated object creation.
146 Returns:
147 A ``_LocalCallable`` whose ``__name__`` equals ``cls._op_name``,
148 enabling Torch operation-name resolution to find the correct
149 ``DistributedOp``.
150 """
151 if '_local_callable' not in cls.__dict__:
152 def _local_fn(*a, **kw):
153 # Called by _OP_DISPATCHER with extracted local (non-DTensor) tensors.
154 # has_dtensor will be False here, so super().apply() is taken directly.
155 return cls.apply(*a, **kw)
157 cls._local_callable = _LocalCallable(_local_fn, cls._op_name)
158 return cls._local_callable