已合并
migrate distributed ops to the new dispatch flow #736
migrate distributed ops to the new dispatch flow #736
已合并
rongyue创建于 6月1日
共 27 个文件变更+1859-1066
@@ -278,6 +278,7 @@ class OpDispatcher:
278 "normal_", "uniform_", "bernoulli", "bernoulli_",278 "normal_", "uniform_", "bernoulli", "bernoulli_",
279 "native_dropout", "rand", "rand_like", "randn",279 "native_dropout", "rand", "rand_like", "randn",
280 "randn_like", "randint_like", "kaiming_uniform_",280 "randn_like", "randint_like", "kaiming_uniform_",
281+ "multinomial",
281 }282 }
282 # Only mint random op support283 # Only mint random op support
283 # MindSpore use the actual kernel name.284 # MindSpore use the actual kernel name.
@@ -438,14 +439,15 @@ class OpDispatcher:
438 439 
439 return input_layouts, extra_args, input_args, input_kwargs, cache_key_values440 return input_layouts, extra_args, input_args, input_kwargs, cache_key_values
440 441 
441- def _with_layout_infer(self, func: callable, *args, **kwargs) -> Tensor:442+ def _with_layout_infer(self, func: callable, *args, _packed_call=None, **kwargs) -> Tensor:
442- """_with_layout_infer"""443+ """_with_layout_infer
444+ 
445+ NOTE: aclop packed args are already normalized upstream by
446+ ``_dispatch_layout_infer`` via ``_normalize_aclop_args``. The
447+ ``_packed_call`` kwarg carries the ``(prim, name)`` tuple when the
448+ op uses aclop format, or ``None`` otherwise.
449+ """
443 func_name = platform.get_op_name(func)450 func_name = platform.get_op_name(func)
444- packed_call = None
445- if(func_name in self.unpack_ops and len(args) == 3 and
446- isinstance(args[1], str) and isinstance(args[2],(tuple,list))):
447- packed_call = (args[0], args[1])
448- args = tuple(args[2])
449 451 
450 input_layouts, extra_args, input_args, input_kwargs, cache_key_values = \452 input_layouts, extra_args, input_args, input_kwargs, cache_key_values = \
451 OpDispatcher._process_args_and_kwargs(args, kwargs)453 OpDispatcher._process_args_and_kwargs(args, kwargs)
@@ -469,10 +471,7 @@ class OpDispatcher:
469 if op_impl is None:471 if op_impl is None:
470 op_impl = func472 op_impl = func
471 473 
472- if packed_call is not None:474+ py_output = OpDispatcher._call_op_impl(op_impl, _packed_call, input_args, input_kwargs)
473- py_output = op_impl(packed_call[0], packed_call[1], tuple(input_args), **input_kwargs)
474- else:
475- py_output = op_impl(*input_args, **input_kwargs)
476 475 
477 if isinstance(py_output, (tuple, list)):476 if isinstance(py_output, (tuple, list)):
478 output = ()477 output = ()
@@ -530,8 +529,14 @@ class OpDispatcher:
530 529 
531 return DTensor.from_local(py_output, output_layout.mesh, output_layout.alias_placements)530 return DTensor.from_local(py_output, output_layout.mesh, output_layout.alias_placements)
532 531 
533- def _with_layout_infer_with_tuple_expand(self, func: callable, *args, **kwargs) -> Tensor:532+ def _with_layout_infer_with_tuple_expand(self, func: callable, *args, _packed_call=None, **kwargs) -> Tensor:
534- """_with_layout_infer_with_tuple_expand"""533+ """_with_layout_infer_with_tuple_expand
534+ 
535+ NOTE: aclop packed args are already normalized upstream by
536+ ``_dispatch_layout_infer`` via ``_normalize_aclop_args``. The
537+ ``_packed_call`` kwarg carries the ``(prim, name)`` tuple when the
538+ op uses aclop format, or ``None`` otherwise.
539+ """
535 expanded_args = []540 expanded_args = []
536 input_args = []541 input_args = []
537 for arg in args:542 for arg in args:
@@ -571,7 +576,7 @@ class OpDispatcher:
571 if op_impl is None:576 if op_impl is None:
572 op_impl = func577 op_impl = func
573 578 
574- py_output = op_impl(*input_args, **input_kwargs)579+ py_output = OpDispatcher._call_op_impl(op_impl, _packed_call, input_args, input_kwargs)
575 return distribute_op.wrap_output(py_output, output_layout)580 return distribute_op.wrap_output(py_output, output_layout)
576 581 
577 @staticmethod582 @staticmethod
@@ -704,15 +709,15 @@ class OpDispatcher:
704 709 
705 return input_layouts, input_shapes, extra_args, input_args, input_kwargs, cache_key_values710 return input_layouts, input_shapes, extra_args, input_args, input_kwargs, cache_key_values
706 711 
707- def _with_layout_infer_with_shape(self, func: callable, *args, **kwargs) -> Tensor:712+ def _with_layout_infer_with_shape(self, func: callable, *args, _packed_call=None, **kwargs) -> Tensor:
708- """_with_layout_infer_with_shape"""713+ """_with_layout_infer_with_shape
714+ 
715+ NOTE: aclop packed args are already normalized upstream by
716+ ``_dispatch_layout_infer`` via ``_normalize_aclop_args``. The
717+ ``_packed_call`` kwarg carries the ``(prim, name)`` tuple when the
718+ op uses aclop format, or ``None`` otherwise.
719+ """
709 func_name = platform.get_op_name(func)720 func_name = platform.get_op_name(func)
710- packed_call = None
711- # Packed fallback args for some ops (e.g. Mod: (prim_obj, "Mod", (x, y))).
712- if (func_name in self.unpack_ops and len(args) == 3 and
713- isinstance(args[1], str) and isinstance(args[2], (tuple, list))):
714- packed_call = (args[0], args[1])
715- args = tuple(args[2])
716 721 
717 (input_layouts, input_shapes, extra_args, input_args,722 (input_layouts, input_shapes, extra_args, input_args,
718 input_kwargs, cache_key_values) = OpDispatcher._process_args_and_kwargs_with_shape(args, kwargs)723 input_kwargs, cache_key_values) = OpDispatcher._process_args_and_kwargs_with_shape(args, kwargs)
@@ -738,10 +743,7 @@ class OpDispatcher:
738 if op_impl is None:743 if op_impl is None:
739 op_impl = func744 op_impl = func
740 745 
741- if packed_call is not None:746+ py_output = OpDispatcher._call_op_impl(op_impl, _packed_call, input_args, input_kwargs)
742- py_output = op_impl(packed_call[0], packed_call[1], tuple(input_args), **input_kwargs)
743- else:
744- py_output = op_impl(*input_args, **input_kwargs)
745 747 
746 # set output layout748 # set output layout
747 if isinstance(py_output, (tuple, list)):749 if isinstance(py_output, (tuple, list)):
@@ -868,7 +870,7 @@ class OpDispatcher:
868 local_kwargs = {k: v.to_local() if isinstance(v, DTensor) else v for k, v in kwargs.items()}870 local_kwargs = {k: v.to_local() if isinstance(v, DTensor) else v for k, v in kwargs.items()}
869 first_local_arg = first_arg.to_local()871 first_local_arg = first_arg.to_local()
870 872 
871- if self._rng_tracker is None and is_rng_supported_mesh():873+ if self._rng_tracker is None and is_rng_supported_mesh(first_arg.device_mesh):
872 self._rng_tracker = OffsetBasedRNGTracker()874 self._rng_tracker = OffsetBasedRNGTracker()
873 875 
874 maybe_user_generator = local_kwargs.pop("generator", None)876 maybe_user_generator = local_kwargs.pop("generator", None)
@@ -897,6 +899,8 @@ class OpDispatcher:
897 local_args = _apply_shard_offset_to_rng_args(local_args, offset_incr)899 local_args = _apply_shard_offset_to_rng_args(local_args, offset_incr)
898 local_results = op_call(*local_args, **local_kwargs)900 local_results = op_call(*local_args, **local_kwargs)
899 else:901 else:
902+ if maybe_user_generator is not None:
903+ local_kwargs["generator"] = maybe_user_generator
900 local_results = op_call(*local_args, **local_kwargs)904 local_results = op_call(*local_args, **local_kwargs)
901 905 
902 return self._wrap_random_result(op_name, local_results, first_arg, args, kwargs)906 return self._wrap_random_result(op_name, local_results, first_arg, args, kwargs)
@@ -1138,6 +1142,58 @@ class OpDispatcher:
1138 f"(not recommended for large vocabulary), use logits.full_tensor() explicitly."1142 f"(not recommended for large vocabulary), use logits.full_tensor() explicitly."
1139 )1143 )
1140 1144 
1145+ @staticmethod
1146+ def _normalize_aclop_args(op_name: str, unpack_ops: list, args: tuple) -> tuple:
1147+ """
1148+ Normalize aclop-packed arguments for MindSpore backend operators.
1149+ 
1150+ NOTE: This handles MindSpore aclop operators whose kernel signature packs
1151+ arguments as ``(prim, op_name_str, (real_arg0, real_arg1, ...))``. The
1152+ ``prim`` and ``op_name_str`` are preserved as ``packed_call`` for the
1153+ final kernel invocation, while the real tensor arguments are extracted
1154+ for layout inference and preprocessing.
1155+ 
1156+ **aclop is planned for deprecation.** Once aclop is fully removed, this
1157+ normalization and the associated ``unpack_ops`` list can be deleted.
1158+ 
1159+ Args:
1160+ op_name (str): Canonical operator name.
1161+ unpack_ops (list): List of op names that may use aclop packed format.
1162+ args (tuple): Raw positional arguments from the op call.
1163+ 
1164+ Returns:
1165+ tuple: ``(packed_call, normalized_args)``
1166+ - **packed_call**: ``(prim, op_name_str)`` tuple for kernel
1167+ invocation, or ``None`` if no unpacking was performed.
1168+ - **normalized_args**: The real tensor arguments (unpacked if
1169+ the packed format was detected, otherwise the original args).
1170+ """
1171+ if op_name in unpack_ops and len(args) == 3 and \
1172+ isinstance(args[1], str) and isinstance(args[2], (tuple, list)):
1173+ return (args[0], args[1]), tuple(args[2])
1174+ return None, args
1175+ 
1176+ @staticmethod
1177+ def _call_op_impl(op_impl: callable, packed_call, args, kwargs: dict):
1178+ """Invoke *op_impl* with optional aclop packed-call wrapping.
1179+ 
1180+ When *packed_call* is not ``None`` the MindSpore aclop kernel expects
1181+ ``(prim, op_name, (arg0, arg1, ...))``. Otherwise *args* are spread
1182+ as positional arguments in the usual way.
1183+ 
1184+ Args:
1185+ op_impl: The op implementation callable.
1186+ packed_call: ``(prim, op_name)`` tuple or ``None``.
1187+ args: Local tensor arguments (list or tuple).
1188+ kwargs: Keyword arguments dict.
1189+ 
1190+ Returns:
1191+ Result of the *op_impl* invocation.
1192+ """
1193+ if packed_call is not None:
1194+ return op_impl(packed_call[0], packed_call[1], tuple(args), **kwargs)
1195+ return op_impl(*args, **kwargs)
1196+ 
1141 def _dispatch_layout_infer(1197 def _dispatch_layout_infer(
1142 self, op_name: str, op_call: callable, args: tuple, kwargs: dict1198 self, op_name: str, op_call: callable, args: tuple, kwargs: dict
1143 ):1199 ):
@@ -1189,25 +1245,39 @@ class OpDispatcher:
1189 cache_manager = LayoutCacheManager.get_instance()1245 cache_manager = LayoutCacheManager.get_instance()
1190 distribute_op = cache_manager.distributed_op(op_name)1246 distribute_op = cache_manager.distributed_op(op_name)
1191 1247 
1248+ # Normalize aclop-packed args before any per-op processing.
1249+ # This allows all distributed op classes (ElementWiseDistributedOp,
1250+ # GatherNdDistributedOp, etc.) to receive clean unpacked args,
1251+ # avoiding duplicated unpack logic in each class's preprocess.
1252+ packed_call, args = self._normalize_aclop_args(op_name, getattr(self, 'unpack_ops', []), args)
1253+ 
1192 result = distribute_op.preprocess(args, kwargs)1254 result = distribute_op.preprocess(args, kwargs)
1193 if result is not None:1255 if result is not None:
1194- return self._dispatch_new(op_call, distribute_op, result)1256+ return self._dispatch_new(op_call, distribute_op, packed_call, result)
1195 1257 
1196 suffix = self.layout_infer_ops[op_name].get('infer_layout_suffix', '')1258 suffix = self.layout_infer_ops[op_name].get('infer_layout_suffix', '')
1197 if not suffix:1259 if not suffix:
1198- return self._with_layout_infer(op_call, *args, **kwargs)1260+ return self._with_layout_infer(op_call, *args, _packed_call=packed_call, **kwargs)
1261+ 
1262+ if suffix == 'WithShape':
1263+ return self._with_layout_infer_with_shape(op_call, *args, _packed_call=packed_call, **kwargs)
1264+ 
1265+ if suffix == 'WithTupleExpand':
1266+ return self._with_layout_infer_with_tuple_expand(op_call, *args, _packed_call=packed_call, **kwargs)
1199 1267 
1200 handler_name = self._suffix_dispatch.get(suffix)1268 handler_name = self._suffix_dispatch.get(suffix)
1201 if handler_name is None:1269 if handler_name is None:
1202 raise RuntimeError(f"Operator {op_name} specified wrong suffix in parallel yaml.")1270 raise RuntimeError(f"Operator {op_name} specified wrong suffix in parallel yaml.")
1203 return getattr(self, handler_name)(op_call, *args, **kwargs)1271 return getattr(self, handler_name)(op_call, *args, **kwargs)
1204 1272 
1205- def _dispatch_new(self, func, distribute_op, result) -> Tensor:1273+ def _dispatch_new(self, func, distribute_op, packed_call, result) -> Tensor:
1206 """New dispatch flow using preprocess result.1274 """New dispatch flow using preprocess result.
1207 1275 
1208 Args:1276 Args:
1209 func: Original function.1277 func: Original function.
1210 distribute_op: Distributed operation instance.1278 distribute_op: Distributed operation instance.
1279+ packed_call: (prim, op_name_str) tuple for aclop kernel invocation,
1280+ or None for regular ops.
1211 result: Preprocessed result (local_args, local_kwargs, cache_values).1281 result: Preprocessed result (local_args, local_kwargs, cache_values).
1212 1282 
1213 Returns:1283 Returns:
@@ -1229,7 +1299,7 @@ class OpDispatcher:
1229 op_layout_cache[cache_key] = (infer_result, op_impl)1299 op_layout_cache[cache_key] = (infer_result, op_impl)
1230 output_layouts, _ = infer_result1300 output_layouts, _ = infer_result
1231 op_impl = func if op_impl is None else op_impl1301 op_impl = func if op_impl is None else op_impl
1232- py_output = op_impl(*local_args, **local_kwargs)1302+ py_output = OpDispatcher._call_op_impl(op_impl, packed_call, local_args, local_kwargs)
1233 return distribute_op.wrap_output(py_output, output_layouts)1303 return distribute_op.wrap_output(py_output, output_layouts)
1234 1304 
1235 def dispatch(self, op_call: callable, args: tuple, kwargs: dict) -> object:1305 def dispatch(self, op_call: callable, args: tuple, kwargs: dict) -> object:
@@ -16,82 +16,95 @@
16Distributed implementation for Cumsum operator.16Distributed implementation for Cumsum operator.
17"""17"""
18 18 
19-from hyper_parallel.core.dtensor.layout import Layout19+import copy
20+from typing import Tuple
21+ 
20from .parallel_ops import DistributedOp22from .parallel_ops import DistributedOp
21 23 
22 24 
25+def _normalize_cumsum_args(x, dim, dtype=None):
26+ return (x, dim), {'dtype': dtype}
27+ 
28+ 
23class CumsumDistributedOp(DistributedOp):29class CumsumDistributedOp(DistributedOp):
24 """Distributed implementation for torch.cumsum."""30 """Distributed implementation for torch.cumsum."""
31+ _MS_PRIMITIVE_OP_NAMES = frozenset({'CumsumExt'})
25 32 
26- def infer_layout(self, layouts, extra_args=None):33+ def preprocess(self, args: tuple, kwargs: dict) -> tuple:
27 """34 """
28- Infer output layout for torch.cumsum35+ Preprocess arguments for Cumsum operator.
29- 
30- PyTorch semantics:
31- - Computes cumulative sum along dimension `dim`
32- - Output shape is identical to input shape
33- - Operation is sequential along `dim`: each element depends on all preceding elements in that dimension
34- 
35- Critical sharding constraint:
36- - The dimension `dim` MUST be unsharded (-1 in tensor_map)
37 36 
38 Args:37 Args:
39- layouts (tuple): Layouts of inputs. Expected:38+ args (tuple): Input arguments, first element is the input tensor.
40- layouts[0] (Layout): Input tensor layout (required).39+ kwargs (dict): Keyword arguments (dim, dtype).
41- extra_args (tuple): Should contain 'dim'. Expected:
42- extra_args[0] (int): Dimension to perform cumsum over (required).
43 40 
44 Returns:41 Returns:
45- Layout: Output tensor layout (identical to input layout after validation).42+ tuple: (local_args, local_kwargs, cache_values)
46 """43 """
47- if not layouts or layouts[0] is None:44+ args, kwargs = _normalize_cumsum_args(*args, **kwargs)
48- raise ValueError(45+ input_tensor = args[0]
49- f"Operation {self.op_name}: cumsum requires a valid input tensor layout."46+ dim = args[1]
50- )47+ dtype = kwargs['dtype']
51- input_layout = layouts[0]48+ 
52- in_tensor_map = input_layout.tensor_map49+ local_input = input_tensor.to_local()
53- input_ndim = len(in_tensor_map)50+ if self.op_name in self._MS_PRIMITIVE_OP_NAMES:
51+ local_args = (local_input, dim, dtype)
52+ local_kwargs = {}
53+ else:
54+ local_args = (local_input, dim)
55+ local_kwargs = {}
56+ if dtype is not None:
57+ local_kwargs['dtype'] = dtype
58+ 
59+ cache_values = [input_tensor.layout, dim]
60+ return local_args, local_kwargs, cache_values
61+ 
62+ def infer_layout(self, cache_values: list) -> Tuple[tuple, None]:
63+ """
64+ Infer output layout for Cumsum operator.
65+ 
66+ Rules:
67+ 1. Input must not have Partial status.
68+ 2. dim must be an integer within the valid range [-ndim, ndim-1].
69+ 3. The cumsum dimension must not be sharded, including StridedShard mappings.
70+ 4. Output layout is identical to the input layout.
71+ 
72+ Args:
73+ cache_values (list): [input_layout, dim]
74+ 
75+ Returns:
76+ tuple: ((output_layout,), None)
77+ 
78+ Raises:
79+ ValueError: If input has Partial status, dim is not an int, dim is out of range,
80+ or the cumsum dimension is sharded.
81+ """
82+ layout = cache_values[0]
83+ dim = cache_values[1]
84+ 
85+ if not self._allow_partial_inputs:
86+ self._check_partial_inputs([layout])
54 87 
55- if not extra_args or extra_args[0] is None:
56- raise ValueError(
57- f"Operation {self.op_name}: cumsum requires 'dim' parameter in extra_args."
58- )
59- dim = extra_args[0]
60 if not isinstance(dim, int):88 if not isinstance(dim, int):
61 raise ValueError(89 raise ValueError(
62- f"Operation {self.op_name}: 'dim' must be an integer, got {type(dim)}."90+ f"For {self.op_name}, dimension should be int, but got {type(dim)}"
63 )91 )
64 92 
65- # Normalize negative dimensions93+ alias_map = layout.alias_tensor_map
H
Hhedongdong6月10日

仅建议:后续开发过程中如果觉得infer_layout里面的一些高频操作重复代码太多,可以抽取一些utils函数或者基类函数使用,这个慢慢总结就行。

likedislike
66- normalized_dim = dim if dim >= 0 else dim + input_ndim94+ ndim = len(alias_map)
67- if normalized_dim < 0 or normalized_dim >= input_ndim:95+ if dim < -ndim or dim >= ndim:
68 raise ValueError(96 raise ValueError(
69- f"Operation {self.op_name}: Dimension {dim} out of range for "97+ f"For {self.op_name}, dimension out of range "
70- f"{input_ndim}-dimensional input tensor."98+ f"(expected to be in range of [{-ndim}, {ndim - 1}], but got {dim})"
99+ )
100+ if dim < 0:
101+ dim += ndim
102+ 
103+ mapping = alias_map[dim]
104+ if mapping != "None":
105+ raise ValueError(
106+ f"For {self.op_name}, cumsum along a sharded dimension "
107+ f"(dim {dim} mapped to {mapping}) is not supported."
71 )108 )
72 109 
73- # cumsum dimension must be unsharded110+ return ((copy.deepcopy(layout),), None)
74- if in_tensor_map[normalized_dim] != -1:
75- raise ValueError(
76- f"Operation {self.op_name}: Cannot perform sharding on normalized dimension {normalized_dim}, "
77- f"but found sharding assignment: {in_tensor_map[normalized_dim]}"
78- )
79- 
80- mesh_shape = input_layout.mesh_shape
81- alias_name = input_layout.alias_name
82- rank_list = input_layout.rank_list
83- 
84- # Create output layout
85- def idx_to_alias(idx, aliases):
86- if idx == -1:
87- return "None"
88- return aliases[len(aliases) - idx - 1]
89- output_map = tuple(idx_to_alias(idx, alias_name) for idx in in_tensor_map)
90- 
91- output_layout = Layout(
92- mesh_shape=mesh_shape,
93- alias_name=alias_name,
94- rank_list=rank_list
95- )
96- output_layout = output_layout(*output_map)
97- return output_layout
@@ -17,9 +17,44 @@ Distributed implementation for Element-wise operator.
17"""17"""
18 18 
19import copy19import copy
20+from typing import Callable, Optional, Tuple
21+ 
20from .parallel_ops import DistributedOp22from .parallel_ops import DistributedOp
21 23 
22 24 
25+def _unwrap_local_value(value):
26+ """Convert DTensor-like values to local tensors while preserving containers."""
27+ if hasattr(value, "_layout"):
28+ return value.to_local()
29+ if isinstance(value, tuple):
30+ return tuple(_unwrap_local_value(item) for item in value)
31+ if isinstance(value, list):
32+ return [_unwrap_local_value(item) for item in value]
33+ return value
34+ 
35+ 
36+def _collect_layout_and_shape(value):
37+ """Collect layout and shape from one argument for layout inference cache."""
38+ layout = value.layout if hasattr(value, "_layout") else None
39+ shape = value.shape if hasattr(value, "shape") else None
40+ return layout, shape
41+ 
42+ 
43+def _build_elementwise_cache_values(args, kwargs):
44+ """Build cache_values from the real element-wise input arguments."""
45+ input_layouts = []
46+ input_shapes = []
47+ for value in args:
48+ layout, shape = _collect_layout_and_shape(value)
49+ input_layouts.append(layout)
50+ input_shapes.append(shape)
51+ for value in kwargs.values():
52+ layout, shape = _collect_layout_and_shape(value)
53+ input_layouts.append(layout)
54+ input_shapes.append(shape)
55+ return [*input_layouts, input_shapes]
56+ 
57+ 
23class ElementWiseDistributedOp(DistributedOp):58class ElementWiseDistributedOp(DistributedOp):
24 """59 """
25 Base class for distributed element-wise operators.60 Base class for distributed element-wise operators.
@@ -31,28 +66,51 @@ class ElementWiseDistributedOp(DistributedOp):
31 op_name (str): Name of the operator to register.66 op_name (str): Name of the operator to register.
32 """67 """
33 68 
34- def infer_layout(self, layouts, extra_args=None):69+ def preprocess(self, args: tuple, kwargs: dict) -> tuple:
70+ """
71+ Preprocess arguments for element-wise operators.
72+ 
73+ NOTE: aclop packed-args normalization (for MindSpore aclop operators
74+ like Mod, StopGradient that pack args as ``(prim, name, (real_args...))``)
75+ is handled upstream in ``OpDispatcher._dispatch_layout_infer`` via
76+ ``_normalize_aclop_args``. This method receives clean unpacked args.
77+ 
78+ Args:
79+ args (tuple): Positional arguments passed to the operator.
80+ kwargs (dict): Keyword arguments passed to the operator.
81+ 
82+ Returns:
83+ tuple: (local_args, local_kwargs, cache_values)
84+ """
85+ local_kwargs = {key: _unwrap_local_value(value) for key, value in kwargs.items()}
86+ 
87+ local_args = tuple(_unwrap_local_value(arg) for arg in args)
88+ cache_values = _build_elementwise_cache_values(args, kwargs)
H
Hhedongdong6月10日

这个分布式算子比较基础,迁移起来影响比较大,基础原则是保证存量功能正确不回退就行,能支持现在注册列表的所有算子。

从代码来看,我们后续还是要区分单输入(不需要shape),双输入(涉及广播,需要shape),各类输入可能涉及属性值等,目前统一缓存了shape也没能为他。后续可以按计划进一步优化。

likedislike
89+ return local_args, local_kwargs, cache_values
90+ 
91+ def infer_layout(self, cache_values: list) -> Tuple[tuple, None]:
35 """92 """
36 Infer output layouts for element-wise operations with broadcasting support.93 Infer output layouts for element-wise operations with broadcasting support.
37 94 
38- For element-wise operations:95+ Rules:
39- - Supports broadcasting following NumPy broadcasting rules96+ 1. Inputs must not have Partial status unless the operator explicitly allows it.
40- - All inputs must have compatible shapes for broadcasting97+ 2. Input shapes must be broadcast-compatible when shape information is available.
41- - Output will have the broadcasted shape and appropriate sharding strategy98+ 3. Broadcasting dimensions cannot be sharded.
42- - Handles both simple and complex sharding patterns (including tuple-type tensor_maps)99+ 4. Non-broadcast sharding patterns must be compatible across inputs.
100+ 5. Output layout uses the merged sharding strategy and merged Partial status.
43 101 
44 Args:102 Args:
45- layouts (tuple): Tuple of layouts for input tensors103+ cache_values (list): [input_layout_0, ..., input_layout_n, input_shapes].
46- extra_args: Extra arguments for the operation. It can be:
47- - dict containing 'input_shapes'
48- - list/tuple where the last element is input_shapes (WithShape path)
49 104 
50 Returns:105 Returns:
51- Layout: Layout for output tensor with merged sharding strategy.106+ tuple: ((output_layout,), None)
52 107 
53 Raises:108 Raises:
54 ValueError: If input layouts are not compatible for broadcasting.109 ValueError: If input layouts are not compatible for broadcasting.
55 """110 """
111+ layouts = tuple(cache_values[:-1])
112+ input_shapes = cache_values[-1] if cache_values else None
113+ 
56 if not layouts:114 if not layouts:
57 return None115 return None
58 116 
@@ -61,23 +119,19 @@ class ElementWiseDistributedOp(DistributedOp):
61 if not valid_layouts:119 if not valid_layouts:
62 return None120 return None
63 121 
64- # Check partial inputs - ElementWiseDistributedOp does not support partial by default
65- # This check is performed after basic layout validation
66 if not self._allow_partial_inputs:122 if not self._allow_partial_inputs:
67 self._check_partial_inputs(layouts)123 self._check_partial_inputs(layouts)
68 124 
69 if len(valid_layouts) == 1:125 if len(valid_layouts) == 1:
70- return valid_layouts[0]126+ return ((copy.deepcopy(valid_layouts[0]),), None)
71- 
72- input_shapes = self._extract_input_shapes(extra_args)
73 127 
74 if not input_shapes:128 if not input_shapes:
75- return self._handle_no_input_shapes(valid_layouts)129+ return ((self._handle_no_input_shapes(valid_layouts),), None)
76 130 
77 aligned_layouts, aligned_shapes = self._align_layouts_and_shapes(layouts, input_shapes)131 aligned_layouts, aligned_shapes = self._align_layouts_and_shapes(layouts, input_shapes)
78 132 
79 if len(aligned_layouts) <= 1 or len(aligned_layouts) != len(aligned_shapes):133 if len(aligned_layouts) <= 1 or len(aligned_layouts) != len(aligned_shapes):
80- return valid_layouts[0]134+ return ((copy.deepcopy(valid_layouts[0]),), None)
81 135 
82 output_shape = self._compute_output_shape(aligned_shapes)136 output_shape = self._compute_output_shape(aligned_shapes)
83 merged_tensor_map, merged_partial = self._merge_all_layouts(137 merged_tensor_map, merged_partial = self._merge_all_layouts(
@@ -89,20 +143,22 @@ class ElementWiseDistributedOp(DistributedOp):
89 143 
90 self._check_all_inputs_broadcasts_and_partial(aligned_layouts, aligned_shapes, output_shape)144 self._check_all_inputs_broadcasts_and_partial(aligned_layouts, aligned_shapes, output_shape)
91 145 
92- return self._create_output_layout(aligned_layouts[0], merged_tensor_map, merged_partial)146+ output_layout = self._create_output_layout(aligned_layouts[0], merged_tensor_map, merged_partial)
147+ return ((output_layout,), None)
93 148 
94 def _handle_no_input_shapes(self, valid_layouts):149 def _handle_no_input_shapes(self, valid_layouts):
95 """150 """
96 Handle the case when input shapes are not available.151 Handle the case when input shapes are not available.
97 """152 """
98 first_layout = valid_layouts[0]153 first_layout = valid_layouts[0]
154+ first_alias_map = first_layout.alias_tensor_map
99 for layout in valid_layouts[1:]:155 for layout in valid_layouts[1:]:
100- if layout.tensor_map != first_layout.tensor_map:156+ if layout.alias_tensor_map != first_alias_map:
101 raise ValueError(157 raise ValueError(
102 f"For {self.op_name}, cannot infer layout without shapes: "158 f"For {self.op_name}, cannot infer layout without shapes: "
103- f"mismatched tensor_map {first_layout.tensor_map} vs {layout.tensor_map}."159+ f"mismatched alias_tensor_map {first_alias_map} vs {layout.alias_tensor_map}."
104 )160 )
105- return first_layout161+ return copy.deepcopy(first_layout)
106 162 
107 def _align_layouts_and_shapes(self, layouts, input_shapes):163 def _align_layouts_and_shapes(self, layouts, input_shapes):
108 """164 """
@@ -169,24 +225,6 @@ class ElementWiseDistributedOp(DistributedOp):
169 225 
170 return merged_tensor_map, merged_partial226 return merged_tensor_map, merged_partial
171 227 
172- def _extract_input_shapes(self, extra_args):
173- """
174- Extract input_shapes from extra_args.
175- 
176- Compatible with:
177- - dict: {"input_shapes": [...]}
178- - list/tuple (WithShape dispatcher): extra_args = [..., input_shapes]
179- """
180- if isinstance(extra_args, dict):
181- return extra_args.get("input_shapes", None)
182- 
183- if isinstance(extra_args, (list, tuple)) and extra_args:
184- maybe_shapes = extra_args[-1]
185- if isinstance(maybe_shapes, (list, tuple)):
186- return maybe_shapes
187- 
188- return None
189- 
190 def _merge_partial_status(self, partial1, partial2, merged_tensor_map, tensor_map1, tensor_map2, layouts):228 def _merge_partial_status(self, partial1, partial2, merged_tensor_map, tensor_map1, tensor_map2, layouts):
191 """229 """
192 Merge partial status from two inputs.230 Merge partial status from two inputs.
@@ -313,61 +351,6 @@ class ElementWiseDistributedOp(DistributedOp):
313 f"Should be without Partial status for broadcasting without communication"351 f"Should be without Partial status for broadcasting without communication"
314 )352 )
315 353 
316- def _merge_tensor_maps_without_shape(self, layout1, layout2):
317- """
318- Merge tensor_maps without shape information (for broadcasting scenarios).
319- 
320- Merging rules without shape:
321- - If both dimensions are not sharded: use -1
322- - If one is sharded and one is not: use the sharded one (assume broadcasting)
323- - If both are sharded: they must be identical, otherwise raise error
324- 
325- Args:
326- layout1: Layout of the first input
327- layout2: Layout of the second input
328- 
329- Returns:
330- tuple: Merged tensor_map
331- 
332- Raises:
333- ValueError: If sharding strategies conflict
334- """
335- map1 = layout1.tensor_map if layout1.tensor_map else tuple()
336- map2 = layout2.tensor_map if layout2.tensor_map else tuple()
337- 
338- # Align ranks by padding with -1
339- max_len = max(len(map1), len(map2))
340- padded_map1 = (-1,) * (max_len - len(map1)) + map1
341- padded_map2 = (-1,) * (max_len - len(map2)) + map2
342- 
343- merged_map = []
344- for i, (m1, m2) in enumerate(zip(padded_map1, padded_map2)):
345- m1_axis = self._normalize_tensor_map_element(m1)
346- m2_axis = self._normalize_tensor_map_element(m2)
347- 
348- m1_axis_for_compare = frozenset(m1_axis)
349- m2_axis_for_compare = frozenset(m2_axis)
350- 
351- m1_is_sharded = bool(m1_axis)
352- m2_is_sharded = bool(m2_axis)
353- 
354- if not m1_is_sharded and not m2_is_sharded:
355- merged_map.append(-1)
356- elif not m1_is_sharded:
357- merged_map.append(self._denormalize_tensor_map_element(m2_axis))
358- elif not m2_is_sharded:
359- merged_map.append(self._denormalize_tensor_map_element(m1_axis))
360- else:
361- if m1_axis_for_compare != m2_axis_for_compare:
362- raise ValueError(
363- f"For {self.op_name}, inputs should have same sharding pattern, "
364- f"but got confilcting sharding at dimension {i}, "
365- f"input1 shaded on {m1_axis} and input2 shaded on {m2_axis}."
366- )
367- merged_map.append(self._denormalize_tensor_map_element(m1_axis))
368- 
369- return tuple(merged_map)
370- 
371 def _broadcast_shapes(self, shape1, shape2):354 def _broadcast_shapes(self, shape1, shape2):
372 """355 """
373 Calculate the broadcasted shape of two shapes according to broadcasting rules.356 Calculate the broadcasted shape of two shapes according to broadcasting rules.
@@ -621,33 +604,65 @@ class AddDistributedOp(ElementWiseWithPartialDistributedOp):
621 which is useful for operations like gradient accumulation where partial604 which is useful for operations like gradient accumulation where partial
622 results need to be preserved through the computation graph.605 results need to be preserved through the computation graph.
623 """606 """
624- def get_expand_impl(self, func, infer_result, layouts, extra_args=None):607+ def infer_layout(self, cache_values: list) -> Tuple[tuple, None]:
608+ """
609+ Infer output layout for Add operator.
610+ 
611+ Rules:
612+ 1. Follow element-wise broadcasting and sharding merge rules.
613+ 2. Partial status is allowed and propagated.
614+ 3. Propagated Partial status must be "sum" or None.
615+ 
616+ Args:
617+ cache_values (list): [input_layout_0, ..., input_layout_n, input_shapes].
618+ 
619+ Returns:
620+ tuple: ((output_layout,), None)
621+ 
622+ Raises:
623+ ValueError: If propagated Partial status is not "sum" or None.
624+ """
625+ infer_result = super().infer_layout(cache_values)
626+ if infer_result is None:
627+ return infer_result
628+ 
629+ output_layout = infer_result[0][0]
630+ for i, partial_type in enumerate(output_layout.partial):
631+ if partial_type is not None and partial_type != "sum":
632+ raise ValueError(
633+ f"For {self.op_name}, inputs partial status should be 'sum' or None, "
634+ f"but got {partial_type} at index {i}."
635+ )
636+ 
637+ return infer_result
638+ 
639+ def get_expand_impl(self, func: Optional[Callable], infer_result: tuple,
640+ cache_values: list) -> Optional[Callable]:
625 """641 """
626 Get expand implementation for the operator642 Get expand implementation for the operator
627 """643 """
644+ layouts = tuple(cache_values[:-1])
628 x1_layout = layouts[0]645 x1_layout = layouts[0]
629 x2_layout = layouts[1]646 x2_layout = layouts[1]
630- x1_partial = x1_layout.is_partial() if x1_layout is not None else None647+ x1_partial = x1_layout.is_partial() if x1_layout is not None else False
631- x2_partial = x2_layout.is_partial() if x2_layout is not None else None648+ x2_partial = x2_layout.is_partial() if x2_layout is not None else False
632 649 
633- if x1_partial != x2_partial:650+ if x1_partial == x2_partial:
634- scaling_factor = 1651+ return None
635- for i, partial_type in enumerate(infer_result.partial):
636- if partial_type == "sum":
637- scaling_factor *= infer_result.mesh_shape[i]
638- elif partial_type is not None:
639- raise ValueError(
640- f"For {self.op_name}, inputs partial status should be 'sum' or None, "
641- f"but got {partial_type} at index {i}."
642- )
643 652 
644- # use expand_impl only when one of x1 and x2 is with partial placement.653+ output_layout = infer_result[0][0]
645- def expand_impl1(x1, x2):654+ scaling_factor = 1
646- add_out = func(x1 / scaling_factor, x2)655+ for i, partial_type in enumerate(output_layout.partial):
647- return add_out656+ if partial_type == "sum":
657+ scaling_factor *= output_layout.mesh_shape[i]
648 658 
649- def expand_impl2(x1, x2):659+ # use expand_impl only when one of x1 and x2 is with partial placement.
650- add_out = func(x1, x2 / scaling_factor)660+ def _expand_impl1(x1, x2, *args, **kwargs):
651- return add_out661+ add_out = func(x1 / scaling_factor, x2, *args, **kwargs)
652- return expand_impl1 if not x1_partial else expand_impl2662+ return add_out
653- return None663+ 
664+ def _expand_impl2(x1, x2, *args, **kwargs):
665+ add_out = func(x1, x2 / scaling_factor, *args, **kwargs)
666+ return add_out
667+ 
668+ return _expand_impl1 if not x1_partial else _expand_impl2
@@ -12,37 +12,109 @@
12# See the License for the specific language governing permissions and12# See the License for the specific language governing permissions and
13# limitations under the License.13# limitations under the License.
14# ============================================================================14# ============================================================================
15-"""15+"""Distributed implementation for the embedding operator."""
16-Distributed implementation for the embedding operator.16+ 
17-This module handles Tensor Parallelism for Embedding layers (Column/Row Parallel).17+from typing import Callable, Optional, Tuple
18-"""
19 18 
20from hyper_parallel.core.dtensor.layout import Layout19from hyper_parallel.core.dtensor.layout import Layout
21from .parallel_ops import DistributedOp20from .parallel_ops import DistributedOp
22 21 
23 22 
23+def _normalize_embedding_args(input_tensor, weight_tensor, padding_idx=None, max_norm=None,
24+ norm_type=2.0, scale_grad_by_freq=False, sparse=False):
25+ return (
26+ input_tensor,
27+ weight_tensor,
28+ padding_idx,
29+ max_norm,
30+ norm_type,
31+ scale_grad_by_freq,
32+ sparse,
33+ ), {}
34+ 
35+ 
24class EmbeddingDistributedOp(DistributedOp):36class EmbeddingDistributedOp(DistributedOp):
25 """37 """
26 Distributed implementation for embedding operators.38 Distributed implementation for embedding operators.
27 Supports Column Parallelism (CP) and Row Parallelism (RP).39 Supports Column Parallelism (CP) and Row Parallelism (RP).
28 """40 """
41+ _MS_PRIMITIVE_OP_NAMES = frozenset({'Embedding'})
29 42 
30- def infer_layout(self, layouts, extra_args=None):43+ def preprocess(self, args: tuple, kwargs: dict) -> tuple:
31 """44 """
32- Infers the output layout (sharding state) based on input and weight layouts.45+ Preprocess arguments for Embedding operator.
46+ 
47+ Args:
48+ args (tuple): Input arguments containing input and weight tensors.
49+ kwargs (dict): Keyword arguments for embedding options.
50+ 
51+ Returns:
52+ tuple: (local_args, local_kwargs, cache_values) where cache_values is
53+ [input_layout, weight_layout].
33 """54 """
34- if not layouts or len(layouts) < 2:55+ args, _ = _normalize_embedding_args(*args, **kwargs)
56+ input_tensor, weight_tensor = args[0], args[1]
57+ if self.op_name in self._MS_PRIMITIVE_OP_NAMES:
58+ local_args = (input_tensor.to_local(), weight_tensor.to_local()) + args[2:6]
59+ else:
60+ local_args = (input_tensor.to_local(), weight_tensor.to_local()) + args[2:]
61+ local_kwargs = {}
62+ cache_values = [input_tensor.layout, weight_tensor.layout]
63+ return local_args, local_kwargs, cache_values
64+ 
65+ def infer_layout(self, cache_values: list) -> Tuple[tuple, None]:
66+ """
67+ Infer output layout for Embedding operator.
68+ 
69+ Rules:
70+ 1. Input and weight must not have Partial status.
71+ 2. Input and weight must share the same mesh_shape.
72+ 3. Weight must be 2D [vocab_size, embedding_dim].
73+ 4. Output shape is [*input_shape, embedding_dim], preserving input sharding
74+ and inheriting weight embedding-dimension sharding.
75+ 5. If weight vocab dimension is sharded, output carries Partial('sum') on
76+ that vocab sharding axis.
77+ 
78+ Args:
79+ cache_values (list): [input_layout, weight_layout].
80+ 
81+ Returns:
82+ tuple: ((output_layout,), None)
83+ 
84+ Raises:
85+ ValueError: If layouts are missing, partial, incompatible, or weight is not 2D.
86+ """
87+ if len(cache_values) != 2:
35 raise ValueError(88 raise ValueError(
36- f"Operation {self.op_name}: requires both input and weight layouts."89+ f"For {self.op_name}, cache_values length should be 2, but got {len(cache_values)}"
90+ )
91+ 
92+ input_layout = cache_values[0]
93+ weight_layout = cache_values[1]
94+ if not input_layout or not weight_layout:
95+ raise ValueError(
96+ f"For {self.op_name}, requires both input and weight layouts."
97+ )
98+ 
99+ self._check_partial_inputs([input_layout, weight_layout])
100+ 
101+ if input_layout.mesh_shape != weight_layout.mesh_shape:
102+ raise ValueError(
103+ f"For {self.op_name}, input and weight must have the same mesh_shape, "
104+ f"but got input: {input_layout.mesh_shape} and weight: {weight_layout.mesh_shape}"
37 )105 )
38 106 
39- input_layout = layouts[0]
40- weight_layout = layouts[1]
41 weight_tensor_map = weight_layout.tensor_map107 weight_tensor_map = weight_layout.tensor_map
108+ if len(weight_tensor_map) != 2:
109+ raise ValueError(
110+ f"For {self.op_name}, weight should be 2D [vocab_size, embedding_dim], "
111+ f"but got {len(weight_tensor_map)}D"
112+ )
42 113 
43 # weight_tensor_map: [vocab_size_dim, embed_dim_dim]114 # weight_tensor_map: [vocab_size_dim, embed_dim_dim]
44- w_shard_vocab_axis = weight_tensor_map[0]
45 w_shard_embed_axis = weight_tensor_map[1]115 w_shard_embed_axis = weight_tensor_map[1]
116+ weight_alias_map = weight_layout.alias_tensor_map
117+ w_shard_vocab_alias = weight_alias_map[0]
46 118 
47 # Output shape is [*input_shape, embed_dim]119 # Output shape is [*input_shape, embed_dim]
48 output_tensor_map = list(input_layout.tensor_map)120 output_tensor_map = list(input_layout.tensor_map)
@@ -56,18 +128,17 @@ class EmbeddingDistributedOp(DistributedOp):
56 output_layout.set_tensor_map(tuple(output_tensor_map))128 output_layout.set_tensor_map(tuple(output_tensor_map))
57 129 
58 # If vocab is sharded (Row Parallelism), output is in Partial Sum state130 # If vocab is sharded (Row Parallelism), output is in Partial Sum state
59- if w_shard_vocab_axis != -1:131+ if w_shard_vocab_alias != "None":
60 # pylint: disable=protected-access132 # pylint: disable=protected-access
61 output_layout._partial = list(input_layout.partial)133 output_layout._partial = list(input_layout.partial)
62- vocab_axis_name = input_layout.alias_name[len(input_layout.alias_name) - 1 - w_shard_vocab_axis]134+ output_layout.set_partial_by_dev_axis(w_shard_vocab_alias, 'sum')
63- output_layout.set_partial_by_dev_axis(vocab_axis_name, 'sum')
64 # pylint: disable=protected-access135 # pylint: disable=protected-access
65 output_layout._alias_tensor_map = output_layout._build_readable_tensor_map()136 output_layout._alias_tensor_map = output_layout._build_readable_tensor_map()
66 # pylint: disable=protected-access137 # pylint: disable=protected-access
67 output_layout.tensor_map_to_placement()138 output_layout.tensor_map_to_placement()
68 output_layout.update_compact_str()139 output_layout.update_compact_str()
69 140 
70- return output_layout141+ return ((output_layout,), None)
71 142 
72 def _parse_params(self, args, kwargs):143 def _parse_params(self, args, kwargs):
73 """144 """
@@ -122,17 +193,20 @@ class EmbeddingDistributedOp(DistributedOp):
122 193 
123 return local_input, mask_int194 return local_input, mask_int
124 195 
125- def get_expand_impl(self, func, infer_result, layouts, extra_args=None):196+ def get_expand_impl(self, func: Optional[Callable], infer_result: tuple,
197+ cache_values: list) -> Optional[Callable]:
126 """198 """
127 Returns the execution implementation wrapper.199 Returns the execution implementation wrapper.
128 Helper functions are used to keep Cyclomatic Complexity (CCN) low.200 Helper functions are used to keep Cyclomatic Complexity (CCN) low.
129 """201 """
130- weight_layout = layouts[1]202+ weight_layout = cache_values[1]
131 w_shard_vocab_axis = weight_layout.tensor_map[0]203 w_shard_vocab_axis = weight_layout.tensor_map[0]
132- w_shard_embed_axis = weight_layout.tensor_map[1]204+ weight_alias_map = weight_layout.alias_tensor_map
205+ w_shard_vocab_alias = weight_alias_map[0]
206+ w_shard_embed_alias = weight_alias_map[1]
133 207 
134 # Use native implementation if no weight sharding is applied208 # Use native implementation if no weight sharding is applied
135- if w_shard_vocab_axis == -1 and w_shard_embed_axis == -1:209+ if w_shard_vocab_alias == "None" and w_shard_embed_alias == "None":
136 return None210 return None
137 211 
138 def distributed_embedding_impl(*args, **kwargs):212 def distributed_embedding_impl(*args, **kwargs):
@@ -144,18 +218,24 @@ class EmbeddingDistributedOp(DistributedOp):
144 218 
145 # Check for max_norm with specific error messages for CP and RP219 # Check for max_norm with specific error messages for CP and RP
146 if max_norm is not None:220 if max_norm is not None:
147- if w_shard_embed_axis != -1:221+ if w_shard_embed_alias != "None":
148- raise ValueError("Column-Parallel Embedding does not support `max_norm` parameter.")222+ raise ValueError(
149- if w_shard_vocab_axis != -1:223+ f"For {self.op_name}, Column-Parallel Embedding does not support `max_norm` parameter."
150- raise ValueError("Row-Parallel Embedding does not support `max_norm` parameter.")224+ )
225+ if w_shard_vocab_alias != "None":
226+ raise ValueError(
227+ f"For {self.op_name}, Row-Parallel Embedding does not support `max_norm` parameter."
228+ )
151 229 
152 # Check for scale_grad_by_freq with RP230 # Check for scale_grad_by_freq with RP
153- if scale_grad and w_shard_vocab_axis != -1:231+ if scale_grad and w_shard_vocab_alias != "None":
154- raise ValueError("Row-Parallel Embedding does not support `scale_grad_by_freq=True`.")232+ raise ValueError(
233+ f"For {self.op_name}, Row-Parallel Embedding does not support `scale_grad_by_freq=True`."
234+ )
155 235 
156 # 2. Row Parallel Processing236 # 2. Row Parallel Processing
157 input_mask_int = None237 input_mask_int = None
158- if w_shard_vocab_axis != -1:238+ if w_shard_vocab_alias != "None":
159 is_args_pad = len(args) > 2239 is_args_pad = len(args) > 2
160 mapped_input, input_mask_int = self._handle_rp_input(240 mapped_input, input_mask_int = self._handle_rp_input(
161 input_tensor, weight_tensor, weight_layout, w_shard_vocab_axis,241 input_tensor, weight_tensor, weight_layout, w_shard_vocab_axis,
@@ -167,7 +247,7 @@ class EmbeddingDistributedOp(DistributedOp):
167 output = func(*new_args, **new_kwargs)247 output = func(*new_args, **new_kwargs)
168 248 
169 # 4. Erase invalid partial embeddings (Row-Parallel only)249 # 4. Erase invalid partial embeddings (Row-Parallel only)
170- if w_shard_vocab_axis != -1 and input_mask_int is not None:250+ if w_shard_vocab_alias != "None" and input_mask_int is not None:
171 expanded_mask = input_mask_int[..., None].to(output.dtype)251 expanded_mask = input_mask_int[..., None].to(output.dtype)
172 output = output * expanded_mask252 output = output * expanded_mask
173 253 
@@ -15,45 +15,92 @@
15"""15"""
16Distributed implementation for ExpandDims operator.16Distributed implementation for ExpandDims operator.
17"""17"""
18+from typing import Tuple
19+ 
18from hyper_parallel.core.dtensor.layout import Layout20from hyper_parallel.core.dtensor.layout import Layout
19from .parallel_ops import DistributedOp21from .parallel_ops import DistributedOp
20 22 
21 23 
24+def _normalize_expand_dims_args(x, axis=None, dim=None):
25+ if axis is None:
26+ axis = dim
27+ return (x, axis), {}
28+ 
29+ 
22class ExpandDimsDistributedOp(DistributedOp):30class ExpandDimsDistributedOp(DistributedOp):
23 """Distributed implementation for ExpandDims operator."""31 """Distributed implementation for ExpandDims operator."""
24 32 
25- def infer_layout(self, layouts, extra_args=None):33+ def preprocess(self, args: tuple, kwargs: dict) -> tuple:
26 """34 """
27- Infer output layout for ExpandDims.35+ Preprocess arguments for ExpandDims operator.
28 36 
29 Args:37 Args:
30- layouts (tuple): Tuple containing input layout.38+ args (tuple): Input arguments, first element is the input tensor.
31- extra_args: axis parameter (int or list/tuple containing int).39+ kwargs (dict): Keyword arguments (axis or dim).
32 40 
33 Returns:41 Returns:
34- Layout: Output layout with inserted dimension.42+ tuple: (local_args, local_kwargs, cache_values)
35 """43 """
36- if not layouts:44+ args, _ = _normalize_expand_dims_args(*args, **kwargs)
37- raise ValueError(f"{self.__class__.__name__} requires at least one input layout")45+ input_tensor, axis = args[0], args[1]
46+ local_args = (input_tensor.to_local(), axis)
47+ cache_values = [input_tensor.layout, axis]
48+ return local_args, {}, cache_values
38 49 
39- x_layout = layouts[0]50+ def infer_layout(self, cache_values: list) -> Tuple[tuple, None]:
51+ """
52+ Infer output layout for ExpandDims operator.
53+ 
54+ Rules:
55+ 1. Input must not have Partial status.
56+ 2. axis must be an integer within the valid range [-(rank + 1), rank].
57+ 3. The inserted dimension is replicated.
58+ 4. Existing input dimension mappings are shifted and otherwise preserved.
59+ 
60+ Args:
61+ cache_values (list): [input_layout, axis]
62+ 
63+ Returns:
64+ tuple: ((output_layout,), None)
65+ 
66+ Raises:
67+ ValueError: If input has Partial status, input layout is missing,
68+ axis is missing or invalid, or axis is out of range.
69+ """
70+ if not cache_values:
71+ raise ValueError(
72+ f"For {self.op_name}, cache_values should contain input layout, "
73+ f"but got empty cache_values."
74+ )
75+ 
76+ x_layout = cache_values[0]
77+ if not self._allow_partial_inputs:
78+ self._check_partial_inputs([x_layout])
40 79 
41 if x_layout.mesh_shape is None:80 if x_layout.mesh_shape is None:
42- raise ValueError("Input layout cannot be None.")81+ raise ValueError(
82+ f"For {self.op_name}, input layout mesh_shape should not be None, "
83+ f"but got None."
84+ )
43 85 
44- if not extra_args:86+ axis = cache_values[1] if len(cache_values) > 1 else None
45- raise ValueError(f"{self.op_name}: axis parameter is required")
46 87 
47- axis = extra_args[0] if isinstance(extra_args, (tuple, list)) else extra_args88+ if axis is None:
89+ raise ValueError(f"For {self.op_name}, axis parameter is required.")
90+ if not isinstance(axis, int):
91+ raise ValueError(
92+ f"For {self.op_name}, axis should be int, but got {type(axis)}."
93+ )
48 94 
49 in_rank = len(x_layout.alias_tensor_map)95 in_rank = len(x_layout.alias_tensor_map)
96+ original_axis = axis
50 if axis < 0:97 if axis < 0:
51 axis = axis + in_rank + 198 axis = axis + in_rank + 1
52 99 
53 if axis < 0 or axis > in_rank:100 if axis < 0 or axis > in_rank:
54 raise ValueError(101 raise ValueError(
55- f"{self.op_name}: axis {axis} out of range for input rank {in_rank}. "102+ f"For {self.op_name}, axis {original_axis} out of range for input rank {in_rank}. "
56- f"Valid range is [{-in_rank-1}, {in_rank}]"103+ f"Valid range is [{-in_rank - 1}, {in_rank}]."
57 )104 )
58 105 
59 x_map = list(x_layout.alias_tensor_map)106 x_map = list(x_layout.alias_tensor_map)
@@ -66,9 +113,10 @@ class ExpandDimsDistributedOp(DistributedOp):
66 )113 )
67 output_layout = output_layout(*x_map)114 output_layout = output_layout(*x_map)
68 115 
69- for i, partial_op in enumerate(x_layout.partial):116+ if self._allow_partial_inputs:
70- if partial_op is not None:117+ for i, partial_op in enumerate(x_layout.partial):
71- dev_axis_name = x_layout.alias_name[i]118+ if partial_op is not None:
72- output_layout.set_partial_by_dev_axis(dev_axis_name, partial_op)119+ dev_axis_name = x_layout.alias_name[i]
120+ output_layout.set_partial_by_dev_axis(dev_axis_name, partial_op)
73 121 
74- return output_layout122+ return ((output_layout,), None)
@@ -15,96 +15,108 @@
15"""15"""
16Distributed implementation for Flatten operator.16Distributed implementation for Flatten operator.
17"""17"""
18+from typing import Tuple
19+ 
18from hyper_parallel.core.shard.ops.parallel_reshape import ReshapeDistributedOp20from hyper_parallel.core.shard.ops.parallel_reshape import ReshapeDistributedOp
19 21 
20 22 
23+def _normalize_flatten_args(x, start_dim=0, end_dim=-1):
24+ return (x, start_dim, end_dim), {}
25+ 
26+ 
21class FlattenDistributedOp(ReshapeDistributedOp):27class FlattenDistributedOp(ReshapeDistributedOp):
22 """Distributed implementation for torch.flatten."""28 """Distributed implementation for torch.flatten."""
23 29 
24- def infer_layout(self, layouts, extra_args=None):30+ def preprocess(self, args: tuple, kwargs: dict) -> tuple:
25 """31 """
26- Infer output layout for torch.flatten.32+ Preprocess arguments for Flatten operator.
27- 
28- PyTorch semantics:
29- - flatten(input, start_dim=0, end_dim=-1)
30- - Flattens the input tensor starting from `start_dim` to `end_dim`.
31 33 
32 Args:34 Args:
33- layouts (tuple): Layouts of inputs.35+ args (tuple): Input tensor followed by optional start_dim and end_dim.
34- extra_args (tuple): Contains scalar arguments (start_dim, end_dim)36+ kwargs (dict): Optional keyword arguments.
35- and the input shapes list appended by WithShape suffix.
36 37 
37 Returns:38 Returns:
38- Layout: Output tensor layout.39+ tuple: (local_args, local_kwargs, cache_values)
39 """40 """
40- if not layouts or layouts[0] is None:41+ args, _ = _normalize_flatten_args(*args, **kwargs)
42+ input_tensor, start_dim, end_dim = args[0], args[1], args[2]
43+ local_args = (input_tensor.to_local(), start_dim, end_dim)
44+ local_kwargs = {}
45+ cache_values = [input_tensor.layout, start_dim, end_dim, tuple(input_tensor.shape)]
46+ return local_args, local_kwargs, cache_values
47+ 
48+ def infer_layout(self, cache_values: list) -> Tuple[tuple, None]:
49+ """
50+ Infer output layout for Flatten operator.
51+ 
52+ Rules:
53+ 1. Partial input is allowed and preserved by reshape layout inference.
54+ 2. input_shape must be provided and match the input rank.
55+ 3. start_dim and end_dim must be integers within the valid input rank.
56+ 4. If start_dim >= end_dim after normalization, the output layout is the
57+ input layout.
58+ 5. Otherwise, dimensions from start_dim through end_dim are merged using
59+ reshape-compatible sharding rules.
60+ 
61+ Args:
62+ cache_values (list): [input_layout, start_dim, end_dim, input_shape].
63+ 
64+ Returns:
65+ tuple: ((output_layout,), None)
66+ 
67+ Raises:
68+ ValueError: If cache_values are invalid, dimensions are out of range,
69+ or the flatten would change sharded slices incompatibly.
70+ """
71+ input_layout, start_dim, end_dim, input_shape = (
72+ cache_values[0], cache_values[1], cache_values[2], cache_values[3]
73+ )
74+ if input_layout is None:
41 raise ValueError(75 raise ValueError(
42- f"Operation {self.op_name}: flatten requires a valid input tensor layout."76+ f"For {self.op_name}, flatten requires a valid input tensor layout."
77+ )
78+ if not isinstance(input_shape, (list, tuple)):
79+ raise ValueError(
80+ f"For {self.op_name}, input_shape should be list or tuple, "
81+ f"but got {type(input_shape)}."
82+ )
83+ if len(input_shape) != len(input_layout.tensor_map):
84+ raise ValueError(
85+ f"For {self.op_name}, input shape rank should match layout rank, "
86+ f"but got {len(input_shape)} and {len(input_layout.tensor_map)}."
87+ )
88+ if not isinstance(start_dim, int) or not isinstance(end_dim, int):
89+ raise ValueError(
90+ f"For {self.op_name}, start_dim and end_dim should be int, "
91+ f"but got {type(start_dim)} and {type(end_dim)}."
43 )92 )
44- 
45- input_layout = layouts[0]
46- # WithShape suffix appends a list of input_shapes as the last item in extra_args
47- input_shapes = extra_args[-1]
48- input_shape = input_shapes[0]
49- 
50- # PyTorch flatten defaults: start_dim=0, end_dim=-1
51- start_dim = 0
52- end_dim = -1
53- 
54- # Parse scalar args (start_dim, end_dim) from extra_args
55- num_scalar_args = len(extra_args) - 1
56- if num_scalar_args > 0:
57- start_dim = extra_args[0]
58- if num_scalar_args > 1:
59- end_dim = extra_args[1]
60 93 
61 ndim = len(input_shape)94 ndim = len(input_shape)
62 95 
63- # Handle 0-D tensor case specifically to avoid parent class indexing errors
64 if ndim == 0:96 if ndim == 0:
65- out_layout = layouts[0].__class__.from_device_mesh(input_layout.mesh)97+ out_layout = input_layout.__class__.from_device_mesh(input_layout.mesh)
66 out_layout.set_placements(input_layout.placements)98 out_layout.set_placements(input_layout.placements)
67- out_layout.placement_to_tensor_map(1) # Flattened 0-D becomes 1-D of shape (1,)99+ out_layout.placement_to_tensor_map(1)
68- return out_layout100+ return ((out_layout,), None)
69 101 
70- # Handle negative dimensions
71 if start_dim < 0:102 if start_dim < 0:
72 start_dim += ndim103 start_dim += ndim
73 if end_dim < 0:104 if end_dim < 0:
74 end_dim += ndim105 end_dim += ndim
75 106 
76- # Validate dimension bounds
77 if start_dim < 0 or start_dim >= ndim or end_dim < 0 or end_dim >= ndim:107 if start_dim < 0 or start_dim >= ndim or end_dim < 0 or end_dim >= ndim:
78- raise ValueError(f"Dimension out of range for flatten: start_dim={start_dim}, end_dim={end_dim}")108+ raise ValueError(
109+ f"For {self.op_name}, dimension out of range "
110+ f"(start_dim={start_dim}, end_dim={end_dim}, ndim={ndim})."
111+ )
79 112 
80- 
81- # If start_dim > end_dim, PyTorch returns the tensor unchanged (identity operation).
82- # This also covers the case where start_dim == end_dim (e.g., flatten(1, 1))
83- # because the loop `range(start_dim, end_dim + 1)` would still execute, but
84- # effectively for a single dimension, resulting in `dst_shape == input_shape`.
85- # However, the underlying `_merge_unshared_axis` logic in ReshapeDistributedOp
86- # is not designed for an identity mapping and introduces inconsistencies.
87- # Thus, for an identity flatten, we should simply return the original layout.
88 if start_dim >= end_dim:113 if start_dim >= end_dim:
89- # If flattening a single dimension or a range that results in no actual flattening,114+ return ((input_layout,), None)
90- # the layout should remain unchanged.
91- return input_layout # Directly return the input layout
92 115 
93- 
94- # Calculate the target flattened shape
95 flattened_size = 1116 flattened_size = 1
96 for i in range(start_dim, end_dim + 1):117 for i in range(start_dim, end_dim + 1):
97 flattened_size *= input_shape[i]118 flattened_size *= input_shape[i]
98 dst_shape = list(input_shape[:start_dim]) + [flattened_size] + list(input_shape[end_dim + 1:])119 dst_shape = list(input_shape[:start_dim]) + [flattened_size] + list(input_shape[end_dim + 1:])
99 120 
100- # Prepare args for ReshapeDistributedOp.infer_layout121+ out_layout, _ = self._infer_reshape_layout(input_layout, dst_shape, input_shape)
101- # Since self.op_name is 'flatten' (not in ["reshape", "view"]),122+ return ((out_layout,), None)
102- # the parent class will treat this as the MindSpore Reshape branch,
103- # which expects: extra_args = [dst_shape, input_shape]
104- mock_extra_args = [dst_shape, input_shape]
105- 
106- # Reuse parent class logic to safely resolve Shard tensor mapping merges
107- out_layout, _ = super().infer_layout(layouts, mock_extra_args)
108- 
109- # Returning purely the Layout object, which is what the WithShape suffix expects
110- return out_layout
@@ -386,6 +386,11 @@ class GatherNdDistributedOp(DistributedOp):
386 """386 """
387 Preprocess arguments for GatherNd operator.387 Preprocess arguments for GatherNd operator.
388 388 
389+ NOTE: aclop packed-args normalization (for MindSpore aclop operators
390+ that pack args as ``(prim, name, (real_args...))``) is handled
391+ upstream in ``OpDispatcher._dispatch_layout_infer`` via
392+ ``_normalize_aclop_args``. This method receives clean unpacked args.
393+ 
389 Args:394 Args:
390 args (tuple): Input arguments (input, indices).395 args (tuple): Input arguments (input, indices).
391 kwargs (dict): Keyword arguments.396 kwargs (dict): Keyword arguments.
@@ -393,19 +398,11 @@ class GatherNdDistributedOp(DistributedOp):
393 Returns:398 Returns:
394 tuple: (local_args, local_kwargs, cache_values)399 tuple: (local_args, local_kwargs, cache_values)
395 """400 """
396- packed_call = None
397- if len(args) == 3 and isinstance(args[1], str) and isinstance(args[2], (tuple, list)):
398- packed_call = (args[0], args[1])
399- args = tuple(args[2])
400- 
401 args, _ = _normalize_gathernd_args(*args, **kwargs)401 args, _ = _normalize_gathernd_args(*args, **kwargs)
402 input_tensor, indices = args[0], args[1]402 input_tensor, indices = args[0], args[1]
403 local_input = input_tensor.to_local() if hasattr(input_tensor, "_layout") else input_tensor403 local_input = input_tensor.to_local() if hasattr(input_tensor, "_layout") else input_tensor
404 local_indices = indices.to_local()404 local_indices = indices.to_local()
405- if packed_call is not None:405+ local_args = (local_input, local_indices)
406- local_args = (packed_call[0], packed_call[1], (local_input, local_indices))
407- else:
408- local_args = (local_input, local_indices)
409 local_kwargs = {}406 local_kwargs = {}
410 cache_values = [407 cache_values = [
411 input_tensor.layout if hasattr(input_tensor, "_layout") else None,408 input_tensor.layout if hasattr(input_tensor, "_layout") else None,
@@ -1,86 +0,0 @@
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-"""
16-Distributed implementation for Multinomial operator.
17-"""
18- 
19-from hyper_parallel.core.dtensor.layout import Layout
20-from .parallel_ops import DistributedOp
21- 
22- 
23-class MultinomialDistributedOp(DistributedOp):
24- """Distributed implementation for Multinomial operator."""
25- 
26- def infer_layout(self, layouts, extra_args=None):
27- """
28- Infer output layout for Multinomial operator.
29- 
30- Args:
31- layouts (tuple): Layouts of input tensor.
32- extra_args (tuple): Arguments for the operator (num_samples, replacement, generator).
33- Note: logic assumes num_samples affects shape but not the sharding map pattern.
34- 
35- Returns:
36- Layout: Layout for output tensor.
37- """
38- # Check partial inputs
39- if not self._allow_partial_inputs:
40- self._check_partial_inputs(layouts)
41- layout = layouts[0]
42- in_tensor_map = layout.alias_tensor_map
43- ndim = len(in_tensor_map)
44- 
45- # PyTorch multinomial supports 1D or 2D tensors
46- if ndim not in (1, 2):
47- raise ValueError(f"For 'multinomial', input dimension must be 1 or 2, but got {ndim}.")
48- 
49- # The last dimension (probability dimension) must NOT be sharded.
50- # Multinomial sampling requires the full probability distribution (cumulative sum, normalization)
51- # to be present on the device. If it's sharded, we cannot sample correctly without communication.
52- # In alias_tensor_map, "None" indicates the dimension is NOT sharded.
53- prob_dim_map = in_tensor_map[-1]
54- 
55- # Check if the last dimension is sharded (i.e., not "None")
56- if prob_dim_map != "None":
57- raise ValueError(
58- f"For 'multinomial', the last dimension (probability category dimension) "
59- f"must not be sharded (Replicated). Got layout map: {in_tensor_map}. "
60- f"Please redistribute the tensor to replicate the last dimension before calling multinomial."
61- )
62- out_tensor_map = ()
63- if ndim == 1:
64- # Input: (C,) -> Probability distribution
65- # Output: (num_samples,) -> Sampled indices
66- # Since the input distribution C is replicated (checked above), the samples generated
67- # from it have no spatial correlation to shards, so the output is also Replicated ("None").
68- out_tensor_map = ("None",)
69- else:
70- # Input: (N, C) -> Batch of distributions
71- # Output: (N, num_samples) -> Batch of sampled indices
72- #
73- # Rule:
74- # 1. Preserve sharding on the Batch dimension (dim 0). If input is data parallel, output is too.
75- # 2. The new dimension (num_samples) is created locally and is not sharded ("None").
76- batch_dim_map = in_tensor_map[0]
77- out_tensor_map = (batch_dim_map, "None")
78- 
79- output_layout = Layout(
80- mesh_shape=layout.mesh_shape,
81- alias_name=layout.alias_name,
82- rank_list=layout.rank_list
83- )
84- 
85- # Create output layout using the inferred tensor map aliases
86- return output_layout(*out_tensor_map)
@@ -16,79 +16,166 @@
16"""16"""
17Distributed implementation for RepeatInterleave operator.17Distributed implementation for RepeatInterleave operator.
18"""18"""
19+import copy
20+from typing import Tuple
21+ 
19from hyper_parallel.core.dtensor.layout import Layout22from hyper_parallel.core.dtensor.layout import Layout
20from .parallel_ops import DistributedOp23from .parallel_ops import DistributedOp
21 24 
22 25 
26+def _normalize_repeat_interleave_args(x, repeats, dim=None, *, output_size=None):
27+ """Normalize positional and keyword arguments into a canonical form.
28+ 
29+ Args:
30+ x: Input tensor.
31+ repeats: Number of repetitions (int or Tensor).
32+ dim: Dimension along which to repeat values. None means flatten first.
33+ output_size: Total output size (keyword-only in torch).
34+ 
35+ Returns:
36+ tuple: (positional_args_tuple, kwargs_dict)
37+ """
38+ kwargs = {}
39+ if output_size is not None:
40+ kwargs['output_size'] = output_size
41+ return (x, repeats, dim), kwargs
42+ 
43+ 
23class RepeatInterleaveDistributedOp(DistributedOp):44class RepeatInterleaveDistributedOp(DistributedOp):
24- """Distributed implementation for torch.repeat_interleave."""45+ """Distributed implementation for torch.repeat_interleave.
25 46 
26- def infer_layout(self, layouts, extra_args=None):47+ Sharding constraints:
27- """48+ - When dim is specified: the repeat dimension must be replicated.
28- Infer output layout for RepeatInterleave operator.49+ - When dim is None (flatten mode): only the first dimension may be sharded.
29 50 
30- RepeatInterleave: output = repeat_interleave(input, repeats, dim)51+ Output layout:
31- 52+ - When dim is specified: same as input layout.
32- Rules:53+ - When dim is None: 1-D layout preserving sharding on dim 0.
33- 1. dim = None if not specified.54+ """
34- 2. The dimension `dim` MUST be unsharded to ensure global repeat_interleave correctness.55+ 
35- 3. Output layout usually same as input, but shape changes.56+ @staticmethod
57+ def _validate_input_layouts(input_layout, dim, op_name: str) -> None:
58+ """Validate sharding constraints for repeat_interleave.
36 59 
37 Args:60 Args:
38- layouts (tuple): Layouts of inputs. Expected:61+ input_layout: Layout of the input tensor.
39- layouts[0] (Layout): Input tensor layout (required).62+ dim: The repeat dimension, or None for flatten mode.
40- extra_args (tuple, optional): Contains repeats and dim. Expected:63+ op_name: Operator name used in error messages.
41- extra_args[0] (int or Tensor): Number of repeats or Tensor
42- extra_args[1] (int, optional): Dimension to repeat. Defaults to -1.
43 64 
44- Returns:65+ Raises:
45- tuple: Layouts for values.66+ ValueError: If dim is out of range or the repeat dimension is sharded.
46 """67 """
47- if not layouts or layouts[0] is None:68+ in_tensor_map = input_layout.alias_tensor_map
48- raise ValueError("repeat_interleave requires a valid input tensor layout.")69+ ndim = len(in_tensor_map)
49 70 
50- input_layout = layouts[0]71+ if dim is not None:
51- in_tensor_map = input_layout.tensor_map72+ actual_dim = dim if dim >= 0 else ndim + dim
52- dim = None # The dimension along which to repeat values.73+ if not 0 <= actual_dim < ndim:
53- # By default, use the flattened input array, and return a flat output array.
54- if len(extra_args) >= 2 and extra_args[1] is not None:
55- dim = extra_args[1]
56- if dim is None:
57- sharded_dims = [i for i, shard in enumerate(in_tensor_map) if shard != -1]
58- if not sharded_dims: # not shard
59- output_tensor_map = [-1]
60- # Only can shard on the first dimension.
61- elif sharded_dims == [0] and in_tensor_map[0] != -1:
62- output_tensor_map = [in_tensor_map[0]]
63- else:
64- # Other dims must NOT be sharded.
65 raise ValueError(74 raise ValueError(
66- f"Operation {self.op_name}: Cannot flatten tensor when dim=None."75+ f"For {op_name}, dimension should be in [0, {ndim}), but got {dim}."
76+ )
77+ mapping = in_tensor_map[actual_dim]
78+ if isinstance(mapping, (list, tuple)):
79+ is_sharded = any(axis != "None" for axis in mapping)
80+ else:
81+ is_sharded = mapping != "None"
82+ if is_sharded:
83+ raise ValueError(
84+ f"For {op_name}, the repeat dimension should be replicated, "
85+ f"but got dim={dim} mapped to {mapping}."
67 )86 )
68 87 
69- def idx_to_alias(idx, aliases):88+ def preprocess(self, args: tuple, kwargs: dict) -> tuple:
70- if idx == -1:89+ """Preprocess arguments for RepeatInterleave operator.
71- return "None"90+ 
72- return aliases[len(aliases) - idx - 1]91+ Extracts local tensors from DTensor inputs and builds cache_values
73- output_map = tuple(idx_to_alias(idx, input_layout.alias_name) for idx in output_tensor_map)92+ containing only the information needed for layout inference.
93+ 
94+ Args:
95+ args: Positional arguments (input, repeats, dim).
96+ kwargs: Keyword arguments (output_size).
97+ 
98+ Returns:
99+ tuple: (local_args, local_kwargs, cache_values)
100+ """
101+ args, kwargs = _normalize_repeat_interleave_args(*args, **kwargs)
102+ input_tensor = args[0]
103+ repeats = args[1]
104+ dim = args[2]
105+ output_size = kwargs.get('output_size', None)
106+ 
107+ local_input = input_tensor.to_local()
108+ 
109+ # repeats can be int or Tensor; handle DTensor defensively
110+ if hasattr(repeats, 'to_local'):
111+ local_repeats = repeats.to_local()
112+ else:
113+ local_repeats = repeats
114+ 
115+ local_args = (local_input, local_repeats, dim)
116+ local_kwargs = {}
117+ if output_size is not None:
118+ local_kwargs['output_size'] = output_size
119+ 
120+ cache_values = [input_tensor.layout, dim]
121+ return local_args, local_kwargs, cache_values
122+ 
123+ def infer_layout(self, cache_values: list) -> Tuple[tuple, None]:
124+ """Infer output layout for RepeatInterleave operator.
125+ 
126+ Rules:
127+ 1. Input must not have Partial status.
128+ 2. When dim is specified: the repeat dimension must be replicated.
129+ Output layout is a deep copy of the input layout.
130+ 3. When dim is None (flatten mode): only dim 0 may be sharded.
131+ Output is a 1-D layout preserving the mesh axis on dim 0.
132+ 
133+ Args:
134+ cache_values: [input_layout, dim] where dim is the repeat dimension
135+ or None for flatten mode.
136+ 
137+ Returns:
138+ tuple: ((output_layout,), None)
139+ 
140+ Raises:
141+ ValueError: If input has Partial status, dim is out of range,
142+ the repeat dimension is sharded, or flatten is attempted on
143+ a tensor sharded on a non-first dimension.
144+ """
145+ input_layout = cache_values[0]
146+ dim = cache_values[1]
147+ 
148+ self._check_partial_inputs([input_layout])
149+ self._validate_input_layouts(input_layout, dim, self.op_name)
150+ 
151+ in_tensor_map = input_layout.alias_tensor_map
152+ 
153+ if dim is None:
154+ # Flatten mode: output is 1-D.
155+ sharded_dims = []
156+ for i, shard in enumerate(in_tensor_map):
157+ if isinstance(shard, (list, tuple)):
158+ is_sharded = any(axis != "None" for axis in shard)
159+ else:
160+ is_sharded = shard != "None"
161+ if is_sharded:
162+ sharded_dims.append(i)
163+ if not sharded_dims:
164+ output_tensor_map = ("None",)
165+ elif sharded_dims == [0]:
166+ output_tensor_map = (in_tensor_map[0],)
167+ else:
168+ raise ValueError(
169+ f"For {self.op_name}, sharded dims in flatten mode should be [] or [0], "
170+ f"but got {sharded_dims}."
171+ )
74 172 
75 output_layout = Layout(173 output_layout = Layout(
76 mesh_shape=input_layout.mesh_shape,174 mesh_shape=input_layout.mesh_shape,
77 alias_name=input_layout.alias_name,175 alias_name=input_layout.alias_name,
78 rank_list=input_layout.rank_list176 rank_list=input_layout.rank_list
79 )177 )
178+ return (output_layout(*output_tensor_map),), None
80 179 
81- return output_layout(*output_map)180+ # dim specified: output layout = deep copy of input layout
82- input_dim = len(in_tensor_map)181+ return (copy.deepcopy(input_layout),), None
83- if dim < 0:
84- dim = input_dim + dim
85- # Check if dimension is within valid range
86- if not 0 <= dim < input_dim:
87- raise ValueError(f"Dimension out of range (expected to be in [0, {input_dim}), but got {dim}).")
88- # The chosen dim must NOT be sharded
89- if in_tensor_map[dim] != -1:
90- raise ValueError(
91- f"Operation {self.op_name}: Cannot perform sharding on params along the chosen dim"
92- )
93- # Output layout same as input layout (shape change does not affect sharding pattern)
94- return input_layout
@@ -16,6 +16,8 @@
16Distributed implementation for Reshape operator.16Distributed implementation for Reshape operator.
17"""17"""
18 18 
19+from typing import Callable, Optional, Tuple
20+ 
19from hyper_parallel.core.dtensor.layout import Layout21from hyper_parallel.core.dtensor.layout import Layout
20from hyper_parallel.platform import get_platform22from hyper_parallel.platform import get_platform
21from .parallel_ops import DistributedOp23from .parallel_ops import DistributedOp
@@ -23,6 +25,21 @@ platform = get_platform()
23Tensor = platform.Tensor25Tensor = platform.Tensor
24 26 
25 27 
28+def _normalize_reshape_args(x, *shape, **kwargs):
29+ """Normalize reshape/view arguments into positional args and empty kwargs."""
30+ unexpected_kwargs = set(kwargs) - {'shape'}
31+ if unexpected_kwargs:
32+ unexpected = next(iter(unexpected_kwargs))
33+ raise TypeError(f"reshape got an unexpected keyword argument '{unexpected}'.")
34+ if shape and 'shape' in kwargs:
35+ raise TypeError("reshape got shape from both args and kwargs.")
36+ if not shape and 'shape' in kwargs:
37+ shape = (kwargs['shape'],)
38+ if not shape:
39+ raise TypeError("reshape missing required shape argument.")
40+ return (x,) + shape, {}
41+ 
42+ 
26def _filter_none_split_tensor_map(tensor_map, mesh_shape):43def _filter_none_split_tensor_map(tensor_map, mesh_shape):
27 """44 """
28 Filter out the elements in tensor_map where the size of the corresponding dimension in device_matrix is 1.45 Filter out the elements in tensor_map where the size of the corresponding dimension in device_matrix is 1.
@@ -152,47 +169,6 @@ class ReshapeDistributedOp(DistributedOp):
152 local_dst_shape.append(dst_shape[idx] // x_mesh_shape[-1 - map_id] if dst_shape[idx] > 0 else -1)169 local_dst_shape.append(dst_shape[idx] // x_mesh_shape[-1 - map_id] if dst_shape[idx] > 0 else -1)
153 return output_map, local_dst_shape170 return output_map, local_dst_shape
154 171 
155- def _parse_shape_args(self, extra_args):
156- """Parse shape arguments from extra_args.
157- 
158- Args:
159- extra_args: Extra arguments containing shape info
160- 
161- Returns:
162- tuple: (dst_shape, input_shape)
163- """
164- if self.op_name in ["reshape", "view"]:
165- return self._parse_torch_shape_args(extra_args)
166- return self._parse_mindspore_shape_args(extra_args)
167- 
168- def _parse_torch_shape_args(self, extra_args):
169- """Parse PyTorch style shape arguments."""
170- if len(extra_args) < 2:
171- raise ValueError(f"{self.op_name} requires output shape and input shape.")
172- 
173- input_shape = extra_args[-1]
174- shape_args = extra_args[:-1]
175- 
176- if len(shape_args) == 1:
177- first_arg = shape_args[0]
178- if isinstance(first_arg, (list, tuple)):
179- dst_shape = first_arg
180- elif isinstance(first_arg, Tensor):
181- dst_shape = first_arg.tolist()
182- else:
183- dst_shape = shape_args
184- else:
185- dst_shape = shape_args
186- 
187- return dst_shape, input_shape
188- 
189- def _parse_mindspore_shape_args(self, extra_args):
190- """Parse MindSpore style shape arguments."""
191- if len(extra_args) != 2:
192- raise ValueError("Reshape requires output shape and input shape.")
193- 
194- return extra_args[0], extra_args[1]
195- 
196 def _normalize_shape(self, dst_shape):172 def _normalize_shape(self, dst_shape):
197 """Normalize dst_shape to list format."""173 """Normalize dst_shape to list format."""
198 if isinstance(dst_shape, Tensor):174 if isinstance(dst_shape, Tensor):
@@ -265,25 +241,29 @@ class ReshapeDistributedOp(DistributedOp):
265 if partial_op is not None and i < len(out_layout.alias_name):241 if partial_op is not None and i < len(out_layout.alias_name):
266 out_layout.set_partial_by_dev_axis(out_layout.alias_name[i], partial_op)242 out_layout.set_partial_by_dev_axis(out_layout.alias_name[i], partial_op)
267 243 
268- def infer_layout(self, layouts, extra_args=None):244+ def preprocess(self, args: tuple, kwargs: dict) -> tuple:
269 """245 """
270- Infer output layout for reshape operator.246+ Preprocess arguments for Reshape operator.
271- 
272- For reshape operations, data slice on each device after reshape should be same as data slice before reshape.
273 247 
274 Args:248 Args:
275- layouts (Layout): Layout of input x249+ args (tuple): Input tensor followed by target shape arguments.
276- extra_args:250+ kwargs (dict): Keyword arguments.
277- For MindSpore Reshape: (destination shape, original shape)
278- For PyTorch reshape/view: (shape_arg1, shape_arg2, ..., original shape) or (shape_tuple, original shape)
279 251 
280 Returns:252 Returns:
281- tuple: Layout for output tensor253+ tuple: (local_args, local_kwargs, cache_values)
282 """254 """
283- x_layout = layouts[0]255+ args, _ = _normalize_reshape_args(*args, **kwargs)
284- x_dict = x_layout.to_dict()256+ input_tensor = args[0]
257+ dst_shape = args[1:] if len(args) > 2 else args[1]
285 258 
286- dst_shape, input_shape = self._parse_shape_args(extra_args)259+ local_args = (input_tensor.to_local(), dst_shape)
260+ local_kwargs = {}
261+ cache_values = [input_tensor.layout, dst_shape, tuple(input_tensor.shape)]
262+ return local_args, local_kwargs, cache_values
263+ 
264+ def _infer_reshape_layout(self, x_layout, dst_shape, input_shape):
265+ """Infer reshape output layout and local destination shape."""
266+ x_dict = x_layout.to_dict()
287 dst_shape = self._normalize_shape(dst_shape)267 dst_shape = self._normalize_shape(dst_shape)
288 268 
289 x_map = _filter_none_split_tensor_map(x_dict["tensor_map"], x_dict["mesh_shape"])269 x_map = _filter_none_split_tensor_map(x_dict["tensor_map"], x_dict["mesh_shape"])
@@ -307,3 +287,53 @@ class ReshapeDistributedOp(DistributedOp):
307 self._apply_partial_status(x_layout, out_layout)287 self._apply_partial_status(x_layout, out_layout)
308 288 
309 return out_layout, local_dst_shape289 return out_layout, local_dst_shape
290+ 
291+ def infer_layout(self, cache_values: list) -> Tuple[tuple, list]:
292+ """
293+ Infer output layout for Reshape operator.
294+ 
295+ Rules:
296+ 1. Partial input is allowed and preserved on the output layout.
297+ 2. Target shape must be a Tensor, tuple, or list.
298+ 3. Input and output total element counts must match after resolving one dynamic axis.
299+ 4. Reshape must preserve each device's local data slice; sharded axes can only be
300+ split or merged when the shard boundary remains valid.
301+ 5. Output Partial status follows the input Partial status.
302+ 
303+ Args:
304+ cache_values (list): [input_layout, dst_shape, input_shape].
305+ 
306+ Returns:
307+ tuple: ((output_layout,), local_dst_shape)
308+ 
309+ Raises:
310+ ValueError: If target shape is invalid or the reshape would change sharded slices.
311+ """
312+ if len(cache_values) != 3:
313+ raise ValueError(
314+ f"For {self.op_name}, cache_values length should be 3, but got {len(cache_values)}"
315+ )
316+ 
317+ x_layout, dst_shape, input_shape = cache_values[0], cache_values[1], cache_values[2]
318+ if x_layout is None:
319+ raise ValueError(f"For {self.op_name}, reshape requires a valid input tensor layout.")
320+ 
321+ out_layout, local_dst_shape = self._infer_reshape_layout(x_layout, dst_shape, input_shape)
322+ return ((out_layout,), local_dst_shape)
323+ 
324+ def get_expand_impl(self, func: Optional[Callable], infer_result: tuple,
325+ cache_values: list) -> Optional[Callable]:
326+ """Return a closure that calls reshape/view with the inferred local target shape."""
327+ del cache_values
328+ if func is None:
329+ return None
330+ 
331+ local_dst_shape = infer_result[1]
332+ if local_dst_shape is None:
333+ return None
334+ 
335+ def expand_impl(x: object, shape: object) -> object:
336+ del shape
337+ return func(x, local_dst_shape)
338+ 
339+ return expand_impl
@@ -16,29 +16,51 @@
16Distributed implementation for Slice operator.16Distributed implementation for Slice operator.
17"""17"""
18# pylint: disable=E040218# pylint: disable=E0402
19+from typing import Callable, Optional, Tuple
20+ 
19from .parallel_ops import DistributedOp21from .parallel_ops import DistributedOp
20 22 
21 23 
24+def _normalize_slice_args(x, begin, end):
25+ return (x, begin, end), {}
26+ 
27+ 
22class SliceDistributedOp(DistributedOp):28class SliceDistributedOp(DistributedOp):
23- """Distributed implementation for MatMul operator."""29+ """Distributed implementation for Slice operator."""
30+ 
31+ def preprocess(self, args: tuple, kwargs: dict) -> tuple:
32+ """
33+ Preprocess arguments for Slice operator.
34+ 
35+ Args:
36+ args (tuple): Input arguments containing x, begin and end.
37+ kwargs (dict): Keyword arguments.
38+ 
39+ Returns:
40+ tuple: (local_args, local_kwargs, cache_values)
41+ """
42+ args, _ = _normalize_slice_args(*args, **kwargs)
43+ input_tensor, begin, end = args
44+ local_args = (input_tensor.to_local(), begin, end)
45+ local_kwargs = {}
46+ cache_values = [input_tensor.layout, begin, end, input_tensor.shape]
47+ return local_args, local_kwargs, cache_values
24 48 
25 def _is_shard_dim(self, layout):49 def _is_shard_dim(self, layout):
26 """return the shard num in each dim"""50 """return the shard num in each dim"""
27 shard_dim = []51 shard_dim = []
28- dev_mat = layout.mesh_shape52+ for axis_name in layout.alias_tensor_map:
29- tensor_map = layout.tensor_map53+ if axis_name == "None":
30- for dev_idx in tensor_map:54+ shard_dim.append(1)
31- if isinstance(dev_idx, (tuple, list)):55+ continue
56+ if isinstance(axis_name, (tuple, list)):
32 shard_num = 157 shard_num = 1
33- for idx in dev_idx:58+ for axis in axis_name:
34- if idx != -1 and dev_mat[len(dev_mat) - idx - 1] != 1:59+ if axis != "None":
35- shard_num *= dev_mat[len(dev_mat) - idx - 1]60+ shard_num *= layout.mesh.get_device_num_along_axis(axis)
36 shard_dim.append(shard_num)61 shard_dim.append(shard_num)
37- else:62+ continue
38- if dev_idx != -1 and dev_mat[len(dev_mat) - dev_idx - 1] != 1:63+ shard_dim.append(layout.mesh.get_device_num_along_axis(axis_name))
39- shard_dim.append(dev_mat[len(dev_mat) - dev_idx - 1])
40- else:
41- shard_dim.append(1)
42 return shard_dim64 return shard_dim
43 65 
44 def _check_layout(self, layout, begin, end, shape):66 def _check_layout(self, layout, begin, end, shape):
@@ -54,23 +76,69 @@ class SliceDistributedOp(DistributedOp):
54 f"the begin is {begin}, the end is {end}, the shape is {shape}, layout is {layout.to_dict()}")76 f"the begin is {begin}, the end is {end}, the shape is {shape}, layout is {layout.to_dict()}")
55 return shard_dim77 return shard_dim
56 78 
57- def infer_layout(self, layouts, extra_args=None):79+ def infer_layout(self, cache_values: list) -> Tuple[tuple, tuple]:
58 """80 """
59- Infer output layout for slice operator. The shard dim must be fully fetched.81+ Infer output layout for Slice operator.
82+ 
83+ Rules:
84+ 1. Input must not have Partial status.
85+ 2. begin, end and global_shape must have the same rank as input layout.
86+ 3. Any sharded dimension must be fully fetched.
87+ 4. Output layout is identical to the input layout.
60 88 
61 Args:89 Args:
62- layouts (Layout): Layout of input x90+ cache_values (list): [input_layout, begin, end, global_shape].
63- extra_args: (begin, end, global shape)
64 91 
65 Returns:92 Returns:
66- layout (Layout): the out layout93+ tuple: ((output_layout,), (new_begin, new_end)).
67- new_begin (tuple): begin after modification94+ 
68- new_end (tuple): end after modification95+ Raises:
96+ ValueError: If input has Partial status, arguments rank mismatch, or a sharded
97+ dimension is not fully fetched.
69 """98 """
70- begin = extra_args[0]99+ layout, begin, end, global_shape = cache_values
71- end = extra_args[1]100+ self._check_partial_inputs([layout])
72- global_shape = extra_args[2]101+ 
73- shard_dim = self._check_layout(layouts, begin, end, global_shape)102+ if len(begin) != len(end) or len(begin) != len(global_shape):
103+ raise ValueError(
104+ f"For {self.op_name}, begin, end and global_shape must have the same length, "
105+ f"but got begin: {len(begin)}, end: {len(end)}, global_shape: {len(global_shape)}"
106+ )
107+ if len(begin) != len(layout.alias_tensor_map):
108+ raise ValueError(
109+ f"For {self.op_name}, slice arguments rank must match input layout rank, "
110+ f"but got args rank: {len(begin)} and layout rank: {len(layout.alias_tensor_map)}"
111+ )
112+ 
113+ shard_dim = self._check_layout((layout,), begin, end, global_shape)
74 new_begin = tuple(begin[i] // shard_dim[i] for i in range(len(begin)))114 new_begin = tuple(begin[i] // shard_dim[i] for i in range(len(begin)))
75 new_end = tuple(end[i] // shard_dim[i] for i in range(len(end)))115 new_end = tuple(end[i] // shard_dim[i] for i in range(len(end)))
76- return layouts[0], new_begin, new_end116+ return ((layout,), (new_begin, new_end))
117+ 
118+ def get_expand_impl(self, func: Optional[Callable], infer_result: tuple,
119+ cache_values: list) -> Optional[Callable]:
120+ """
121+ Return a custom Slice implementation when local begin/end need adjustment.
122+ 
123+ Args:
124+ func: Original operator callable.
125+ infer_result (tuple): ((output_layout,), (new_begin, new_end)) from infer_layout.
126+ cache_values (list): [input_layout, begin, end, global_shape].
127+ 
128+ Returns:
129+ callable | None: expand_impl closure when local slice bounds differ, else None.
130+ """
131+ if func is None:
132+ return None
133+ 
134+ begin = cache_values[1]
135+ end = cache_values[2]
136+ new_begin, new_end = infer_result[1]
137+ if begin == new_begin and end == new_end:
138+ return None
139+ 
140+ def expand_impl(input_tensor: object, *_unused_args: object) -> object:
141+ """Call Slice with local slice bounds."""
142+ return func(input_tensor, new_begin, new_end)
143+ 
144+ return expand_impl
@@ -13,77 +13,89 @@
13# limitations under the License.13# limitations under the License.
14# ============================================================================14# ============================================================================
15"""15"""
16-Distributed implementation for ExpandDims operator.16+Distributed implementation for Squeeze operator.
17"""17"""
18+from copy import deepcopy
19+from typing import Tuple
20+ 
18from hyper_parallel.core.dtensor.layout import Layout21from hyper_parallel.core.dtensor.layout import Layout
19from .parallel_ops import DistributedOp22from .parallel_ops import DistributedOp
20 23 
21 24 
25+def _normalize_squeeze_args(x, axis=None):
26+ return (x, axis), {}
27+ 
28+ 
22class SqueezeDistributedOp(DistributedOp):29class SqueezeDistributedOp(DistributedOp):
23 """Distributed implementation for Squeeze operator."""30 """Distributed implementation for Squeeze operator."""
24 31 
25- def infer_layout(self, layouts, extra_args=None):32+ def preprocess(self, args: tuple, kwargs: dict) -> tuple:
33+ args, _ = _normalize_squeeze_args(*args, **kwargs)
34+ input_tensor, axis = args[0], args[1]
35+ 
36+ if axis is None:
37+ local_args = (input_tensor.to_local(),)
38+ else:
39+ local_args = (input_tensor.to_local(), axis)
40+ 
41+ input_shape = getattr(input_tensor, "shape", None)
42+ cache_values = [input_tensor.layout, axis, input_shape]
43+ return local_args, {}, cache_values
44+ 
45+ def infer_layout(self, cache_values: list) -> Tuple[tuple, None]:
26 """46 """
27- Infer output layout for Squeeze.47+ Infer output layout for Squeeze operator.
48+ 
49+ Rules:
50+ 1. Input must not have Partial status.
51+ 2. input_shape rank must match input layout rank.
52+ 3. If axis is None, only size-1 replicated dimensions are removed.
53+ 4. If axis is specified, every axis must be in range, have size 1, and
54+ must not be sharded.
55+ 5. Output layout removes the squeezed dimensions and preserves the
56+ remaining input dimension mappings.
28 57 
29 Args:58 Args:
30- layouts (tuple): Tuple containing input layout.59+ cache_values (list): [input_layout, axis, input_shape]
31- extra_args: Extra arguments containing axis and input_shapes.
32- Can be dict or list/tuple where last element is input_shapes.
33 60 
34 Returns:61 Returns:
35- Layout: Output layout with squeezed dimensions removed.62+ tuple: ((output_layout,), None)
63+ 
64+ Raises:
65+ ValueError: If input has Partial status, input_shape is missing, axis
66+ is invalid, or a requested squeeze dimension is sharded.
36 """67 """
37- if not layouts:68+ if not cache_values:
38 raise ValueError(69 raise ValueError(
39- f"For {self.op_name}, layouts should contain at least one input layout, "70+ f"For {self.op_name}, cache_values should contain input layout, "
40- f"but got empty layouts."71+ f"but got empty cache_values."
41 )72 )
42 73 
43- x_layout = layouts[0]74+ x_layout = cache_values[0]
75+ if not self._allow_partial_inputs:
76+ self._check_partial_inputs([x_layout])
77+ 
44 if x_layout.mesh_shape is None:78 if x_layout.mesh_shape is None:
45 raise ValueError(79 raise ValueError(
46 f"For {self.op_name}, input layout mesh_shape should not be None, "80 f"For {self.op_name}, input layout mesh_shape should not be None, "
47 f"but got None."81 f"but got None."
48 )82 )
49 83 
50- axis, input_shape = self._extract_args(extra_args)84+ axis = cache_values[1] if len(cache_values) > 1 else None
85+ input_shape = cache_values[2] if len(cache_values) > 2 else None
51 if input_shape is None:86 if input_shape is None:
52 raise ValueError(87 raise ValueError(
53- f"For {self.op_name}, input_shapes should be provided in extra_args, "88+ f"For {self.op_name}, input_shape should be provided in cache_values, "
54 f"but got None."89 f"but got None."
55 )90 )
56- 91+ if not isinstance(input_shape, (list, tuple)):
57- return self._compute_squeeze_layout(x_layout, axis, input_shape)
58- 
59- def _extract_args(self, extra_args):
60- """Extract axis and input_shape from extra_args."""
61- if isinstance(extra_args, dict):
62- input_shapes = extra_args.get("input_shapes", None)
63- axis = extra_args.get("axis", None)
64- elif isinstance(extra_args, (list, tuple)) and extra_args:
65- # Last element is input_shapes
66- input_shapes = extra_args[-1]
67- if not isinstance(input_shapes, (list, tuple)):
68- raise ValueError(
69- f"For {self.op_name}, input_shapes should be list or tuple, "
70- f"but got {type(input_shapes)}."
71- )
72- # First element is axis (if available)
73- axis = extra_args[0] if len(extra_args) > 1 else None
74- else:
75 raise ValueError(92 raise ValueError(
76- f"For {self.op_name}, extra_args should be dict or list/tuple, "93+ f"For {self.op_name}, input_shape should be list or tuple, "
77- f"but got {type(extra_args)}."94+ f"but got {type(input_shape)}."
78 )95 )
79 96 
80- # Get input shape (first element of input_shapes)97+ output_layout = self._compute_squeeze_layout(x_layout, axis, input_shape)
81- if input_shapes:98+ return ((output_layout,), None)
82- input_shape = input_shapes[0] if isinstance(input_shapes[0], (list, tuple)) else input_shapes
83- else:
84- input_shape = None
85- 
86- return axis, input_shape
87 99 
88 def _compute_squeeze_layout(self, x_layout, axis, input_shape):100 def _compute_squeeze_layout(self, x_layout, axis, input_shape):
89 """Compute the squeezed layout."""101 """Compute the squeezed layout."""
@@ -108,14 +120,7 @@ class SqueezeDistributedOp(DistributedOp):
108 f"but got {axis}."120 f"but got {axis}."
109 )121 )
110 122 
111- # Return scalar layout123+ return deepcopy(x_layout)
112- output_layout = Layout(
113- mesh_shape=x_layout.mesh_shape,
114- alias_name=x_layout.alias_name,
115- rank_list=x_layout.rank_list
116- )
117- output_layout = output_layout()
118- return output_layout
119 124 
120 def _validate_input_shape(self, x_layout, input_shape):125 def _validate_input_shape(self, x_layout, input_shape):
121 """Validate that input shape matches layout rank."""126 """Validate that input shape matches layout rank."""
@@ -150,6 +155,19 @@ class SqueezeDistributedOp(DistributedOp):
150 # Convert axis to list if it's a single integer155 # Convert axis to list if it's a single integer
151 if isinstance(axis, int):156 if isinstance(axis, int):
152 axis = [axis]157 axis = [axis]
158+ elif isinstance(axis, tuple):
159+ axis = list(axis)
160+ elif not isinstance(axis, list):
161+ raise ValueError(
162+ f"For {self.op_name}, axis should be int, list or tuple, "
163+ f"but got {type(axis)}."
164+ )
165+ 
166+ if not all(isinstance(ax, int) for ax in axis):
167+ raise ValueError(
168+ f"For {self.op_name}, every axis value should be int, "
169+ f"but got {axis}."
170+ )
153 171 
154 # Convert negative indices to positive172 # Convert negative indices to positive
155 axis = [ax if ax >= 0 else ax + in_rank for ax in axis]173 axis = [ax if ax >= 0 else ax + in_rank for ax in axis]
@@ -191,6 +209,9 @@ class SqueezeDistributedOp(DistributedOp):
191 209 
192 def _create_output_layout(self, x_layout, dims_to_squeeze):210 def _create_output_layout(self, x_layout, dims_to_squeeze):
193 """Create output layout after squeezing dimensions."""211 """Create output layout after squeezing dimensions."""
212+ if not dims_to_squeeze:
213+ return deepcopy(x_layout)
214+ 
194 # Get current alias tensor map215 # Get current alias tensor map
195 x_map = list(x_layout.alias_tensor_map)216 x_map = list(x_layout.alias_tensor_map)
196 217 
@@ -16,33 +16,76 @@
16Element-wise distributed operator implementation.16Element-wise distributed operator implementation.
17"""17"""
18 18 
19+import copy
20+from typing import Tuple
21+ 
19from .parallel_ops import DistributedOp22from .parallel_ops import DistributedOp
20 23 
21 24 
25+def _unwrap_local_value(value):
26+ """Convert DTensor-like values to local tensors while preserving scalar slots."""
27+ return value.to_local() if hasattr(value, "_layout") else value
28+ 
29+ 
22class TupleElementWiseDistributedOp(DistributedOp):30class TupleElementWiseDistributedOp(DistributedOp):
23 """31 """
24 Distributed implementation for tuple element-wise operators.32 Distributed implementation for tuple element-wise operators.
25 33 
26 Inherits from DistributedOp and provides element-wise specific implementations.34 Inherits from DistributedOp and provides element-wise specific implementations.
27 """35 """
28- def infer_layout(self, layouts, extra_args=None):36+ 
37+ def preprocess(self, args: tuple, kwargs: dict) -> tuple:
38+ """
39+ Preprocess arguments for tuple element-wise operators.
40+ 
41+ Args:
42+ args (tuple): Positional arguments passed to the operator.
43+ kwargs (dict): Keyword arguments passed to the operator.
44+ 
45+ Returns:
46+ tuple: (local_args, local_kwargs, cache_values)
47+ """
48+ expanded_args = []
49+ local_args = []
50+ for arg in args:
51+ if isinstance(arg, (tuple, list)):
52+ expanded_args.extend(arg)
53+ local_args.append(tuple(_unwrap_local_value(item) for item in arg))
54+ else:
55+ expanded_args.append(arg)
56+ local_args.append(_unwrap_local_value(arg))
57+ 
58+ local_kwargs = {key: _unwrap_local_value(value) for key, value in kwargs.items()}
59+ cache_values = [getattr(arg, "layout", None) for arg in expanded_args]
60+ cache_values.extend(getattr(value, "layout", None) for value in kwargs.values())
61+ 
62+ return tuple(local_args), local_kwargs, cache_values
63+ 
64+ def infer_layout(self, cache_values: list) -> Tuple[tuple, None]:
29 """65 """
30 Infer output layouts for element-wise operations.66 Infer output layouts for element-wise operations.
31 67 
32- For element-wise operations, all inputs should have the same layout,68+ Rules:
33- and the output will have the same layout.69+ 1. Inputs must not have Partial status.
70+ 2. Tuple/list positional arguments are inferred from their expanded elements.
71+ 3. Output layouts are identical to the expanded input layouts.
34 72 
35 Args:73 Args:
36- primitive: Primitive instance74+ cache_values (list): Expanded input layouts, using None for non-DTensor slots.
37- layouts: Layouts of input tensors
38 75 
39 Returns:76 Returns:
40- tuple: Layout for output tensor.77+ tuple: (output_layouts, None)
41 78 
42 Raises:79 Raises:
43- ValueError: If input layouts are not compatible.80+ ValueError: If input has Partial status.
44 """81 """
45- if not layouts:82+ if not cache_values:
46 return None83 return None
47 84 
48- return layouts85+ self._check_partial_inputs(cache_values)
86+ 
87+ output_layouts = tuple(
88+ copy.deepcopy(layout) if layout is not None else None
89+ for layout in cache_values
90+ )
91+ return output_layouts, None
@@ -1,4 +0,0 @@
1-multinomial:
2- dist_op_name: _torch_multinomial_dist_op
3- distributed_op_class: MultinomialDistributedOp
4- distributed_op_file: parallel_multinomial
@@ -15,10 +15,10 @@
15"""parallel_cumsum test"""15"""parallel_cumsum test"""
16import os16import os
17import unittest17import unittest
18-from unittest.mock import patch18+from unittest.mock import MagicMock, patch
19import numpy as np19import numpy as np
20 20 
21-from hyper_parallel.core.dtensor.dtensor import _build_layout21+from hyper_parallel.core.dtensor.dtensor import _build_layout, _LAYOUT_CACHE
22from hyper_parallel.core.dtensor.placement_types import Shard, Replicate22from hyper_parallel.core.dtensor.placement_types import Shard, Replicate
23from hyper_parallel.core.shard.ops.parallel_cumsum import CumsumDistributedOp23from hyper_parallel.core.shard.ops.parallel_cumsum import CumsumDistributedOp
24from hyper_parallel.core.dtensor.device_mesh import (24from hyper_parallel.core.dtensor.device_mesh import (
@@ -28,6 +28,7 @@ from hyper_parallel.core.dtensor.device_mesh import (
28from hyper_parallel.platform.platform import EXISTING_COMM_GROUPS28from hyper_parallel.platform.platform import EXISTING_COMM_GROUPS
29 29 
30op = CumsumDistributedOp("cumsum")30op = CumsumDistributedOp("cumsum")
31+op_ms = CumsumDistributedOp("CumsumExt")
31 32 
32 33 
33class TestParallelCumsum(unittest.TestCase):34class TestParallelCumsum(unittest.TestCase):
@@ -40,11 +41,13 @@ class TestParallelCumsum(unittest.TestCase):
40 """41 """
41 EXISTING_COMM_GROUPS.clear()42 EXISTING_COMM_GROUPS.clear()
42 _DEVICE_MESH_MAP.clear()43 _DEVICE_MESH_MAP.clear()
44+ _LAYOUT_CACHE.clear()
43 45 
44 def tearDown(self):46 def tearDown(self):
45 """Clean up after each test method."""47 """Clean up after each test method."""
46 EXISTING_COMM_GROUPS.clear()48 EXISTING_COMM_GROUPS.clear()
47 _DEVICE_MESH_MAP.clear()49 _DEVICE_MESH_MAP.clear()
50+ _LAYOUT_CACHE.clear()
48 51 
49 def _setup_mock_platform(self, mock_platform, platform_type=None, world_size=8):52 def _setup_mock_platform(self, mock_platform, platform_type=None, world_size=8):
50 """Configure common mock-platform attributes used across tests.53 """Configure common mock-platform attributes used across tests.
@@ -83,19 +86,23 @@ class TestParallelCumsum(unittest.TestCase):
83 x_placements = (Shard(0), Replicate())86 x_placements = (Shard(0), Replicate())
84 x_layout = _build_layout(mesh, x_placements, 2)87 x_layout = _build_layout(mesh, x_placements, 2)
85 88 
86- output_layout = op.infer_layout((x_layout,), extra_args=(-1,))89+ cache_values = [x_layout, -1]
90+ output_layouts, extra_info = op.infer_layout(cache_values)
91+ output_layout = output_layouts[0]
87 92 
88 expected_map = (1, -1)93 expected_map = (1, -1)
89 assert output_layout.tensor_map == expected_map, (94 assert output_layout.tensor_map == expected_map, (
90 f"Data parallel cumsum failed. Expected {expected_map}, "95 f"Data parallel cumsum failed. Expected {expected_map}, "
91 f"got {output_layout.tensor_map}"96 f"got {output_layout.tensor_map}"
92 )97 )
98+ assert output_layout is not x_layout, "Cumsum output layout should be a deep copy"
99+ assert extra_info is None, f"Cumsum extra_info should be None, got {extra_info}"
93 100
94 # Since `get_expand_impl` is not overridden, it returns None by default.101 # Since `get_expand_impl` is not overridden, it returns None by default.
95 # The same applies to other test classes, so it is unnecessary to test its return value.102 # The same applies to other test classes, so it is unnecessary to test its return value.
96- assert op.get_expand_impl(None, output_layout, (x_layout,), (-1,)) is None, (103+ assert op.get_expand_impl(None, (output_layouts, None), cache_values) is None, (
97 f"get_expand_impl test failed. Expected None, "104 f"get_expand_impl test failed. Expected None, "
98- f"got {op.get_expand_impl(None, output_layout, (x_layout,), (-1,))}"105+ f"got {op.get_expand_impl(None, (output_layouts, None), cache_values)}"
99 )106 )
100 107 
101 @patch("hyper_parallel.core.dtensor.device_mesh.platform")108 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
@@ -109,13 +116,16 @@ class TestParallelCumsum(unittest.TestCase):
109 x_placements = (Replicate(), Shard(1))116 x_placements = (Replicate(), Shard(1))
110 x_layout = _build_layout(mesh, x_placements, 2)117 x_layout = _build_layout(mesh, x_placements, 2)
111 118 
112- output_layout = op.infer_layout((x_layout,), extra_args=(0,))119+ cache_values = [x_layout, 0]
120+ output_layouts, extra_info = op.infer_layout(cache_values)
121+ output_layout = output_layouts[0]
113 122 
114 expected_map = (-1, 0)123 expected_map = (-1, 0)
115 assert output_layout.tensor_map == expected_map, (124 assert output_layout.tensor_map == expected_map, (
116 f"Tensor parallel cumsum failed. Expected {expected_map}, "125 f"Tensor parallel cumsum failed. Expected {expected_map}, "
117 f"got {output_layout.tensor_map}"126 f"got {output_layout.tensor_map}"
118 )127 )
128+ assert extra_info is None, f"Cumsum extra_info should be None, got {extra_info}"
119 129 
120 @patch("hyper_parallel.core.dtensor.device_mesh.platform")130 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
121 def test_cumsum_layout_mixed_parallel(self, mock_platform):131 def test_cumsum_layout_mixed_parallel(self, mock_platform):
@@ -128,13 +138,16 @@ class TestParallelCumsum(unittest.TestCase):
128 x_placements = (Shard(0), Replicate(), Shard(2))138 x_placements = (Shard(0), Replicate(), Shard(2))
129 x_layout = _build_layout(mesh, x_placements, 3)139 x_layout = _build_layout(mesh, x_placements, 3)
130 140 
131- output_layout = op.infer_layout((x_layout,), extra_args=(1,))141+ cache_values = [x_layout, 1]
142+ output_layouts, extra_info = op.infer_layout(cache_values)
143+ output_layout = output_layouts[0]
132 144 
133 expected_map = (2, -1, 0)145 expected_map = (2, -1, 0)
134 assert output_layout.tensor_map == expected_map, (146 assert output_layout.tensor_map == expected_map, (
135 f"Mixed parallel cumsum failed. Expected {expected_map}, "147 f"Mixed parallel cumsum failed. Expected {expected_map}, "
136 f"got {output_layout.tensor_map}"148 f"got {output_layout.tensor_map}"
137 )149 )
150+ assert extra_info is None, f"Cumsum extra_info should be None, got {extra_info}"
138 151 
139 @patch("hyper_parallel.core.dtensor.device_mesh.platform")152 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
140 def test_cumsum_layout_negative_dim(self, mock_platform):153 def test_cumsum_layout_negative_dim(self, mock_platform):
@@ -147,13 +160,16 @@ class TestParallelCumsum(unittest.TestCase):
147 x_placements = (Replicate(), Shard(1))160 x_placements = (Replicate(), Shard(1))
148 x_layout = _build_layout(mesh, x_placements, 2)161 x_layout = _build_layout(mesh, x_placements, 2)
149 162 
150- output_layout = op.infer_layout((x_layout,), extra_args=(-2,))163+ cache_values = [x_layout, -2]
164+ output_layouts, extra_info = op.infer_layout(cache_values)
165+ output_layout = output_layouts[0]
151 166 
152 expected_map = (-1, 0)167 expected_map = (-1, 0)
153 assert output_layout.tensor_map == expected_map, (168 assert output_layout.tensor_map == expected_map, (
154 f"Negative dimension cumsum failed. Expected {expected_map}, "169 f"Negative dimension cumsum failed. Expected {expected_map}, "
155 f"got {output_layout.tensor_map}"170 f"got {output_layout.tensor_map}"
156 )171 )
172+ assert extra_info is None, f"Cumsum extra_info should be None, got {extra_info}"
157 173 
158 @patch("hyper_parallel.core.dtensor.device_mesh.platform")174 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
159 def test_cumsum_layout_invalid_sharding_on_cumsum_dim(self, mock_platform):175 def test_cumsum_layout_invalid_sharding_on_cumsum_dim(self, mock_platform):
@@ -166,8 +182,8 @@ class TestParallelCumsum(unittest.TestCase):
166 x_placements = (Shard(0), Shard(1))182 x_placements = (Shard(0), Shard(1))
167 x_layout = _build_layout(mesh, x_placements, 2)183 x_layout = _build_layout(mesh, x_placements, 2)
168 184 
169- with self.assertRaisesRegex(ValueError, "Cannot perform sharding on normalized dimension 1"):185+ with self.assertRaisesRegex(ValueError, "sharded dimension"):
170- op.infer_layout((x_layout,), extra_args=(-1,))186+ op.infer_layout([x_layout, -1])
171 187 
172 @patch("hyper_parallel.core.dtensor.device_mesh.platform")188 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
173 def test_cumsum_layout_dim_out_of_range_positive(self, mock_platform):189 def test_cumsum_layout_dim_out_of_range_positive(self, mock_platform):
@@ -180,8 +196,8 @@ class TestParallelCumsum(unittest.TestCase):
180 x_placements = (Replicate(), Replicate())196 x_placements = (Replicate(), Replicate())
181 x_layout = _build_layout(mesh, x_placements, 2)197 x_layout = _build_layout(mesh, x_placements, 2)
182 198 
183- with self.assertRaisesRegex(ValueError, "Dimension 2 out of range for 2-dimensional input tensor"):199+ with self.assertRaisesRegex(ValueError, "dimension out of range"):
184- op.infer_layout((x_layout,), extra_args=(2,))200+ op.infer_layout([x_layout, 2])
185 201 
186 @patch("hyper_parallel.core.dtensor.device_mesh.platform")202 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
187 def test_cumsum_layout_missing_dim_parameter(self, mock_platform):203 def test_cumsum_layout_missing_dim_parameter(self, mock_platform):
@@ -194,11 +210,8 @@ class TestParallelCumsum(unittest.TestCase):
194 x_placements = (Replicate(), Replicate())210 x_placements = (Replicate(), Replicate())
195 x_layout = _build_layout(mesh, x_placements, 2)211 x_layout = _build_layout(mesh, x_placements, 2)
196 212 
197- with self.assertRaisesRegex(ValueError, "cumsum requires 'dim' parameter in extra_args"):213+ with self.assertRaisesRegex(ValueError, "dimension should be int"):
198- op.infer_layout((x_layout,), extra_args=None)214+ op.infer_layout([x_layout, None])
199- 
200- with self.assertRaisesRegex(ValueError, "cumsum requires 'dim' parameter in extra_args"):
201- op.infer_layout((x_layout,), extra_args=(None,))
202 215 
203 @patch("hyper_parallel.core.dtensor.device_mesh.platform")216 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
204 def test_cumsum_layout_invalid_dim_type(self, mock_platform):217 def test_cumsum_layout_invalid_dim_type(self, mock_platform):
@@ -211,8 +224,81 @@ class TestParallelCumsum(unittest.TestCase):
211 x_placements = (Replicate(), Replicate())224 x_placements = (Replicate(), Replicate())
212 x_layout = _build_layout(mesh, x_placements, 2)225 x_layout = _build_layout(mesh, x_placements, 2)
213 226 
214- with self.assertRaisesRegex(ValueError, "'dim' must be an integer, got <class 'str'>"):227+ with self.assertRaisesRegex(ValueError, "dimension should be int"):
215- op.infer_layout((x_layout,), extra_args=("invalid",))228+ op.infer_layout([x_layout, "invalid"])
229+ 
230+ @patch("hyper_parallel.core.dtensor.device_mesh.platform")
231+ def test_cumsum_partial_input_raises_error(self, mock_platform):
232+ """
233+ Feature: Cumsum with Partial input
234+ Description: Input layout has Partial status
235+ Expectation: ValueError raised
236+ """
237+ mesh = self._make_2x4_mesh(mock_platform)
238+ x_placements = (Replicate(), Replicate())
239+ x_layout = _build_layout(mesh, x_placements, 2)
240+ x_layout.set_partial_by_dev_axis("dp", "sum")
241+ 
242+ with self.assertRaisesRegex(ValueError, "Partial status"):
243+ op.infer_layout([x_layout, -1])
244+ 
245+ @patch("hyper_parallel.core.dtensor.device_mesh.platform")
246+ def test_cumsum_preprocess_torch_dtype_in_kwargs(self, mock_platform):
247+ """
248+ Feature: Cumsum preprocess for PyTorch
249+ Description: dtype is keyword-only parameter
250+ Expectation: local tensor and dim are positional; dtype is in kwargs
251+ """
252+ mesh = self._make_2x4_mesh(mock_platform)
253+ x_layout = _build_layout(mesh, (Shard(0), Replicate()), 2)
254+ 
255+ mock_tensor = MagicMock()
256+ mock_tensor.layout = x_layout
257+ mock_tensor.to_local.return_value = MagicMock()
258+ 
259+ local_args, local_kwargs, cache_values = op.preprocess(
260+ (mock_tensor,),
261+ {'dim': -1, 'dtype': 'float32'}
262+ )
263+ 
264+ assert len(local_args) == 2, (
265+ f"For PyTorch 'cumsum', local_args should be (tensor, dim), got {local_args}"
266+ )
267+ assert local_args[1] == -1, f"dim should be preserved as -1, got {local_args[1]}"
268+ assert local_kwargs == {'dtype': 'float32'}, (
269+ f"For PyTorch 'cumsum', dtype should be kwargs, got {local_kwargs}"
270+ )
271+ assert cache_values == [x_layout, -1], f"Unexpected cache_values: {cache_values}"
272+ 
273+ @patch("hyper_parallel.core.dtensor.device_mesh.platform")
274+ def test_cumsum_preprocess_mindspore_primitive_in_args(self, mock_platform):
275+ """
276+ Feature: Cumsum preprocess for MindSpore Primitive
277+ Description: CumsumExt does not accept kwargs
278+ Expectation: dim and dtype are routed to positional args, including dtype=None
279+ """
280+ mesh = self._make_2x4_mesh(mock_platform)
281+ x_layout = _build_layout(mesh, (Shard(0), Replicate()), 2)
282+ 
283+ mock_tensor = MagicMock()
284+ mock_tensor.layout = x_layout
285+ mock_tensor.to_local.return_value = MagicMock()
286+ 
287+ local_args, local_kwargs, cache_values = op_ms.preprocess(
288+ (mock_tensor,),
289+ {'dim': -1}
290+ )
291+ 
292+ assert not local_kwargs, (
293+ f"For MindSpore 'CumsumExt', local_kwargs should be empty, got {local_kwargs}"
294+ )
295+ assert len(local_args) == 3, (
296+ f"For MindSpore 'CumsumExt', local_args should be (tensor, dim, dtype), got {local_args}"
297+ )
298+ assert local_args[1:] == (-1, None), (
299+ f"MindSpore positional dim/dtype mismatch, got {local_args[1:]}"
300+ )
301+ assert cache_values == [x_layout, -1], f"Unexpected cache_values: {cache_values}"
216 302 
217 303 
218if __name__ == "__main__":304if __name__ == "__main__":
@@ -13,9 +13,8 @@
13# limitations under the License.13# limitations under the License.
14# ============================================================================14# ============================================================================
15"""Unit tests for element-wise distributed operators"""15"""Unit tests for element-wise distributed operators"""
16-import os
17import unittest16import unittest
18-from unittest.mock import patch17+from unittest.mock import MagicMock, patch
19import copy18import copy
20import numpy as np19import numpy as np
21 20 
@@ -29,6 +28,31 @@ from hyper_parallel.core.dtensor.device_mesh import (
29from hyper_parallel.platform.platform import EXISTING_COMM_GROUPS28from hyper_parallel.platform.platform import EXISTING_COMM_GROUPS
30 29 
31 30 
31+def _extract_test_input_shapes(extra_args):
32+ """Extract input shapes from extra_args dict."""
33+ if isinstance(extra_args, dict):
34+ return extra_args.get("input_shapes", None)
35+ return None
36+ 
37+ 
38+def _make_cache_values(layouts, extra_args=None):
39+ """Build cache_values for the new preprocess/infer_layout protocol."""
40+ return [*layouts, _extract_test_input_shapes(extra_args)]
41+ 
42+ 
43+def _infer_output_layout(op, layouts, extra_args=None):
44+ """Infer and return the single output layout."""
45+ output_layouts, extra_info = op.infer_layout(_make_cache_values(layouts, extra_args))
46+ assert extra_info is None
47+ return output_layouts[0]
48+ 
49+ 
50+def _get_expand_impl(op, func, output_layout, layouts, extra_args=None):
51+ """Call get_expand_impl with new-protocol infer_result and cache_values."""
52+ cache_values = _make_cache_values(layouts, extra_args)
53+ return op.get_expand_impl(func, ((output_layout,), None), cache_values)
54+ 
55+ 
32class TestParallelElementwiseOps(unittest.TestCase):56class TestParallelElementwiseOps(unittest.TestCase):
33 """Unit tests for ElementWiseDistributedOp."""57 """Unit tests for ElementWiseDistributedOp."""
34 def setUp(self):58 def setUp(self):
@@ -39,6 +63,7 @@ class TestParallelElementwiseOps(unittest.TestCase):
39 """63 """
40 EXISTING_COMM_GROUPS.clear()64 EXISTING_COMM_GROUPS.clear()
41 _DEVICE_MESH_MAP.clear()65 _DEVICE_MESH_MAP.clear()
66+ _LAYOUT_CACHE.clear()
42 self.op = ElementWiseDistributedOp("element_wise")67 self.op = ElementWiseDistributedOp("element_wise")
43 self.op_with_partial = AddDistributedOp("element_wise_with_partial")68 self.op_with_partial = AddDistributedOp("element_wise_with_partial")
44 69 
@@ -46,6 +71,7 @@ class TestParallelElementwiseOps(unittest.TestCase):
46 """Clean up after each test method."""71 """Clean up after each test method."""
47 EXISTING_COMM_GROUPS.clear()72 EXISTING_COMM_GROUPS.clear()
48 _DEVICE_MESH_MAP.clear()73 _DEVICE_MESH_MAP.clear()
74+ _LAYOUT_CACHE.clear()
49 75 
50 def _setup_mock_platform(self, mock_platform, platform_type=None, world_size=8):76 def _setup_mock_platform(self, mock_platform, platform_type=None, world_size=8):
51 """Configure common mock-platform attributes used across tests.77 """Configure common mock-platform attributes used across tests.
@@ -79,12 +105,21 @@ class TestParallelElementwiseOps(unittest.TestCase):
79 op_ = self.op_with_partial105 op_ = self.op_with_partial
80 else:106 else:
81 op_ = self.op107 op_ = self.op
82- output_layout = op_.infer_layout((x_layout, y_layout), extra_args)108+ 
109+ cache_values = [
110+ x_layout,
111+ y_layout,
112+ extra_args.get("input_shapes"),
113+ ]
114+ 
115+ infer_result = op_.infer_layout(cache_values)
116+ output_layout = infer_result[0][0]
117+ 
83 got_map = output_layout.tensor_map118 got_map = output_layout.tensor_map
84 assert got_map == expected_map, (119 assert got_map == expected_map, (
85 f"Element-wise failed. Expected {expected_map}, got {got_map}"120 f"Element-wise failed. Expected {expected_map}, got {got_map}"
86 )121 )
87- impl = op_.get_expand_impl(None, output_layout, (x_layout, y_layout), extra_args)122+ impl = op_.get_expand_impl(None, infer_result, cache_values)
88 if flag:123 if flag:
89 assert impl is not None, (124 assert impl is not None, (
90 f"get_expand_impl test failed. Expected non-None, got {impl}"125 f"get_expand_impl test failed. Expected non-None, got {impl}"
@@ -101,12 +136,20 @@ class TestParallelElementwiseOps(unittest.TestCase):
101 op_ = self.op_with_partial136 op_ = self.op_with_partial
102 else:137 else:
103 op_ = self.op138 op_ = self.op
104- output_layout = op_.infer_layout((x_layout,), extra_args)139+ 
140+ cache_values = [
141+ x_layout,
142+ extra_args.get("input_shapes"),
143+ ]
144+ 
145+ infer_result = op_.infer_layout(cache_values)
146+ output_layout = infer_result[0][0]
147+ 
105 got_map = output_layout.tensor_map148 got_map = output_layout.tensor_map
106 assert got_map == expected_map, (149 assert got_map == expected_map, (
107 f"Element-wise failed. Expected {expected_map}, got {got_map}"150 f"Element-wise failed. Expected {expected_map}, got {got_map}"
108 )151 )
109- impl = op_.get_expand_impl(None, output_layout, (x_layout,), extra_args)152+ impl = op_.get_expand_impl(None, infer_result, cache_values)
110 if flag:153 if flag:
111 assert impl is not None, (154 assert impl is not None, (
112 f"get_expand_impl test failed. Expected non-None, got {impl}"155 f"get_expand_impl test failed. Expected non-None, got {impl}"
@@ -117,6 +160,37 @@ class TestParallelElementwiseOps(unittest.TestCase):
117 f"get_expand_impl test failed. Expected None, got {impl}"160 f"get_expand_impl test failed. Expected None, got {impl}"
118 )161 )
119 162 
163+ def test_preprocess_unwraps_dtensors_and_builds_cache_values(self):
164+ """
165+ Feature: Element-wise preprocess
166+ Description: DTensor-like inputs are converted to local tensors and cached with layout/shape.
167+ Expectation: local args/kwargs use local tensors and cache_values carries layouts first.
168+ """
169+ x_layout = MagicMock()
170+ y_layout = MagicMock()
171+ x_local = MagicMock()
172+ y_local = MagicMock()
173+ 
174+ x_tensor = MagicMock()
175+ x_tensor.layout = x_layout
176+ x_tensor.shape = (4, 8)
177+ x_tensor.to_local.return_value = x_local
178+ x_tensor._layout = x_layout
179+ 
180+ y_tensor = MagicMock()
181+ y_tensor.layout = y_layout
182+ y_tensor.shape = (4, 8)
183+ y_tensor.to_local.return_value = y_local
184+ y_tensor._layout = y_layout
185+ 
186+ local_args, local_kwargs, cache_values = self.op.preprocess((x_tensor,), {"other": y_tensor})
187+ 
188+ assert local_args == (x_local,)
189+ assert local_kwargs == {"other": y_local}
190+ assert cache_values[0] is x_layout
191+ assert cache_values[1] is y_layout
192+ assert cache_values[2] == [(4, 8), (4, 8)]
193+ 
120 @patch("hyper_parallel.core.dtensor.device_mesh.platform")194 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
121 def test_single_input_partial_0(self, mock_platform):195 def test_single_input_partial_0(self, mock_platform):
122 """196 """
@@ -129,7 +203,7 @@ class TestParallelElementwiseOps(unittest.TestCase):
129 x_layout.set_partial_by_dev_axis("dp", "sum")203 x_layout.set_partial_by_dev_axis("dp", "sum")
130 204 
131 with self.assertRaisesRegex(ValueError, "has Partial status which is not allowed"):205 with self.assertRaisesRegex(ValueError, "has Partial status which is not allowed"):
132- self.op.infer_layout((x_layout,), extra_args={"input_shapes": [(4, 8, 16)]})206+ self.op.infer_layout(_make_cache_values((x_layout,), {"input_shapes": [(4, 8, 16)]}))
133 207 
134 @patch("hyper_parallel.core.dtensor.device_mesh.platform")208 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
135 def test_single_input_replicate_1(self, mock_platform):209 def test_single_input_replicate_1(self, mock_platform):
@@ -177,7 +251,9 @@ class TestParallelElementwiseOps(unittest.TestCase):
177 x_layout = _build_layout(mesh, (Replicate(), Replicate(), Replicate()), 3)251 x_layout = _build_layout(mesh, (Replicate(), Replicate(), Replicate()), 3)
178 x_layout.set_partial_by_dev_axis("dp", "sum")252 x_layout.set_partial_by_dev_axis("dp", "sum")
179 253 
180- output_layout = self.op_with_partial.infer_layout((x_layout,), extra_args={"input_shapes": [(4, 8, 16)]})254+ output_layout = _infer_output_layout(
255+ self.op_with_partial, (x_layout,), {"input_shapes": [(4, 8, 16)]}
256+ )
181 257 
182 assert output_layout.partial[mesh.axis_index("dp")] == "sum"258 assert output_layout.partial[mesh.axis_index("dp")] == "sum"
183 259 
@@ -249,7 +325,7 @@ class TestParallelElementwiseOps(unittest.TestCase):
249 extra_args = {"input_shapes": [(4, 8, 16), (4, 8, 16)]}325 extra_args = {"input_shapes": [(4, 8, 16), (4, 8, 16)]}
250 326 
251 with self.assertRaisesRegex(ValueError, "should have same sharding pattern"):327 with self.assertRaisesRegex(ValueError, "should have same sharding pattern"):
252- self.op.infer_layout((x_layout, y_layout), extra_args=extra_args)328+ self.op.infer_layout(_make_cache_values((x_layout, y_layout), extra_args))
253 329 
254 @patch("hyper_parallel.core.dtensor.device_mesh.platform")330 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
255 def test_sharded_broadcasts_to_replicated_8(self, mock_platform):331 def test_sharded_broadcasts_to_replicated_8(self, mock_platform):
@@ -265,7 +341,7 @@ class TestParallelElementwiseOps(unittest.TestCase):
265 extra_args = {"input_shapes": [(1, 8, 16), (4, 8, 16)]}341 extra_args = {"input_shapes": [(1, 8, 16), (4, 8, 16)]}
266 342 
267 with self.assertRaisesRegex(ValueError, "Broadcasting dimension cannot be sharded"):343 with self.assertRaisesRegex(ValueError, "Broadcasting dimension cannot be sharded"):
268- self.op.infer_layout((x_layout, y_layout), extra_args=extra_args)344+ self.op.infer_layout(_make_cache_values((x_layout, y_layout), extra_args))
269 345 
270 @patch("hyper_parallel.core.dtensor.device_mesh.platform")346 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
271 def test_replicated_broadcasts_to_sharded_9(self, mock_platform):347 def test_replicated_broadcasts_to_sharded_9(self, mock_platform):
@@ -354,11 +430,11 @@ class TestParallelElementwiseOps(unittest.TestCase):
354 y_layout._partial = [None] * len(y_layout._partial)430 y_layout._partial = [None] * len(y_layout._partial)
355 431 
356 extra_args = {"input_shapes": [(4, 8, 16), (4, 8, 16)]}432 extra_args = {"input_shapes": [(4, 8, 16), (4, 8, 16)]}
357- output_layout = self.op_with_partial.infer_layout((x_layout, y_layout), extra_args)433+ output_layout = _infer_output_layout(self.op_with_partial, (x_layout, y_layout), extra_args)
358 434 
359 assert output_layout.partial[mesh.axis_index("dp")] == "sum"435 assert output_layout.partial[mesh.axis_index("dp")] == "sum"
360 436 
361- impl = self.op_with_partial.get_expand_impl(None, output_layout, (x_layout, y_layout), extra_args)437+ impl = _get_expand_impl(self.op_with_partial, None, output_layout, (x_layout, y_layout), extra_args)
362 assert impl is not None, (438 assert impl is not None, (
363 f"get_expand_impl test failed. Expected non-None, got {impl}"439 f"get_expand_impl test failed. Expected non-None, got {impl}"
364 )440 )
@@ -378,11 +454,11 @@ class TestParallelElementwiseOps(unittest.TestCase):
378 y_layout.set_partial_by_dev_axis("dp", "sum")454 y_layout.set_partial_by_dev_axis("dp", "sum")
379 455 
380 extra_args = {"input_shapes": [(4, 8, 16), (4, 8, 16)]}456 extra_args = {"input_shapes": [(4, 8, 16), (4, 8, 16)]}
381- output_layout = self.op_with_partial.infer_layout((x_layout, y_layout), extra_args)457+ output_layout = _infer_output_layout(self.op_with_partial, (x_layout, y_layout), extra_args)
382 458 
383 assert output_layout.partial[mesh.axis_index("dp")] == "sum"459 assert output_layout.partial[mesh.axis_index("dp")] == "sum"
384 460 
385- impl = self.op_with_partial.get_expand_impl(None, output_layout, (x_layout, y_layout), extra_args)461+ impl = _get_expand_impl(self.op_with_partial, None, output_layout, (x_layout, y_layout), extra_args)
386 assert impl is None, (462 assert impl is None, (
387 f"get_expand_impl test failed. Expected None, got {impl}"463 f"get_expand_impl test failed. Expected None, got {impl}"
388 )464 )
@@ -403,7 +479,7 @@ class TestParallelElementwiseOps(unittest.TestCase):
403 extra_args = {"input_shapes": [(4, 8, 16), (4, 8, 16)]}479 extra_args = {"input_shapes": [(4, 8, 16), (4, 8, 16)]}
404 480 
405 with self.assertRaisesRegex(ValueError, "partial operations should be same"):481 with self.assertRaisesRegex(ValueError, "partial operations should be same"):
406- self.op_with_partial.infer_layout((x_layout, y_layout), extra_args=extra_args)482+ self.op_with_partial.infer_layout(_make_cache_values((x_layout, y_layout), extra_args))
407 483 
408 @patch("hyper_parallel.core.dtensor.device_mesh.platform")484 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
409 def test_shard_with_partial_conflict_16(self, mock_platform):485 def test_shard_with_partial_conflict_16(self, mock_platform):
@@ -420,7 +496,7 @@ class TestParallelElementwiseOps(unittest.TestCase):
420 extra_args = {"input_shapes": [(4, 8, 16), (4, 8, 16)]}496 extra_args = {"input_shapes": [(4, 8, 16), (4, 8, 16)]}
421 497 
422 with self.assertRaisesRegex(ValueError, "Shard and Partial should not coexist on same device axis"):498 with self.assertRaisesRegex(ValueError, "Shard and Partial should not coexist on same device axis"):
423- self.op_with_partial.infer_layout((x_layout, y_layout), extra_args=extra_args)499+ self.op_with_partial.infer_layout(_make_cache_values((x_layout, y_layout), extra_args))
424 500 
425 @patch("hyper_parallel.core.dtensor.device_mesh.platform")501 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
426 def test_partial_broadcasts_to_sharded_17(self, mock_platform):502 def test_partial_broadcasts_to_sharded_17(self, mock_platform):
@@ -437,7 +513,7 @@ class TestParallelElementwiseOps(unittest.TestCase):
437 extra_args = {"input_shapes": [(1, 8, 16), (4, 8, 16)]}513 extra_args = {"input_shapes": [(1, 8, 16), (4, 8, 16)]}
438 514 
439 with self.assertRaisesRegex(ValueError, "Shard and Partial should not coexist on same device axis"):515 with self.assertRaisesRegex(ValueError, "Shard and Partial should not coexist on same device axis"):
440- self.op_with_partial.infer_layout((x_layout, y_layout), extra_args=extra_args)516+ self.op_with_partial.infer_layout(_make_cache_values((x_layout, y_layout), extra_args))
441 517 
442 @patch("hyper_parallel.core.dtensor.device_mesh.platform")518 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
443 def test_multiple_inputs_18(self, mock_platform):519 def test_multiple_inputs_18(self, mock_platform):
@@ -454,15 +530,15 @@ class TestParallelElementwiseOps(unittest.TestCase):
454 extra_args = {"input_shapes": [(4, 8, 16), (1, 8, 16), (1, 8, 16)]}530 extra_args = {"input_shapes": [(4, 8, 16), (1, 8, 16), (1, 8, 16)]}
455 expected_map = (2, -1, -1)531 expected_map = (2, -1, -1)
456 532 
457- output_layout = self.op.infer_layout((x_layout, y_layout, z_layout), extra_args=extra_args)533+ output_layout = _infer_output_layout(self.op, (x_layout, y_layout, z_layout), extra_args)
458 got_map = output_layout.tensor_map534 got_map = output_layout.tensor_map
459 assert got_map == expected_map535 assert got_map == expected_map
460 536 
461- assert self.op.get_expand_impl(None, output_layout,537+ assert _get_expand_impl(self.op, None, output_layout,
462- (x_layout, y_layout, z_layout), extra_args) is None, (538+ (x_layout, y_layout, z_layout), extra_args) is None, (
463 f"get_expand_impl test failed. Expected None, "539 f"get_expand_impl test failed. Expected None, "
464- f"""got {self.op.get_expand_impl(None, output_layout,540+ f"""got {_get_expand_impl(self.op, None, output_layout,
465- (x_layout, y_layout, z_layout), extra_args)}"""541+ (x_layout, y_layout, z_layout), extra_args)}"""
466 )542 )
467 543 
468 @patch("hyper_parallel.core.dtensor.device_mesh.platform")544 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
@@ -480,7 +556,7 @@ class TestParallelElementwiseOps(unittest.TestCase):
480 extra_args = {"input_shapes": [(4, 8, 16), (4, 8, 16)]}556 extra_args = {"input_shapes": [(4, 8, 16), (4, 8, 16)]}
481 557 
482 with self.assertRaisesRegex(ValueError, "Shard and Partial should not coexist on same device axis"):558 with self.assertRaisesRegex(ValueError, "Shard and Partial should not coexist on same device axis"):
483- self.op_with_partial.infer_layout((x_layout, y_layout), extra_args=extra_args)559+ self.op_with_partial.infer_layout(_make_cache_values((x_layout, y_layout), extra_args))
484 560 
485 @patch("hyper_parallel.core.dtensor.device_mesh.platform")561 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
486 def test_incompatible_broadcast_shapes_20(self, mock_platform):562 def test_incompatible_broadcast_shapes_20(self, mock_platform):
@@ -496,7 +572,7 @@ class TestParallelElementwiseOps(unittest.TestCase):
496 extra_args = {"input_shapes": [(4, 8, 16), (4, 7, 16)]}572 extra_args = {"input_shapes": [(4, 8, 16), (4, 7, 16)]}
497 573 
498 with self.assertRaisesRegex(ValueError, "cannot be broadcast"):574 with self.assertRaisesRegex(ValueError, "cannot be broadcast"):
499- self.op_with_partial.infer_layout((x_layout, y_layout), extra_args=extra_args)575+ self.op_with_partial.infer_layout(_make_cache_values((x_layout, y_layout), extra_args))
500 576 
501 @patch("hyper_parallel.core.dtensor.device_mesh.platform")577 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
502 def test_1d_to_3d_broadcast_21(self, mock_platform):578 def test_1d_to_3d_broadcast_21(self, mock_platform):
@@ -528,7 +604,7 @@ class TestParallelElementwiseOps(unittest.TestCase):
528 y_layout = _build_layout(mesh, (Replicate(), Replicate(), Replicate()), 3)604 y_layout = _build_layout(mesh, (Replicate(), Replicate(), Replicate()), 3)
529 605 
530 with self.assertRaisesRegex(ValueError, "cannot infer layout without shapes"):606 with self.assertRaisesRegex(ValueError, "cannot infer layout without shapes"):
531- self.op.infer_layout((x_layout, y_layout), extra_args={})607+ self.op.infer_layout(_make_cache_values((x_layout, y_layout), {}))
532 608 
533 609 
534class TestParallelArithmetic(unittest.TestCase):610class TestParallelArithmetic(unittest.TestCase):
@@ -538,11 +614,13 @@ class TestParallelArithmetic(unittest.TestCase):
538 """Set up test fixtures before each test method."""614 """Set up test fixtures before each test method."""
539 EXISTING_COMM_GROUPS.clear()615 EXISTING_COMM_GROUPS.clear()
540 _DEVICE_MESH_MAP.clear()616 _DEVICE_MESH_MAP.clear()
617+ _LAYOUT_CACHE.clear()
541 618 
542 def tearDown(self):619 def tearDown(self):
543 """Clean up after each test method."""620 """Clean up after each test method."""
544 EXISTING_COMM_GROUPS.clear()621 EXISTING_COMM_GROUPS.clear()
545 _DEVICE_MESH_MAP.clear()622 _DEVICE_MESH_MAP.clear()
623+ _LAYOUT_CACHE.clear()
546 624 
547 def _setup_mock_platform(self, mock_platform, platform_type=None, world_size=8):625 def _setup_mock_platform(self, mock_platform, platform_type=None, world_size=8):
548 """Configure common mock-platform attributes used across tests."""626 """Configure common mock-platform attributes used across tests."""
@@ -580,7 +658,7 @@ class TestParallelArithmetic(unittest.TestCase):
580 w_layout = _build_layout(mesh, w_placements, 2)658 w_layout = _build_layout(mesh, w_placements, 2)
581 659 
582 extra_args = {"input_shapes": [(4, 16), (4, 16)]}660 extra_args = {"input_shapes": [(4, 16), (4, 16)]}
583- output_layout = op.infer_layout((x_layout, w_layout), (extra_args))661+ output_layout = _infer_output_layout(op, (x_layout, w_layout), extra_args)
584 expected_map = ("dp", "mp")662 expected_map = ("dp", "mp")
585 assert output_layout.alias_tensor_map == expected_map, (663 assert output_layout.alias_tensor_map == expected_map, (
586 f"Data Parallel test failed. Expected {expected_map}, "664 f"Data Parallel test failed. Expected {expected_map}, "
@@ -589,9 +667,10 @@ class TestParallelArithmetic(unittest.TestCase):
589 667 
590 # Since `get_expand_impl` is not overridden, it returns None by default.668 # Since `get_expand_impl` is not overridden, it returns None by default.
591 # The same applies to other test classes, so it is unnecessary to test its return value.669 # The same applies to other test classes, so it is unnecessary to test its return value.
592- assert op.get_expand_impl(None, output_layout, (x_layout, None, None), (1, True)) is None, (670+ impl = _get_expand_impl(op, None, output_layout, (x_layout, w_layout), extra_args)
671+ assert impl is None, (
593 f"get_expand_impl test failed. Expected None, "672 f"get_expand_impl test failed. Expected None, "
594- f"got {op.get_expand_impl(None, output_layout, (x_layout, None, None), (1, True))}"673+ f"got {impl}"
595 )674 )
596 675 
597 @patch("hyper_parallel.core.dtensor.device_mesh.platform")676 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
@@ -609,7 +688,7 @@ class TestParallelArithmetic(unittest.TestCase):
609 w_placements = (Shard(0), Replicate())688 w_placements = (Shard(0), Replicate())
610 w_layout = _build_layout(mesh, w_placements, 2)689 w_layout = _build_layout(mesh, w_placements, 2)
611 extra_args = {"input_shapes": [(4, 16), (4, 16)]}690 extra_args = {"input_shapes": [(4, 16), (4, 16)]}
612- output_layout = op.infer_layout((x_layout, w_layout), (extra_args))691+ output_layout = _infer_output_layout(op, (x_layout, w_layout), extra_args)
613 expected_map = ("dp", "mp")692 expected_map = ("dp", "mp")
614 assert output_layout.alias_tensor_map == expected_map, (693 assert output_layout.alias_tensor_map == expected_map, (
615 f"Data Parallel test failed. Expected {expected_map}, "694 f"Data Parallel test failed. Expected {expected_map}, "
@@ -631,7 +710,7 @@ class TestParallelArithmetic(unittest.TestCase):
631 w_placements = (Shard(0), Replicate())710 w_placements = (Shard(0), Replicate())
632 w_layout = _build_layout(mesh, w_placements, 2)711 w_layout = _build_layout(mesh, w_placements, 2)
633 extra_args = {"input_shapes": [(4, 16), (4, 16)]}712 extra_args = {"input_shapes": [(4, 16), (4, 16)]}
634- output_layout = op.infer_layout((x_layout, w_layout), extra_args)713+ output_layout = _infer_output_layout(op, (x_layout, w_layout), extra_args)
635 expected_map = ("dp", "mp")714 expected_map = ("dp", "mp")
636 assert output_layout.alias_tensor_map == expected_map, (715 assert output_layout.alias_tensor_map == expected_map, (
637 f"Data Parallel test failed. Expected {expected_map}, "716 f"Data Parallel test failed. Expected {expected_map}, "
@@ -653,7 +732,7 @@ class TestParallelArithmetic(unittest.TestCase):
653 w_placements = (Shard(0), Replicate())732 w_placements = (Shard(0), Replicate())
654 w_layout = _build_layout(mesh, w_placements, 2)733 w_layout = _build_layout(mesh, w_placements, 2)
655 extra_args = {"input_shapes": [(4, 16), (4, 16)]}734 extra_args = {"input_shapes": [(4, 16), (4, 16)]}
656- output_layout = op.infer_layout((x_layout, w_layout), extra_args)735+ output_layout = _infer_output_layout(op, (x_layout, w_layout), extra_args)
657 expected_map = ("dp", "mp")736 expected_map = ("dp", "mp")
658 assert output_layout.alias_tensor_map == expected_map, (737 assert output_layout.alias_tensor_map == expected_map, (
659 f"Data Parallel test failed. Expected {expected_map}, "738 f"Data Parallel test failed. Expected {expected_map}, "
@@ -675,7 +754,7 @@ class TestParallelArithmetic(unittest.TestCase):
675 w_placements = (Shard(0), Replicate())754 w_placements = (Shard(0), Replicate())
676 w_layout = _build_layout(mesh, w_placements, 2)755 w_layout = _build_layout(mesh, w_placements, 2)
677 extra_args = {"input_shapes": [(4, 16), (4, 16)]}756 extra_args = {"input_shapes": [(4, 16), (4, 16)]}
678- output_layout = op.infer_layout((x_layout, w_layout), extra_args)757+ output_layout = _infer_output_layout(op, (x_layout, w_layout), extra_args)
679 expected_map = ("dp", "mp")758 expected_map = ("dp", "mp")
680 assert output_layout.alias_tensor_map == expected_map, (759 assert output_layout.alias_tensor_map == expected_map, (
681 f"Data Parallel test failed. Expected {expected_map}, "760 f"Data Parallel test failed. Expected {expected_map}, "
@@ -728,7 +807,9 @@ class TestParallelZerosLike(unittest.TestCase):
728 placements = (Shard(0), Replicate())807 placements = (Shard(0), Replicate())
729 x_layout = _build_layout(mesh, placements, 2)808 x_layout = _build_layout(mesh, placements, 2)
730 809 
731- output_layout = self.op.infer_layout((x_layout,), {})810+ cache_values = [x_layout, None]
811+ infer_result = self.op.infer_layout(cache_values)
812+ output_layout = infer_result[0][0]
732 813 
733 expected_map = (1, -1)814 expected_map = (1, -1)
734 assert output_layout.tensor_map == expected_map, (815 assert output_layout.tensor_map == expected_map, (
@@ -738,9 +819,8 @@ class TestParallelZerosLike(unittest.TestCase):
738 819 
739 # Since `get_expand_impl` is not overridden, it returns None by default.820 # Since `get_expand_impl` is not overridden, it returns None by default.
740 # The same applies to other test cases, so it is unnecessary to test its return value.821 # The same applies to other test cases, so it is unnecessary to test its return value.
741- assert self.op.get_expand_impl(None, output_layout, (x_layout,), {}) is None, (822+ assert self.op.get_expand_impl(None, infer_result, cache_values) is None, (
742- f"get_expand_impl should return None, "823+ "get_expand_impl should return None"
743- f"got={self.op.get_expand_impl(None, output_layout, (x_layout,), {})}"
744 )824 )
745 825 
746 @patch("hyper_parallel.core.dtensor.device_mesh.platform")826 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
@@ -754,7 +834,9 @@ class TestParallelZerosLike(unittest.TestCase):
754 placements = (Replicate(), Replicate())834 placements = (Replicate(), Replicate())
755 x_layout = _build_layout(mesh, placements, 2)835 x_layout = _build_layout(mesh, placements, 2)
756 836 
757- output_layout = self.op.infer_layout((x_layout,), {})837+ cache_values = [x_layout, None]
838+ infer_result = self.op.infer_layout(cache_values)
839+ output_layout = infer_result[0][0]
758 840 
759 expected_map = (-1, -1)841 expected_map = (-1, -1)
760 assert output_layout.tensor_map == expected_map, (842 assert output_layout.tensor_map == expected_map, (
@@ -774,7 +856,7 @@ class TestParallelZerosLike(unittest.TestCase):
774 x_layout.set_partial_by_dev_axis("dp", "sum")856 x_layout.set_partial_by_dev_axis("dp", "sum")
775 857 
776 with self.assertRaisesRegex(ValueError, "has Partial status which is not allowed"):858 with self.assertRaisesRegex(ValueError, "has Partial status which is not allowed"):
777- self.op.infer_layout((x_layout,), {})859+ self.op.infer_layout([x_layout, None])
778 860 
779 861 
780if __name__ == "__main__":862if __name__ == "__main__":
@@ -18,7 +18,7 @@ import unittest
18from unittest.mock import patch, MagicMock18from unittest.mock import patch, MagicMock
19import numpy as np19import numpy as np
20 20 
21-from hyper_parallel.core.dtensor.dtensor import _build_layout21+from hyper_parallel.core.dtensor.dtensor import _build_layout, _LAYOUT_CACHE
22from hyper_parallel.core.dtensor.placement_types import Shard, Replicate, Partial22from hyper_parallel.core.dtensor.placement_types import Shard, Replicate, Partial
23from hyper_parallel.core.shard.ops.parallel_embedding import EmbeddingDistributedOp23from hyper_parallel.core.shard.ops.parallel_embedding import EmbeddingDistributedOp
24from hyper_parallel.core.dtensor.device_mesh import (24from hyper_parallel.core.dtensor.device_mesh import (
@@ -41,11 +41,13 @@ class TestParallelEmbedding(unittest.TestCase):
41 """Set up test fixtures before each test method."""41 """Set up test fixtures before each test method."""
42 EXISTING_COMM_GROUPS.clear()42 EXISTING_COMM_GROUPS.clear()
43 _DEVICE_MESH_MAP.clear()43 _DEVICE_MESH_MAP.clear()
44+ _LAYOUT_CACHE.clear()
44 45 
45 def tearDown(self):46 def tearDown(self):
46 """Clean up after each test method."""47 """Clean up after each test method."""
47 EXISTING_COMM_GROUPS.clear()48 EXISTING_COMM_GROUPS.clear()
48 _DEVICE_MESH_MAP.clear()49 _DEVICE_MESH_MAP.clear()
50+ _LAYOUT_CACHE.clear()
49 51 
50 def _setup_mock_platform(self, mock_platform, world_size=8):52 def _setup_mock_platform(self, mock_platform, world_size=8):
51 """Configure common mock-platform attributes."""53 """Configure common mock-platform attributes."""
@@ -79,13 +81,15 @@ class TestParallelEmbedding(unittest.TestCase):
79 weight_layout = _build_layout(mesh, (Replicate(), Replicate()), 2)81 weight_layout = _build_layout(mesh, (Replicate(), Replicate()), 2)
80 82 
81 for op in embedding_ops:83 for op in embedding_ops:
82- output_layout = op.infer_layout((input_layout, weight_layout))84+ cache_values = [input_layout, weight_layout]
85+ output_layouts, _ = op.infer_layout(cache_values)
86+ output_layout = output_layouts[0]
83 # Expected map: input(1, -1) + weight_embed(-1) = (1, -1, -1)87 # Expected map: input(1, -1) + weight_embed(-1) = (1, -1, -1)
84 expected_map = (1, -1, -1)88 expected_map = (1, -1, -1)
85 self.assertEqual(output_layout.tensor_map, expected_map, f"Op {op.op_name} failed")89 self.assertEqual(output_layout.tensor_map, expected_map, f"Op {op.op_name} failed")
86 self.assertFalse(output_layout.is_partial())90 self.assertFalse(output_layout.is_partial())
87 # Implementation should be native (None) for DP91 # Implementation should be native (None) for DP
88- self.assertIsNone(op.get_expand_impl(None, output_layout, (input_layout, weight_layout), None))92+ self.assertIsNone(op.get_expand_impl(None, (output_layouts, None), cache_values))
89 93 
90 @patch("hyper_parallel.core.dtensor.device_mesh.platform")94 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
91 def test_embedding_column_parallel(self, mock_platform):95 def test_embedding_column_parallel(self, mock_platform):
@@ -101,13 +105,15 @@ class TestParallelEmbedding(unittest.TestCase):
101 weight_layout = _build_layout(mesh, (Replicate(), Shard(1)), 2)105 weight_layout = _build_layout(mesh, (Replicate(), Shard(1)), 2)
102 106 
103 for op in embedding_ops:107 for op in embedding_ops:
104- output_layout = op.infer_layout((input_layout, weight_layout))108+ cache_values = [input_layout, weight_layout]
109+ output_layouts, _ = op.infer_layout(cache_values)
110+ output_layout = output_layouts[0]
105 # Expected map: input(1, -1) + weight_embed(0) = (1, -1, 0)111 # Expected map: input(1, -1) + weight_embed(0) = (1, -1, 0)
106 expected_map = (1, -1, 0)112 expected_map = (1, -1, 0)
107 self.assertEqual(output_layout.tensor_map, expected_map, f"Op {op.op_name} failed")113 self.assertEqual(output_layout.tensor_map, expected_map, f"Op {op.op_name} failed")
108 114 
109 # CP requires wrapper to intercept max_norm115 # CP requires wrapper to intercept max_norm
110- impl = op.get_expand_impl(None, output_layout, (input_layout, weight_layout), None)116+ impl = op.get_expand_impl(None, (output_layouts, None), cache_values)
111 self.assertTrue(callable(impl))117 self.assertTrue(callable(impl))
112 with self.assertRaisesRegex(ValueError, "Column-Parallel.*does not support `max_norm`"):118 with self.assertRaisesRegex(ValueError, "Column-Parallel.*does not support `max_norm`"):
113 impl(MagicMock(), MagicMock(), max_norm=1.0)119 impl(MagicMock(), MagicMock(), max_norm=1.0)
@@ -125,7 +131,8 @@ class TestParallelEmbedding(unittest.TestCase):
125 weight_layout = _build_layout(mesh, (Replicate(), Shard(0)), 2)131 weight_layout = _build_layout(mesh, (Replicate(), Shard(0)), 2)
126 132 
127 for op in embedding_ops:133 for op in embedding_ops:
128- output_layout = op.infer_layout((input_layout, weight_layout))134+ output_layouts, _ = op.infer_layout([input_layout, weight_layout])
135+ output_layout = output_layouts[0]
129 # Expected map: input(1, -1) + weight_embed(-1) = (1, -1, -1)136 # Expected map: input(1, -1) + weight_embed(-1) = (1, -1, -1)
130 self.assertEqual(output_layout.tensor_map, (1, -1, -1))137 self.assertEqual(output_layout.tensor_map, (1, -1, -1))
131 self.assertTrue(output_layout.is_partial())138 self.assertTrue(output_layout.is_partial())
@@ -148,7 +155,8 @@ class TestParallelEmbedding(unittest.TestCase):
148 weight_layout = _build_layout(mesh, (Replicate(), Shard(0), Shard(1)), 2)155 weight_layout = _build_layout(mesh, (Replicate(), Shard(0), Shard(1)), 2)
149 156 
150 for op in embedding_ops:157 for op in embedding_ops:
151- output_layout = op.infer_layout((input_layout, weight_layout))158+ output_layouts, _ = op.infer_layout([input_layout, weight_layout])
159+ output_layout = output_layouts[0]
152 # Expected map: input(2, -1) + weight_embed(0) = (2, -1, 0)160 # Expected map: input(2, -1) + weight_embed(0) = (2, -1, 0)
153 expected_map = (2, -1, 0)161 expected_map = (2, -1, 0)
154 self.assertEqual(output_layout.tensor_map, expected_map)162 self.assertEqual(output_layout.tensor_map, expected_map)
@@ -169,7 +177,8 @@ class TestParallelEmbedding(unittest.TestCase):
169 weight_layout = _build_layout(mesh, (Replicate(), Replicate()), 2)177 weight_layout = _build_layout(mesh, (Replicate(), Replicate()), 2)
170 178 
171 for op in embedding_ops:179 for op in embedding_ops:
172- output_layout = op.infer_layout((input_layout, weight_layout))180+ output_layouts, _ = op.infer_layout([input_layout, weight_layout])
181+ output_layout = output_layouts[0]
173 self.assertEqual(output_layout.tensor_map, (-1, 0, -1))182 self.assertEqual(output_layout.tensor_map, (-1, 0, -1))
174 183 
175 @patch("hyper_parallel.core.dtensor.device_mesh.platform")184 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
@@ -183,8 +192,8 @@ class TestParallelEmbedding(unittest.TestCase):
183 input_layout = _build_layout(mesh, (Replicate(), Replicate()), 2)192 input_layout = _build_layout(mesh, (Replicate(), Replicate()), 2)
184 193 
185 for op in embedding_ops:194 for op in embedding_ops:
186- with self.assertRaisesRegex(ValueError, "requires both input and weight layouts"):195+ with self.assertRaisesRegex(ValueError, "cache_values length should be 2"):
187- op.infer_layout((input_layout,))196+ op.infer_layout([input_layout])
188 197 
189 @patch("hyper_parallel.core.dtensor.device_mesh.platform")198 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
190 def test_embedding_partial_conflict_error(self, mock_platform):199 def test_embedding_partial_conflict_error(self, mock_platform):
@@ -200,7 +209,7 @@ class TestParallelEmbedding(unittest.TestCase):
200 209 
201 for op in embedding_ops:210 for op in embedding_ops:
202 with self.assertRaisesRegex(ValueError, "Partial dim must be replicate"):211 with self.assertRaisesRegex(ValueError, "Partial dim must be replicate"):
203- op.infer_layout((input_layout, weight_layout))212+ op.infer_layout([input_layout, weight_layout])
204 213 
205 @patch("hyper_parallel.core.dtensor.device_mesh.platform")214 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
206 def test_embedding_row_parallel_params_validation(self, mock_platform):215 def test_embedding_row_parallel_params_validation(self, mock_platform):
@@ -214,8 +223,9 @@ class TestParallelEmbedding(unittest.TestCase):
214 weight_layout = _build_layout(mesh, (Shard(0), Replicate()), 2)223 weight_layout = _build_layout(mesh, (Shard(0), Replicate()), 2)
215 224 
216 for op in embedding_ops:225 for op in embedding_ops:
217- output_layout = op.infer_layout((input_layout, weight_layout))226+ cache_values = [input_layout, weight_layout]
218- impl = op.get_expand_impl(None, output_layout, (input_layout, weight_layout), None)227+ output_layouts, _ = op.infer_layout(cache_values)
228+ impl = op.get_expand_impl(None, (output_layouts, None), cache_values)
219 229 
220 with self.assertRaisesRegex(ValueError, "Row-Parallel.*does not support `scale_grad_by_freq=True`"):230 with self.assertRaisesRegex(ValueError, "Row-Parallel.*does not support `scale_grad_by_freq=True`"):
221 impl(MagicMock(), MagicMock(), scale_grad_by_freq=True)231 impl(MagicMock(), MagicMock(), scale_grad_by_freq=True)
@@ -234,7 +244,8 @@ class TestParallelEmbedding(unittest.TestCase):
234 weight_layout = _build_layout(mesh, (Shard(0), Shard(1)), 2)244 weight_layout = _build_layout(mesh, (Shard(0), Shard(1)), 2)
235 245 
236 for op in embedding_ops:246 for op in embedding_ops:
237- output_layout = op.infer_layout((input_layout, weight_layout))247+ output_layouts, _ = op.infer_layout([input_layout, weight_layout])
248+ output_layout = output_layouts[0]
238 # Input map (-1, -1) + weight_embed(0) = (-1, -1, 0)249 # Input map (-1, -1) + weight_embed(0) = (-1, -1, 0)
239 self.assertEqual(output_layout.tensor_map, (-1, -1, 0))250 self.assertEqual(output_layout.tensor_map, (-1, -1, 0))
240 self.assertTrue(output_layout.is_partial())251 self.assertTrue(output_layout.is_partial())
@@ -255,7 +266,8 @@ class TestParallelEmbedding(unittest.TestCase):
255 weight_layout = _build_layout(mesh, (Replicate(), Shard(1)), 2)266 weight_layout = _build_layout(mesh, (Replicate(), Shard(1)), 2)
256 267 
257 for op in embedding_ops:268 for op in embedding_ops:
258- output_layout = op.infer_layout((input_layout, weight_layout))269+ output_layouts, _ = op.infer_layout([input_layout, weight_layout])
270+ output_layout = output_layouts[0]
259 # Expected map: (1, -1, -1, 0)271 # Expected map: (1, -1, -1, 0)
260 self.assertEqual(output_layout.tensor_map, (1, -1, -1, 0))272 self.assertEqual(output_layout.tensor_map, (1, -1, -1, 0))
261 273 
@@ -272,10 +284,40 @@ class TestParallelEmbedding(unittest.TestCase):
272 weight_layout = _build_layout(mesh, (Replicate(), Replicate()), 2)284 weight_layout = _build_layout(mesh, (Replicate(), Replicate()), 2)
273 285 
274 for op in embedding_ops:286 for op in embedding_ops:
275- output_layout = op.infer_layout((input_layout, weight_layout))287+ output_layouts, _ = op.infer_layout([input_layout, weight_layout])
288+ output_layout = output_layouts[0]
276 # Expected map: (1, 0, -1)289 # Expected map: (1, 0, -1)
277 self.assertEqual(output_layout.tensor_map, (1, 0, -1))290 self.assertEqual(output_layout.tensor_map, (1, 0, -1))
278 291 
292+ def test_embedding_preprocess_routes_platform_args(self):
293+ """
294+ Feature: New dispatch preprocessing.
295+ Description: Convert DTensor inputs to local tensors and cache layouts.
296+ Expectation: PyTorch embedding keeps sparse while MindSpore Embedding drops it.
297+ """
298+ input_layout = MagicMock()
299+ weight_layout = MagicMock()
300+ input_tensor = MagicMock()
301+ input_tensor.layout = input_layout
302+ input_tensor.to_local.return_value = "local_input"
303+ weight_tensor = MagicMock()
304+ weight_tensor.layout = weight_layout
305+ weight_tensor.to_local.return_value = "local_weight"
306+ 
307+ local_args, local_kwargs, cache_values = EmbeddingDistributedOp("embedding").preprocess(
308+ (input_tensor, weight_tensor), {}
309+ )
310+ self.assertEqual(local_args, ("local_input", "local_weight", None, None, 2.0, False, False))
311+ self.assertEqual(local_kwargs, {})
312+ self.assertEqual(cache_values, [input_layout, weight_layout])
313+ 
314+ local_args, local_kwargs, cache_values = EmbeddingDistributedOp("Embedding").preprocess(
315+ (input_tensor, weight_tensor), {}
316+ )
317+ self.assertEqual(local_args, ("local_input", "local_weight", None, None, 2.0, False))
318+ self.assertEqual(local_kwargs, {})
319+ self.assertEqual(cache_values, [input_layout, weight_layout])
320+ 
279 321 
280 322 
281 @patch("hyper_parallel.core.dtensor.device_mesh.platform")323 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
@@ -290,8 +332,9 @@ class TestParallelEmbedding(unittest.TestCase):
290 weight_layout = _build_layout(mesh, (Replicate(), Shard(1)), 2) # Column Parallel332 weight_layout = _build_layout(mesh, (Replicate(), Shard(1)), 2) # Column Parallel
291 333 
292 for op in embedding_ops:334 for op in embedding_ops:
293- output_layout = op.infer_layout((input_layout, weight_layout))335+ cache_values = [input_layout, weight_layout]
294- impl = op.get_expand_impl(MagicMock(), output_layout, (input_layout, weight_layout), None)336+ output_layouts, _ = op.infer_layout(cache_values)
337+ impl = op.get_expand_impl(MagicMock(), (output_layouts, None), cache_values)
295 338 
296 # Should not raise ValueError for scale_grad_by_freq339 # Should not raise ValueError for scale_grad_by_freq
297 try:340 try:
@@ -310,10 +353,12 @@ class TestEmbeddingRowParallelImpl(unittest.TestCase):
310 def setUp(self):353 def setUp(self):
311 EXISTING_COMM_GROUPS.clear()354 EXISTING_COMM_GROUPS.clear()
312 _DEVICE_MESH_MAP.clear()355 _DEVICE_MESH_MAP.clear()
356+ _LAYOUT_CACHE.clear()
313 357 
314 def tearDown(self):358 def tearDown(self):
315 EXISTING_COMM_GROUPS.clear()359 EXISTING_COMM_GROUPS.clear()
316 _DEVICE_MESH_MAP.clear()360 _DEVICE_MESH_MAP.clear()
361+ _LAYOUT_CACHE.clear()
317 362 
318 @patch("hyper_parallel.core.dtensor.device_mesh.platform")363 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
319 def test_row_parallel_max_norm_raises(self, mock_platform):364 def test_row_parallel_max_norm_raises(self, mock_platform):
@@ -330,9 +375,10 @@ class TestEmbeddingRowParallelImpl(unittest.TestCase):
330 weight_layout = _build_layout(mesh, (Shard(0),), 2)375 weight_layout = _build_layout(mesh, (Shard(0),), 2)
331 376 
332 for embedding_op in embedding_ops:377 for embedding_op in embedding_ops:
333- output_layout = embedding_op.infer_layout((input_layout, weight_layout))378+ cache_values = [input_layout, weight_layout]
379+ output_layouts, _ = embedding_op.infer_layout(cache_values)
334 impl = embedding_op.get_expand_impl(380 impl = embedding_op.get_expand_impl(
335- MagicMock(), output_layout, (input_layout, weight_layout), None381+ MagicMock(), (output_layouts, None), cache_values
336 )382 )
337 with self.assertRaisesRegex(ValueError, "Row-Parallel.*does not support.*max_norm"):383 with self.assertRaisesRegex(ValueError, "Row-Parallel.*does not support.*max_norm"):
338 impl(MagicMock(), MagicMock(), max_norm=1.0)384 impl(MagicMock(), MagicMock(), max_norm=1.0)
@@ -15,10 +15,10 @@
15"""parallel_expand_dims test"""15"""parallel_expand_dims test"""
16import os16import os
17import unittest17import unittest
18-from unittest.mock import patch18+from unittest.mock import MagicMock, patch
19import numpy as np19import numpy as np
20 20 
21-from hyper_parallel.core.dtensor.dtensor import _build_layout21+from hyper_parallel.core.dtensor.dtensor import _build_layout, _LAYOUT_CACHE
22from hyper_parallel.core.dtensor.placement_types import Shard, Replicate22from hyper_parallel.core.dtensor.placement_types import Shard, Replicate
23from hyper_parallel.core.shard.ops.parallel_expand_dims import ExpandDimsDistributedOp23from hyper_parallel.core.shard.ops.parallel_expand_dims import ExpandDimsDistributedOp
24from hyper_parallel.core.dtensor.device_mesh import (24from hyper_parallel.core.dtensor.device_mesh import (
@@ -38,12 +38,14 @@ class TestParallelExpandDims(unittest.TestCase):
38 """38 """
39 EXISTING_COMM_GROUPS.clear()39 EXISTING_COMM_GROUPS.clear()
40 _DEVICE_MESH_MAP.clear()40 _DEVICE_MESH_MAP.clear()
41+ _LAYOUT_CACHE.clear()
41 self.op = ExpandDimsDistributedOp("ExpandDims")42 self.op = ExpandDimsDistributedOp("ExpandDims")
42 43 
43 def tearDown(self):44 def tearDown(self):
44 """Clean up after each test method."""45 """Clean up after each test method."""
45 EXISTING_COMM_GROUPS.clear()46 EXISTING_COMM_GROUPS.clear()
46 _DEVICE_MESH_MAP.clear()47 _DEVICE_MESH_MAP.clear()
48+ _LAYOUT_CACHE.clear()
47 49 
48 def _setup_mock_platform(self, mock_platform, platform_type=None, world_size=8):50 def _setup_mock_platform(self, mock_platform, platform_type=None, world_size=8):
49 """Configure common mock-platform attributes used across tests.51 """Configure common mock-platform attributes used across tests.
@@ -67,14 +69,18 @@ class TestParallelExpandDims(unittest.TestCase):
67 return init_device_mesh(device_type="npu", mesh_shape=(2, 2, 2), mesh_dim_names=mesh_dim_names)69 return init_device_mesh(device_type="npu", mesh_shape=(2, 2, 2), mesh_dim_names=mesh_dim_names)
68 70 
69 def _run_scenario(self, x_layout, expected_map, extra_args):71 def _run_scenario(self, x_layout, expected_map, extra_args):
70- """Infer layout of ExpandDims operator"""72+ """Infer layout of ExpandDims operator."""
71- output_layout = self.op.infer_layout((x_layout,), extra_args)73+ axis = extra_args[0] if isinstance(extra_args, (tuple, list)) else extra_args
74+ cache_values = [x_layout, axis]
75+ output_layouts, extra_info = self.op.infer_layout(cache_values)
76+ output_layout = output_layouts[0]
72 assert output_layout.tensor_map == expected_map, (77 assert output_layout.tensor_map == expected_map, (
73 f"ExpandDims failed. Expected {expected_map}, got {output_layout.tensor_map}"78 f"ExpandDims failed. Expected {expected_map}, got {output_layout.tensor_map}"
74 )79 )
75- assert self.op.get_expand_impl(None, output_layout, (x_layout,), extra_args) is None, (80+ assert extra_info is None, f"ExpandDims extra_info should be None, got {extra_info}"
81+ assert self.op.get_expand_impl(None, (output_layouts, None), cache_values) is None, (
76 f"ExpandDims get_expand_impl failed. Expected None, "82 f"ExpandDims get_expand_impl failed. Expected None, "
77- f"got {self.op.get_expand_impl(None, output_layout, (x_layout,), extra_args)}"83+ f"got {self.op.get_expand_impl(None, (output_layouts, None), cache_values)}"
78 )84 )
79 85 
80 @patch("hyper_parallel.core.dtensor.device_mesh.platform")86 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
@@ -206,7 +212,34 @@ class TestParallelExpandDims(unittest.TestCase):
206 x_layout = _build_layout(mesh, x_placements, 3)212 x_layout = _build_layout(mesh, x_placements, 3)
207 213 
208 with self.assertRaisesRegex(ValueError, "out of range for input rank"):214 with self.assertRaisesRegex(ValueError, "out of range for input rank"):
209- self.op.infer_layout((x_layout,), [5])215+ self.op.infer_layout([x_layout, 5])
216+ 
217+ @patch("hyper_parallel.core.dtensor.device_mesh.platform")
218+ def test_expanddims_partial_input_raises_error(self, mock_platform):
219+ """Verify ExpandDims rejects inputs with Partial status."""
220+ mesh = self._make_2x2x2_mesh(mock_platform)
221+ x_layout = _build_layout(mesh, (Replicate(), Replicate(), Replicate()), 3)
222+ x_layout.set_partial_by_dev_axis("dp", "sum")
223+ 
224+ with self.assertRaisesRegex(ValueError, "Partial status"):
225+ self.op.infer_layout([x_layout, 1])
226+ 
227+ @patch("hyper_parallel.core.dtensor.device_mesh.platform")
228+ def test_expanddims_preprocess(self, mock_platform):
229+ """Verify preprocess converts DTensor input to local args and cache values."""
230+ mesh = self._make_2x2x2_mesh(mock_platform)
231+ x_layout = _build_layout(mesh, (Shard(0), Replicate(), Replicate()), 3)
232+ 
233+ mock_tensor = MagicMock()
234+ mock_tensor.layout = x_layout
235+ local_tensor = MagicMock()
236+ mock_tensor.to_local.return_value = local_tensor
237+ 
238+ local_args, local_kwargs, cache_values = self.op.preprocess((mock_tensor, 1), {})
239+ 
240+ assert local_args == (local_tensor, 1)
241+ assert not local_kwargs
242+ assert cache_values == [x_layout, 1]
210 243 
211 244 
212class TestParallelExpandDimsView(unittest.TestCase):245class TestParallelExpandDimsView(unittest.TestCase):
@@ -216,12 +249,14 @@ class TestParallelExpandDimsView(unittest.TestCase):
216 """Set up test fixtures before each test method."""249 """Set up test fixtures before each test method."""
217 EXISTING_COMM_GROUPS.clear()250 EXISTING_COMM_GROUPS.clear()
218 _DEVICE_MESH_MAP.clear()251 _DEVICE_MESH_MAP.clear()
252+ _LAYOUT_CACHE.clear()
219 self.op = ExpandDimsDistributedOp("ExpandDimsView")253 self.op = ExpandDimsDistributedOp("ExpandDimsView")
220 254 
221 def tearDown(self):255 def tearDown(self):
222 """Clean up after each test method."""256 """Clean up after each test method."""
223 EXISTING_COMM_GROUPS.clear()257 EXISTING_COMM_GROUPS.clear()
224 _DEVICE_MESH_MAP.clear()258 _DEVICE_MESH_MAP.clear()
259+ _LAYOUT_CACHE.clear()
225 260 
226 def _setup_mock_platform(self, mock_platform, platform_type=None, world_size=8):261 def _setup_mock_platform(self, mock_platform, platform_type=None, world_size=8):
227 """Configure common mock-platform attributes used across tests."""262 """Configure common mock-platform attributes used across tests."""
@@ -240,13 +275,17 @@ class TestParallelExpandDimsView(unittest.TestCase):
240 275 
241 def _run_scenario(self, x_layout, expected_map, extra_args):276 def _run_scenario(self, x_layout, expected_map, extra_args):
242 """Infer layout of ExpandDimsView operator."""277 """Infer layout of ExpandDimsView operator."""
243- output_layout = self.op.infer_layout((x_layout,), extra_args)278+ axis = extra_args[0] if isinstance(extra_args, (tuple, list)) else extra_args
279+ cache_values = [x_layout, axis]
280+ output_layouts, extra_info = self.op.infer_layout(cache_values)
281+ output_layout = output_layouts[0]
244 assert output_layout.tensor_map == expected_map, (282 assert output_layout.tensor_map == expected_map, (
245 f"ExpandDimsView failed. Expected {expected_map}, got {output_layout.tensor_map}"283 f"ExpandDimsView failed. Expected {expected_map}, got {output_layout.tensor_map}"
246 )284 )
247- assert self.op.get_expand_impl(None, output_layout, (x_layout,), extra_args) is None, (285+ assert extra_info is None, f"ExpandDimsView extra_info should be None, got {extra_info}"
286+ assert self.op.get_expand_impl(None, (output_layouts, None), cache_values) is None, (
248 f"ExpandDimsView get_expand_impl failed. Expected None, "287 f"ExpandDimsView get_expand_impl failed. Expected None, "
249- f"got {self.op.get_expand_impl(None, output_layout, (x_layout,), extra_args)}"288+ f"got {self.op.get_expand_impl(None, (output_layouts, None), cache_values)}"
250 )289 )
251 290 
252 @patch("hyper_parallel.core.dtensor.device_mesh.platform")291 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
@@ -378,7 +417,34 @@ class TestParallelExpandDimsView(unittest.TestCase):
378 x_layout = _build_layout(mesh, x_placements, 3)417 x_layout = _build_layout(mesh, x_placements, 3)
379 418 
380 with self.assertRaisesRegex(ValueError, "out of range for input rank"):419 with self.assertRaisesRegex(ValueError, "out of range for input rank"):
381- self.op.infer_layout((x_layout,), [5])420+ self.op.infer_layout([x_layout, 5])
421+ 
422+ @patch("hyper_parallel.core.dtensor.device_mesh.platform")
423+ def test_expanddims_partial_input_raises_error(self, mock_platform):
424+ """Verify ExpandDims rejects inputs with Partial status."""
425+ mesh = self._make_2x2x2_mesh(mock_platform)
426+ x_layout = _build_layout(mesh, (Replicate(), Replicate(), Replicate()), 3)
427+ x_layout.set_partial_by_dev_axis("dp", "sum")
428+ 
429+ with self.assertRaisesRegex(ValueError, "Partial status"):
430+ self.op.infer_layout([x_layout, 1])
431+ 
432+ @patch("hyper_parallel.core.dtensor.device_mesh.platform")
433+ def test_expanddims_preprocess(self, mock_platform):
434+ """Verify preprocess converts DTensor input to local args and cache values."""
435+ mesh = self._make_2x2x2_mesh(mock_platform)
436+ x_layout = _build_layout(mesh, (Shard(0), Replicate(), Replicate()), 3)
437+ 
438+ mock_tensor = MagicMock()
439+ mock_tensor.layout = x_layout
440+ local_tensor = MagicMock()
441+ mock_tensor.to_local.return_value = local_tensor
442+ 
443+ local_args, local_kwargs, cache_values = self.op.preprocess((mock_tensor, 1), {})
444+ 
445+ assert local_args == (local_tensor, 1)
446+ assert not local_kwargs
447+ assert cache_values == [x_layout, 1]
382 448 
383 449 
384if __name__ == "__main__":450if __name__ == "__main__":
@@ -15,10 +15,10 @@
15"""parallel_flatten test"""15"""parallel_flatten test"""
16import os16import os
17import unittest17import unittest
18-from unittest.mock import patch18+from unittest.mock import MagicMock, patch
19import numpy as np19import numpy as np
20 20 
21-from hyper_parallel.core.dtensor.dtensor import _build_layout21+from hyper_parallel.core.dtensor.dtensor import _build_layout, _LAYOUT_CACHE
22from hyper_parallel.core.dtensor.placement_types import Shard, Replicate22from hyper_parallel.core.dtensor.placement_types import Shard, Replicate
23from hyper_parallel.core.shard.ops.parallel_flatten import FlattenDistributedOp23from hyper_parallel.core.shard.ops.parallel_flatten import FlattenDistributedOp
24from hyper_parallel.core.dtensor.device_mesh import (24from hyper_parallel.core.dtensor.device_mesh import (
@@ -40,11 +40,13 @@ class TestParallelFlatten(unittest.TestCase):
40 """40 """
41 EXISTING_COMM_GROUPS.clear()41 EXISTING_COMM_GROUPS.clear()
42 _DEVICE_MESH_MAP.clear()42 _DEVICE_MESH_MAP.clear()
43+ _LAYOUT_CACHE.clear()
43 44 
44 def tearDown(self):45 def tearDown(self):
45 """Clean up after each test method."""46 """Clean up after each test method."""
46 EXISTING_COMM_GROUPS.clear()47 EXISTING_COMM_GROUPS.clear()
47 _DEVICE_MESH_MAP.clear()48 _DEVICE_MESH_MAP.clear()
49+ _LAYOUT_CACHE.clear()
48 50 
49 def _setup_mock_platform(self, mock_platform, platform_type=None, world_size=8):51 def _setup_mock_platform(self, mock_platform, platform_type=None, world_size=8):
50 """Configure common mock-platform attributes used across tests.52 """Configure common mock-platform attributes used across tests.
@@ -67,6 +69,33 @@ class TestParallelFlatten(unittest.TestCase):
67 self._setup_mock_platform(mock_platform, world_size=8)69 self._setup_mock_platform(mock_platform, world_size=8)
68 return init_device_mesh(device_type="npu", mesh_shape=(2, 4), mesh_dim_names=("dp", "mp"))70 return init_device_mesh(device_type="npu", mesh_shape=(2, 4), mesh_dim_names=("dp", "mp"))
69 71 
72+ def _infer_flatten(self, x_layout, input_shape, start_dim=0, end_dim=-1):
73+ """Infer flatten layout using the new cache_values calling convention."""
74+ output_layouts, extra_info = op.infer_layout([x_layout, start_dim, end_dim, input_shape])
75+ assert extra_info is None, f"Flatten extra_info should be None, got {extra_info}"
76+ return output_layouts[0]
77+ 
78+ @patch("hyper_parallel.core.dtensor.device_mesh.platform")
79+ def test_flatten_preprocess_builds_cache_values(self, mock_platform):
80+ """
81+ Feature: FlattenDistributedOp preprocess.
82+ Description: Verify that preprocess converts DTensor input to local tensor and caches layout/shape metadata.
83+ Expectation: local args, kwargs, and cache values follow the new dispatch convention.
84+ """
85+ mesh = self._make_2x4_mesh(mock_platform)
86+ x_layout = _build_layout(mesh, (Shard(0), Replicate()), 2)
87+ local_tensor = MagicMock()
88+ mock_tensor = MagicMock()
89+ mock_tensor.layout = x_layout
90+ mock_tensor.shape = (8, 4)
91+ mock_tensor.to_local.return_value = local_tensor
92+ 
93+ local_args, local_kwargs, cache_values = op.preprocess((mock_tensor, 1), {})
94+ 
95+ assert local_args == (local_tensor, 1, -1)
96+ assert not local_kwargs
97+ assert cache_values == [x_layout, 1, -1, (8, 4)]
98+ 
70 @patch("hyper_parallel.core.dtensor.device_mesh.platform")99 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
71 def test_flatten_layout_inference_all_dims(self, mock_platform):100 def test_flatten_layout_inference_all_dims(self, mock_platform):
72 """101 """
@@ -79,18 +108,19 @@ class TestParallelFlatten(unittest.TestCase):
79 x_placements = (Shard(0), Replicate(), Replicate())108 x_placements = (Shard(0), Replicate(), Replicate())
80 x_layout = _build_layout(mesh, x_placements, 3)109 x_layout = _build_layout(mesh, x_placements, 3)
81 110 
82- output_layout = op.infer_layout((x_layout,), extra_args=([(2, 4, 8)],))111+ output_layout = self._infer_flatten(x_layout, (2, 4, 8))
83 112 
84 expected_map = (1,)113 expected_map = (1,)
85 assert output_layout.to_dict()["tensor_map"] == expected_map, (114 assert output_layout.to_dict()["tensor_map"] == expected_map, (
86 f"Flatten all dims failed. Expected {expected_map}, got {output_layout.to_dict()['tensor_map']}"115 f"Flatten all dims failed. Expected {expected_map}, got {output_layout.to_dict()['tensor_map']}"
87 )116 )
88 117 
89- # Since `get_expand_impl` is not overridden, it returns None by default.118+ def fake_flatten(x, start_dim=0, end_dim=-1):
90- # The same applies to other test classes, so it is unnecessary to test its return value.119+ return x, start_dim, end_dim
91- assert op.get_expand_impl(None, output_layout, (x_layout,), ([(2, 4, 8)],)) is None, (120+ 
121+ assert op.get_expand_impl(fake_flatten, ((output_layout,), None), [x_layout, 0, -1, (2, 4, 8)]) is None, (
92 f"get_expand_impl test failed. Expected None, "122 f"get_expand_impl test failed. Expected None, "
93- f"got {op.get_expand_impl(None, output_layout, (x_layout,), ([(2, 4, 8)],))}"123+ f"got {op.get_expand_impl(fake_flatten, ((output_layout,), None), [x_layout, 0, -1, (2, 4, 8)])}"
94 )124 )
95 125 
96 @patch("hyper_parallel.core.dtensor.device_mesh.platform")126 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
@@ -105,7 +135,7 @@ class TestParallelFlatten(unittest.TestCase):
105 x_placements = (Shard(0), Shard(1), Replicate())135 x_placements = (Shard(0), Shard(1), Replicate())
106 x_layout = _build_layout(mesh, x_placements, 3)136 x_layout = _build_layout(mesh, x_placements, 3)
107 137 
108- output_layout = op.infer_layout((x_layout,), extra_args=(1, [(2, 4, 8)]))138+ output_layout = self._infer_flatten(x_layout, (2, 4, 8), start_dim=1)
109 139 
110 expected_map = (1, 0)140 expected_map = (1, 0)
111 assert output_layout.to_dict()["tensor_map"] == expected_map, (141 assert output_layout.to_dict()["tensor_map"] == expected_map, (
@@ -123,7 +153,7 @@ class TestParallelFlatten(unittest.TestCase):
123 x_placements = ()153 x_placements = ()
124 x_layout = _build_layout(mesh, x_placements, 0)154 x_layout = _build_layout(mesh, x_placements, 0)
125 155 
126- output_layout = op.infer_layout((x_layout,), extra_args=([()],))156+ output_layout = self._infer_flatten(x_layout, ())
127 157 
128 expected_map = (-1,)158 expected_map = (-1,)
129 assert output_layout.to_dict()["tensor_map"] == expected_map, (159 assert output_layout.to_dict()["tensor_map"] == expected_map, (
@@ -141,7 +171,7 @@ class TestParallelFlatten(unittest.TestCase):
141 x_placements = (Shard(0), Replicate())171 x_placements = (Shard(0), Replicate())
142 x_layout = _build_layout(mesh, x_placements, 2)172 x_layout = _build_layout(mesh, x_placements, 2)
143 173 
144- output_layout = op.infer_layout((x_layout,), extra_args=(1, 0, [(2, 4)]))174+ output_layout = self._infer_flatten(x_layout, (2, 4), start_dim=1, end_dim=0)
145 175 
146 expected_map = (1, -1)176 expected_map = (1, -1)
147 assert output_layout.to_dict()["tensor_map"] == expected_map, (177 assert output_layout.to_dict()["tensor_map"] == expected_map, (
@@ -160,7 +190,7 @@ class TestParallelFlatten(unittest.TestCase):
160 x_layout = _build_layout(mesh, x_placements, 2)190 x_layout = _build_layout(mesh, x_placements, 2)
161 191 
162 with self.assertRaises(ValueError):192 with self.assertRaises(ValueError):
163- op.infer_layout((x_layout,), extra_args=(2, [(2, 4)]))193+ op.infer_layout([x_layout, 2, -1, (2, 4)])
164 194 
165 195 
166if __name__ == "__main__":196if __name__ == "__main__":
@@ -439,19 +439,25 @@ class TestParallelGatherNd(unittest.TestCase):
439 439 
440 @patch("hyper_parallel.core.dtensor.device_mesh.platform")440 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
441 def test_gathernd_preprocess_packed_call(self, mock_platform):441 def test_gathernd_preprocess_packed_call(self, mock_platform):
442- """Verify GatherNd preprocess preserves MindSpore packed fallback calls."""442+ """Verify GatherNd preprocess receives clean unpacked args.
443+ 
444+ NOTE: aclop packed-args normalization now happens upstream in
445+ ``OpDispatcher._dispatch_layout_infer`` via ``_normalize_aclop_args``.
446+ The re-packing for the kernel call is handled by ``_dispatch_new``.
447+ This test validates that ``preprocess`` works correctly with
448+ the already-unpacked (input, indices) args.
449+ """
443 mesh = self._make_1d_mesh(mock_platform, world_size=8)450 mesh = self._make_1d_mesh(mock_platform, world_size=8)
444 input_layout = _build_layout(mesh, (Replicate(), Replicate()), 2)451 input_layout = _build_layout(mesh, (Replicate(), Replicate()), 2)
445 indices_layout = _build_layout(mesh, (Shard(0), Replicate()), 2)452 indices_layout = _build_layout(mesh, (Shard(0), Replicate()), 2)
446 input_tensor = _MockDTensor(input_layout, local_value="input", shape=(16, 64))453 input_tensor = _MockDTensor(input_layout, local_value="input", shape=(16, 64))
447 indices = _MockDTensor(indices_layout, local_value="indices", shape=(16, 2))454 indices = _MockDTensor(indices_layout, local_value="indices", shape=(16, 2))
448- prim = object()
449 455 
450 local_args, local_kwargs, cache_values = self.op.preprocess(456 local_args, local_kwargs, cache_values = self.op.preprocess(
451- (prim, "GatherNd", (input_tensor, indices)), {}457+ (input_tensor, indices), {}
452 )458 )
453 459 
454- assert local_args == (prim, "GatherNd", ("input", "indices"))460+ assert local_args == ("input", "indices")
455 assert not local_kwargs461 assert not local_kwargs
456 assert cache_values == [input_layout, indices_layout, (16, 64), (16, 2)]462 assert cache_values == [input_layout, indices_layout, (16, 64), (16, 2)]
457 463 
@@ -1,218 +0,0 @@
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_multinomial test"""
16-import os
17-import unittest
18-from unittest.mock import patch
19-import numpy as np
20- 
21-from hyper_parallel.core.dtensor.dtensor import _build_layout
22-from hyper_parallel.core.dtensor.placement_types import Shard, Replicate
23-from hyper_parallel.core.shard.ops.parallel_multinomial import MultinomialDistributedOp
24-from hyper_parallel.core.dtensor.device_mesh import (
25- init_device_mesh,
26- _DEVICE_MESH_MAP
27-)
28-from hyper_parallel.platform.platform import EXISTING_COMM_GROUPS
29- 
30-op = MultinomialDistributedOp("multinomial")
31- 
32- 
33-class TestParallelMultinomial(unittest.TestCase):
34- """Unit tests for MultinomialDistributedOp."""
35- def setUp(self):
36- """Set up test fixtures before each test method.
37- 
38- Clears global caches to ensure test isolation and initializes
39- the platform for testing.
40- """
41- EXISTING_COMM_GROUPS.clear()
42- _DEVICE_MESH_MAP.clear()
43- 
44- def tearDown(self):
45- """Clean up after each test method."""
46- EXISTING_COMM_GROUPS.clear()
47- _DEVICE_MESH_MAP.clear()
48- 
49- def _setup_mock_platform(self, mock_platform, platform_type=None, world_size=8):
50- """Configure common mock-platform attributes used across tests.
51- 
52- Args:
53- mock_platform: The MagicMock object injected by @patch.
54- platform_type: Optional PlatformType to set on the mock.
55- world_size: Value returned by mock_platform.get_world_size().
56- """
57- if platform_type is not None:
58- mock_platform.platform_type = platform_type
59- mock_platform.get_rank.return_value = 0
60- mock_platform.get_world_size.return_value = world_size
61- mock_platform.tensor_to_numpy.side_effect = (
62- lambda t: t.numpy() if hasattr(t, "numpy") else np.array(t)
63- )
64- 
65- def _make_2x4_mesh(self, mock_platform):
66- """Set up mock and return a standard 2x4 (dp, mp) mesh via init_device_mesh."""
67- self._setup_mock_platform(mock_platform, world_size=8)
68- return init_device_mesh(device_type="npu", mesh_shape=(2, 4), mesh_dim_names=("dp", "mp"))
69- 
70- @patch("hyper_parallel.core.dtensor.device_mesh.platform")
71- def test_multinomial_infer_layout_1d_replicated(self, mock_platform):
72- """
73- Feature: Multinomial layout inference for 1D input
74- Description: Input is a 1D probability vector (replicated).
75- Expectation: Output layout should be 1D and Replicated ("None").
76- """
77- mesh = self._make_2x4_mesh(mock_platform)
78- x_placements = (Replicate(), Replicate())
79- x_layout = _build_layout(mesh, x_placements, 1)
80- extra_args = (10, True, None)
81- 
82- output_layout = op.infer_layout((x_layout,), extra_args)
83- 
84- expected_map = (-1,)
85- assert output_layout.tensor_map == expected_map, (
86- f"1D inference failed. Expected {expected_map}, "
87- f"got {output_layout.tensor_map}"
88- )
89- 
90- # Since `get_expand_impl` is not overridden, it returns None by default.
91- # The same applies to other test classes, so it is unnecessary to test its return value.
92- assert op.get_expand_impl(None, output_layout, (x_layout,), extra_args) is None, (
93- f"get_expand_impl should return None, "
94- f"but got {output_layout}"
95- )
96- 
97- @patch("hyper_parallel.core.dtensor.device_mesh.platform")
98- def test_multinomial_infer_layout_2d_data_parallel(self, mock_platform):
99- """
100- Feature: Multinomial layout inference for 2D input (Data Parallel)
101- Description: Input (N, C) is sharded on Batch dim (dim 0), C dim (dim 1) is replicated.
102- Expectation: Output (N, num_samples) preserves sharding on dim 0, dim 1 is Replicated.
103- """
104- mesh = self._make_2x4_mesh(mock_platform)
105- x_placements = (Shard(0), Replicate())
106- x_layout = _build_layout(mesh, x_placements, 2)
107- 
108- extra_args = (10, True, None)
109- output_layout = op.infer_layout((x_layout,), extra_args=extra_args)
110- 
111- expected_map = (1, -1)
112- assert output_layout.tensor_map == expected_map, (
113- f"2D Data Parallel inference failed. Expected {expected_map}, "
114- f"got {output_layout.tensor_map}"
115- )
116- 
117- @patch("hyper_parallel.core.dtensor.device_mesh.platform")
118- def test_multinomial_infer_layout_2d_fully_replicated(self, mock_platform):
119- """
120- Feature: Multinomial layout inference for 2D input (Fully Replicated)
121- Description: Input (N, C) is fully replicated.
122- Expectation: Output (N, num_samples) is fully replicated.
123- """
124- mesh = self._make_2x4_mesh(mock_platform)
125- x_placements = (Replicate(), Replicate())
126- x_layout = _build_layout(mesh, x_placements, 2)
127- 
128- extra_args = (10, True, None)
129- output_layout = op.infer_layout((x_layout,), extra_args=extra_args)
130- 
131- expected_map = (-1, -1)
132- assert output_layout.tensor_map == expected_map, (
133- f"2D Replicated inference failed. Expected {expected_map}, "
134- f"got {output_layout.tensor_map}"
135- )
136- 
137- @patch("hyper_parallel.core.dtensor.device_mesh.platform")
138- def test_multinomial_error_sharded_prob_dim(self, mock_platform):
139- """
140- Feature: Error handling for sharded probability dimension
141- Description: Attempt to run multinomial when the last dimension (probability classes) is sharded.
142- Expectation: ValueError raised requiring redistribution.
143- """
144- mesh = self._make_2x4_mesh(mock_platform)
145- x_placements = (Replicate(), Shard(1))
146- x_layout = _build_layout(mesh, x_placements, 2)
147- 
148- assert x_layout.alias_tensor_map == ("None", "mp")
149- 
150- with self.assertRaisesRegex(ValueError, "must not be sharded"):
151- op.infer_layout((x_layout,), extra_args=(10, True, None))
152- 
153- @patch("hyper_parallel.core.dtensor.device_mesh.platform")
154- def test_multinomial_error_invalid_ndim_3d(self, mock_platform):
155- """
156- Feature: Error handling for invalid input dimensions
157- Description: Attempt to run multinomial on a 3D tensor.
158- Expectation: ValueError raised.
159- """
160- mesh = self._make_2x4_mesh(mock_platform)
161- x_placements = (Replicate(), Replicate())
162- x_layout = _build_layout(mesh, x_placements, 3)
163- 
164- with self.assertRaisesRegex(ValueError, "input dimension must be 1 or 2"):
165- op.infer_layout((x_layout,), extra_args=(10, True, None))
166- 
167- @patch("hyper_parallel.core.dtensor.device_mesh.platform")
168- def test_multinomial_error_invalid_ndim_0d(self, mock_platform):
169- """
170- Feature: Error handling for 0D input
171- Description: Attempt to run multinomial on a 0D tensor (scalar).
172- Expectation: ValueError raised.
173- """
174- mesh = self._make_2x4_mesh(mock_platform)
175- x_placements = (Replicate(), Replicate())
176- x_layout = _build_layout(mesh, x_placements, 0)
177- 
178- with self.assertRaisesRegex(ValueError, "input dimension must be 1 or 2"):
179- op.infer_layout((x_layout,), extra_args=(10, True, None))
180- 
181- @patch("hyper_parallel.core.dtensor.device_mesh.platform")
182- def test_multinomial_2d_with_replacement_false(self, mock_platform):
183- """
184- Feature: Multinomial with replacement=False
185- Description: Test 2D input with replacement=False.
186- Expectation: Output layout follows same sharding rules.
187- """
188- mesh = self._make_2x4_mesh(mock_platform)
189- x_placements = (Shard(0), Replicate())
190- x_layout = _build_layout(mesh, x_placements, 2)
191- 
192- extra_args = (5, False, None)
193- output_layout = op.infer_layout((x_layout,), extra_args=extra_args)
194- 
195- expected_map = (1, -1)
196- assert output_layout.tensor_map == expected_map, (
197- f"2D with replacement=False inference failed. Expected {expected_map}, "
198- f"got {output_layout.tensor_map}"
199- )
200- 
201- @patch("hyper_parallel.core.dtensor.device_mesh.platform")
202- def test_multinomial_partial_input(self, mock_platform):
203- """
204- Feature: Multinomial with partial input
205- Description: Input with partial state
206- Expectation: Raise ValueError since _allow_partial_inputs is False
207- """
208- mesh = self._make_2x4_mesh(mock_platform)
209- x_placements = (Shard(0), Replicate())
210- x_layout = _build_layout(mesh, x_placements, 2)
211- x_layout.set_partial_by_dev_axis("mp", "sum")
212- 
213- with self.assertRaisesRegex(ValueError, "has Partial status which is not allowed"):
214- op.infer_layout((x_layout,), extra_args=(10, True, None))
215- 
216- 
217-if __name__ == "__main__":
218- unittest.main()
@@ -13,14 +13,17 @@
13# limitations under the License.13# limitations under the License.
14# ============================================================================14# ============================================================================
15"""parallel_repeat_interleave test"""15"""parallel_repeat_interleave test"""
16-import os
17import unittest16import unittest
18-from unittest.mock import patch17+from unittest.mock import MagicMock, patch
18+ 
19import numpy as np19import numpy as np
20 20 
21-from hyper_parallel.core.dtensor.dtensor import _build_layout21+from hyper_parallel.core.dtensor.dtensor import _build_layout, _LAYOUT_CACHE
22-from hyper_parallel.core.dtensor.placement_types import Shard, Replicate22+from hyper_parallel.core.dtensor.placement_types import Partial, Shard, Replicate
23-from hyper_parallel.core.shard.ops.parallel_repeat_interleave import RepeatInterleaveDistributedOp23+from hyper_parallel.core.shard.ops.parallel_repeat_interleave import (
24+ RepeatInterleaveDistributedOp,
25+ _normalize_repeat_interleave_args,
26+)
24from hyper_parallel.core.dtensor.device_mesh import (27from hyper_parallel.core.dtensor.device_mesh import (
25 init_device_mesh,28 init_device_mesh,
26 _DEVICE_MESH_MAP29 _DEVICE_MESH_MAP
@@ -32,15 +35,18 @@ op = RepeatInterleaveDistributedOp("repeat_interleave")
32 35 
33class TestParallelRepeatInterleave(unittest.TestCase):36class TestParallelRepeatInterleave(unittest.TestCase):
34 """Unit tests for RepeatInterleaveDistributedOp."""37 """Unit tests for RepeatInterleaveDistributedOp."""
35- def setUp(self):38+ 
39+ def setUp(self) -> None:
36 """Set up test fixtures before each test method."""40 """Set up test fixtures before each test method."""
37 EXISTING_COMM_GROUPS.clear()41 EXISTING_COMM_GROUPS.clear()
38 _DEVICE_MESH_MAP.clear()42 _DEVICE_MESH_MAP.clear()
43+ _LAYOUT_CACHE.clear()
39 44 
40- def tearDown(self):45+ def tearDown(self) -> None:
41 """Clean up after each test method."""46 """Clean up after each test method."""
42 EXISTING_COMM_GROUPS.clear()47 EXISTING_COMM_GROUPS.clear()
43 _DEVICE_MESH_MAP.clear()48 _DEVICE_MESH_MAP.clear()
49+ _LAYOUT_CACHE.clear()
44 50 
45 def _setup_mock_platform(self, mock_platform, platform_type=None, world_size=8):51 def _setup_mock_platform(self, mock_platform, platform_type=None, world_size=8):
46 """Configure common mock-platform attributes used across tests."""52 """Configure common mock-platform attributes used across tests."""
@@ -67,21 +73,22 @@ class TestParallelRepeatInterleave(unittest.TestCase):
67 mesh = self._make_2x4_mesh(mock_platform)73 mesh = self._make_2x4_mesh(mock_platform)
68 x_placements = (Shard(0), Replicate())74 x_placements = (Shard(0), Replicate())
69 x_layout = _build_layout(mesh, x_placements, 2)75 x_layout = _build_layout(mesh, x_placements, 2)
70- repeats = 2
71 dim = 176 dim = 1
72- output_layout = op.infer_layout((x_layout,), (repeats, dim))77+ cache_values = [x_layout, dim]
78+ result = op.infer_layout(cache_values)
79+ output_layout = result[0][0]
73 80 
74 expected_map = (1, -1)81 expected_map = (1, -1)
75 assert output_layout.to_dict()["tensor_map"] == expected_map, (82 assert output_layout.to_dict()["tensor_map"] == expected_map, (
76- f"Data Parallel with torch repeat_interleave test failed. Expected {expected_map},"83+ f"Data Parallel with torch repeat_interleave test failed. Expected {expected_map}, "
77- f" got {output_layout.to_dict()['tensor_map']}"84+ f"got {output_layout.to_dict()['tensor_map']}"
78 )85 )
79 86 
80 # Since `get_expand_impl` is not overridden, it returns None by default.87 # Since `get_expand_impl` is not overridden, it returns None by default.
81 # The same applies to other test classes, so it is unnecessary to test its return value.88 # The same applies to other test classes, so it is unnecessary to test its return value.
82- assert op.get_expand_impl(None, output_layout, (x_layout,), (repeats, dim)) is None, (89+ assert op.get_expand_impl(None, result, cache_values) is None, (
83 f"get_expand_impl test failed. Expected None, "90 f"get_expand_impl test failed. Expected None, "
84- f"got {op.get_expand_impl(None, output_layout, (x_layout,), (repeats, dim))}"91+ f"got {op.get_expand_impl(None, result, cache_values)}"
85 )92 )
86 93 
87 @patch("hyper_parallel.core.dtensor.device_mesh.platform")94 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
@@ -94,13 +101,14 @@ class TestParallelRepeatInterleave(unittest.TestCase):
94 mesh = self._make_2x4_mesh(mock_platform)101 mesh = self._make_2x4_mesh(mock_platform)
95 x_placements = (Replicate(), Shard(0))102 x_placements = (Replicate(), Shard(0))
96 x_layout = _build_layout(mesh, x_placements, 2)103 x_layout = _build_layout(mesh, x_placements, 2)
97- repeats = 2
98 dim = 1104 dim = 1
99- output_layout = op.infer_layout((x_layout,), (repeats, dim))105+ cache_values = [x_layout, dim]
106+ result = op.infer_layout(cache_values)
107+ output_layout = result[0][0]
100 expected_map = (0, -1)108 expected_map = (0, -1)
101 assert output_layout.to_dict()["tensor_map"] == expected_map, (109 assert output_layout.to_dict()["tensor_map"] == expected_map, (
102- f"Tensor Parallel with torch repeat_interleave test failed. Expected {expected_map},"110+ f"Tensor Parallel with torch repeat_interleave test failed. Expected {expected_map}, "
103- f" got {output_layout.to_dict()['tensor_map']}"111+ f"got {output_layout.to_dict()['tensor_map']}"
104 )112 )
105 113 
106 @patch("hyper_parallel.core.dtensor.device_mesh.platform")114 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
@@ -113,14 +121,14 @@ class TestParallelRepeatInterleave(unittest.TestCase):
113 mesh = self._make_2x4_mesh(mock_platform)121 mesh = self._make_2x4_mesh(mock_platform)
114 x_placements = (Shard(0), Replicate())122 x_placements = (Shard(0), Replicate())
115 x_layout = _build_layout(mesh, x_placements, 2)123 x_layout = _build_layout(mesh, x_placements, 2)
116- repeats_tensor = [2, 1, 1, 1]
117- 
118 dim = 1124 dim = 1
119- output_layout = op.infer_layout((x_layout,), (repeats_tensor, dim))125+ cache_values = [x_layout, dim]
126+ result = op.infer_layout(cache_values)
127+ output_layout = result[0][0]
120 expected_map = (1, -1)128 expected_map = (1, -1)
121 assert output_layout.to_dict()["tensor_map"] == expected_map, (129 assert output_layout.to_dict()["tensor_map"] == expected_map, (
122- f"Data Parallel with torch repeat_interleave test failed. Expected {expected_map},"130+ f"Data Parallel with torch repeat_interleave test failed. Expected {expected_map}, "
123- f" got {output_layout.to_dict()['tensor_map']}"131+ f"got {output_layout.to_dict()['tensor_map']}"
124 )132 )
125 133 
126 @patch("hyper_parallel.core.dtensor.device_mesh.platform")134 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
@@ -133,14 +141,14 @@ class TestParallelRepeatInterleave(unittest.TestCase):
133 mesh = self._make_2x4_mesh(mock_platform)141 mesh = self._make_2x4_mesh(mock_platform)
134 x_placements = (Replicate(), Shard(0))142 x_placements = (Replicate(), Shard(0))
135 x_layout = _build_layout(mesh, x_placements, 2)143 x_layout = _build_layout(mesh, x_placements, 2)
136- repeats_tensor = [2, 1, 1, 1]
137- 
138 dim = 1144 dim = 1
139- output_layout = op.infer_layout((x_layout,), (repeats_tensor, dim))145+ cache_values = [x_layout, dim]
146+ result = op.infer_layout(cache_values)
147+ output_layout = result[0][0]
140 expected_map = (0, -1)148 expected_map = (0, -1)
141 assert output_layout.to_dict()["tensor_map"] == expected_map, (149 assert output_layout.to_dict()["tensor_map"] == expected_map, (
142- f"Tensor Parallel with torch repeat_interleave test failed. Expected {expected_map},"150+ f"Tensor Parallel with torch repeat_interleave test failed. Expected {expected_map}, "
143- f" got {output_layout.to_dict()['tensor_map']}"151+ f"got {output_layout.to_dict()['tensor_map']}"
144 )152 )
145 153 
146 @patch("hyper_parallel.core.dtensor.device_mesh.platform")154 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
@@ -153,12 +161,13 @@ class TestParallelRepeatInterleave(unittest.TestCase):
153 mesh = self._make_2x4_mesh(mock_platform)161 mesh = self._make_2x4_mesh(mock_platform)
154 x_placements = (Shard(0), Replicate())162 x_placements = (Shard(0), Replicate())
155 x_layout = _build_layout(mesh, x_placements, 2)163 x_layout = _build_layout(mesh, x_placements, 2)
156- repeats = 2164+ cache_values = [x_layout, None]
157- output_layout = op.infer_layout((x_layout,), (repeats,))165+ result = op.infer_layout(cache_values)
166+ output_layout = result[0][0]
158 expected_map = (1,)167 expected_map = (1,)
159 assert output_layout.to_dict()["tensor_map"] == expected_map, (168 assert output_layout.to_dict()["tensor_map"] == expected_map, (
160- f"Data Parallel with dim None repeat_interleave test failed. Expected {expected_map},"169+ f"Data Parallel with dim None repeat_interleave test failed. Expected {expected_map}, "
161- f" got {output_layout.to_dict()['tensor_map']}"170+ f"got {output_layout.to_dict()['tensor_map']}"
162 )171 )
163 172 
164 @patch("hyper_parallel.core.dtensor.device_mesh.platform")173 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
@@ -171,15 +180,102 @@ class TestParallelRepeatInterleave(unittest.TestCase):
171 mesh = self._make_2x4_mesh(mock_platform)180 mesh = self._make_2x4_mesh(mock_platform)
172 x_placements = (Replicate(), Shard(0))181 x_placements = (Replicate(), Shard(0))
173 x_layout = _build_layout(mesh, x_placements, 2)182 x_layout = _build_layout(mesh, x_placements, 2)
174- 183+ cache_values = [x_layout, None]
175- repeats = 2184+ result = op.infer_layout(cache_values)
176- output_layout = op.infer_layout((x_layout,), (repeats,))185+ output_layout = result[0][0]
177 expected_map = (0,)186 expected_map = (0,)
178 assert output_layout.to_dict()["tensor_map"] == expected_map, (187 assert output_layout.to_dict()["tensor_map"] == expected_map, (
179- f"Tensor Parallel dim None repeat_interleave test failed. Expected {expected_map},"188+ f"Tensor Parallel dim None repeat_interleave test failed. Expected {expected_map}, "
180- f" got {output_layout.to_dict()['tensor_map']}"189+ f"got {output_layout.to_dict()['tensor_map']}"
181 )190 )
182 191 
192+ @patch("hyper_parallel.core.dtensor.device_mesh.platform")
193+ def test_torch_repeat_interleave_layout_hybrid_parallel(self, mock_platform):
194+ """
195+ Feature: RepeatInterleave hybrid parallel
196+ Description: Hybrid scenario with multiple input dimensions sharded and repeated dim replicated
197+ Expectation: Success
198+ """
199+ mesh = self._make_2x4_mesh(mock_platform)
200+ x_layout = _build_layout(mesh, ("dp", "None", "tp"), 3)
201+ dim = 1
202+ cache_values = [x_layout, dim]
203+ result = op.infer_layout(cache_values)
204+ output_layout = result[0][0]
205+ expected_map = (1, -1, 0)
206+ assert output_layout.to_dict()["tensor_map"] == expected_map, (
207+ f"Hybrid parallel repeat_interleave test failed. Expected {expected_map}, "
208+ f"got {output_layout.to_dict()['tensor_map']}"
209+ )
210+ 
211+ @patch("hyper_parallel.core.dtensor.device_mesh.platform")
212+ def test_torch_repeat_interleave_layout_all_replicated(self, mock_platform):
213+ """
214+ Feature: RepeatInterleave all replicated
215+ Description: All-replicated input keeps replicated layout after repeat_interleave
216+ Expectation: Success
217+ """
218+ mesh = self._make_2x4_mesh(mock_platform)
219+ x_placements = (Replicate(), Replicate())
220+ x_layout = _build_layout(mesh, x_placements, 2)
221+ dim = 1
222+ cache_values = [x_layout, dim]
223+ result = op.infer_layout(cache_values)
224+ output_layout = result[0][0]
225+ expected_map = (-1, -1)
226+ assert output_layout.to_dict()["tensor_map"] == expected_map, (
227+ f"All replicated repeat_interleave test failed. Expected {expected_map}, "
228+ f"got {output_layout.to_dict()['tensor_map']}"
229+ )
230+ 
231+ @patch("hyper_parallel.core.dtensor.device_mesh.platform")
232+ def test_torch_repeat_interleave_layout_negative_dim(self, mock_platform):
233+ """
234+ Feature: RepeatInterleave negative dim
235+ Description: Negative repeat dim is normalized before validating sharding
236+ Expectation: Success
237+ """
238+ mesh = self._make_2x4_mesh(mock_platform)
239+ x_placements = (Shard(0), Replicate())
240+ x_layout = _build_layout(mesh, x_placements, 2)
241+ dim = -1
242+ cache_values = [x_layout, dim]
243+ result = op.infer_layout(cache_values)
244+ output_layout = result[0][0]
245+ expected_map = (1, -1)
246+ assert output_layout.to_dict()["tensor_map"] == expected_map, (
247+ f"Negative dim repeat_interleave test failed. Expected {expected_map}, "
248+ f"got {output_layout.to_dict()['tensor_map']}"
249+ )
250+ 
251+ @patch("hyper_parallel.core.dtensor.device_mesh.platform")
252+ def test_torch_repeat_interleave_partial_input_error(self, mock_platform):
253+ """
254+ Feature: RepeatInterleave partial input validation
255+ Description: Partial input layout should be rejected before layout inference
256+ Expectation: Success
257+ """
258+ mesh = self._make_2x4_mesh(mock_platform)
259+ x_layout = _build_layout(mesh, (Partial(), Replicate()), 2)
260+ dim = 1
261+ cache_values = [x_layout, dim]
262+ with self.assertRaisesRegex(ValueError, "status which is not allowed"):
263+ op.infer_layout(cache_values)
264+ 
265+ @patch("hyper_parallel.core.dtensor.device_mesh.platform")
266+ def test_torch_repeat_interleave_strided_shard_repeat_dim_error(self, mock_platform):
267+ """
268+ Feature: RepeatInterleave StridedShard repeat dim validation
269+ Description: Repeat dim using tuple alias map should be treated as sharded
270+ Expectation: Success
271+ """
272+ mesh = self._make_2x4_mesh(mock_platform)
273+ x_layout = _build_layout(mesh, (("dp", "tp"), "None"), 2)
274+ dim = 0
275+ cache_values = [x_layout, dim]
276+ with self.assertRaisesRegex(ValueError, "repeat dimension should be replicated"):
277+ op.infer_layout(cache_values)
278+ 
183 @patch("hyper_parallel.core.dtensor.device_mesh.platform")279 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
184 def test_torch_repeat_interleave_layout_sharded_dim_error(self, mock_platform):280 def test_torch_repeat_interleave_layout_sharded_dim_error(self, mock_platform):
185 """281 """
@@ -190,11 +286,10 @@ class TestParallelRepeatInterleave(unittest.TestCase):
190 mesh = self._make_2x4_mesh(mock_platform)286 mesh = self._make_2x4_mesh(mock_platform)
191 x_placements = (Shard(0), Shard(1))287 x_placements = (Shard(0), Shard(1))
192 x_layout = _build_layout(mesh, x_placements, 2)288 x_layout = _build_layout(mesh, x_placements, 2)
193- 
194- repeats = 2
195 dim = 1289 dim = 1
196- with self.assertRaisesRegex(ValueError, "Cannot perform sharding on params along the chosen dim"):290+ cache_values = [x_layout, dim]
197- op.infer_layout((x_layout,), (repeats, dim))291+ with self.assertRaisesRegex(ValueError, "repeat dimension should be replicated"):
292+ op.infer_layout(cache_values)
198 293 
199 @patch("hyper_parallel.core.dtensor.device_mesh.platform")294 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
200 def test_torch_repeat_interleave_layout_error_dim_out_of_range(self, mock_platform):295 def test_torch_repeat_interleave_layout_error_dim_out_of_range(self, mock_platform):
@@ -206,12 +301,98 @@ class TestParallelRepeatInterleave(unittest.TestCase):
206 mesh = self._make_2x4_mesh(mock_platform)301 mesh = self._make_2x4_mesh(mock_platform)
207 x_placements = (Replicate(), Replicate())302 x_placements = (Replicate(), Replicate())
208 x_layout = _build_layout(mesh, x_placements, 2)303 x_layout = _build_layout(mesh, x_placements, 2)
209- 
210- repeats = 2
211 dim = 5304 dim = 5
305+ cache_values = [x_layout, dim]
306+ with self.assertRaisesRegex(ValueError, "dimension should be in"):
307+ op.infer_layout(cache_values)
212 308 
213- with self.assertRaisesRegex(ValueError, "Dimension out of range"):309+ def test_normalize_repeat_interleave_args_basic(self):
214- op.infer_layout((x_layout,), extra_args=(repeats, dim))310+ """
311+ Feature: Normalize repeat_interleave args
312+ Description: Verify normalize function returns correct structure
313+ Expectation: Success
314+ """
315+ mock_input = MagicMock()
316+ args, kwargs = _normalize_repeat_interleave_args(mock_input, 3, 1)
317+ assert args == (mock_input, 3, 1), (
318+ f"Normalize args mismatch: expected (mock_input, 3, 1), got {args}"
319+ )
320+ assert not kwargs, (
321+ f"Normalize kwargs mismatch: expected {{}}, got {kwargs}"
322+ )
323+ 
324+ def test_normalize_repeat_interleave_args_with_output_size(self):
325+ """
326+ Feature: Normalize repeat_interleave args with output_size
327+ Description: Verify output_size goes into kwargs
328+ Expectation: Success
329+ """
330+ mock_input = MagicMock()
331+ args, kwargs = _normalize_repeat_interleave_args(mock_input, 3, 1, output_size=10)
332+ assert args == (mock_input, 3, 1), (
333+ f"Normalize args mismatch: expected (mock_input, 3, 1), got {args}"
334+ )
335+ assert kwargs == {'output_size': 10}, (
336+ f"Normalize kwargs mismatch: expected {{'output_size': 10}}, got {kwargs}"
337+ )
338+ 
339+ @patch("hyper_parallel.core.dtensor.device_mesh.platform")
340+ def test_preprocess_with_int_repeats(self, mock_platform):
341+ """
342+ Feature: Preprocess with int repeats
343+ Description: Verify preprocess correctly handles int repeats
344+ Expectation: Success
345+ """
346+ mesh = self._make_2x4_mesh(mock_platform)
347+ x_placements = (Shard(0), Replicate())
348+ x_layout = _build_layout(mesh, x_placements, 2)
349+ 
350+ mock_input = MagicMock()
351+ mock_input.layout = x_layout
352+ mock_local = MagicMock()
353+ mock_input.to_local.return_value = mock_local
354+ 
355+ local_args, local_kwargs, cache_values = op.preprocess(
356+ (mock_input, 3, 1), {}
357+ )
358+ 
359+ assert local_args[0] is mock_local, (
360+ f"local_args[0] should be local input: expected={mock_local}, got={local_args[0]}"
361+ )
362+ assert local_args[1] == 3, f"local_args[1] should be 3, got {local_args[1]}"
363+ assert local_args[2] == 1, f"local_args[2] should be 1, got {local_args[2]}"
364+ assert not local_kwargs, f"local_kwargs should be empty, got {local_kwargs}"
365+ assert cache_values[0] is x_layout, (
366+ f"cache_values[0] should be input layout: expected={x_layout}, got={cache_values[0]}"
367+ )
368+ assert cache_values[1] == 1, f"cache_values[1] should be 1, got {cache_values[1]}"
369+ 
370+ @patch("hyper_parallel.core.dtensor.device_mesh.platform")
371+ def test_preprocess_dim_none(self, mock_platform):
372+ """
373+ Feature: Preprocess with dim=None
374+ Description: Verify preprocess correctly handles dim=None (flatten mode)
375+ Expectation: Success
376+ """
377+ mesh = self._make_2x4_mesh(mock_platform)
378+ x_placements = (Shard(0), Replicate())
379+ x_layout = _build_layout(mesh, x_placements, 2)
380+ 
381+ mock_input = MagicMock()
382+ mock_input.layout = x_layout
383+ mock_local = MagicMock()
384+ mock_input.to_local.return_value = mock_local
385+ 
386+ local_args, local_kwargs, cache_values = op.preprocess(
387+ (mock_input, 3, None), {}
388+ )
389+ 
390+ assert local_args[0] is mock_local, (
391+ f"local_args[0] should be local input: expected={mock_local}, got={local_args[0]}"
392+ )
393+ assert local_args[1] == 3, f"local_args[1] should be 3, got {local_args[1]}"
394+ assert local_args[2] is None, f"local_args[2] should be None, got {local_args[2]}"
395+ assert cache_values[1] is None, f"cache_values[1] should be None, got {cache_values[1]}"
215 396 
216 397 
217if __name__ == "__main__":398if __name__ == "__main__":
@@ -15,10 +15,10 @@
15"""parallel_reshape test"""15"""parallel_reshape test"""
16import os16import os
17import unittest17import unittest
18-from unittest.mock import patch18+from unittest.mock import MagicMock, patch
19import numpy as np19import numpy as np
20 20 
21-from hyper_parallel.core.dtensor.dtensor import _build_layout21+from hyper_parallel.core.dtensor.dtensor import _build_layout, _LAYOUT_CACHE
22from hyper_parallel.core.dtensor.placement_types import Shard, Replicate, Partial22from hyper_parallel.core.dtensor.placement_types import Shard, Replicate, Partial
23from hyper_parallel.core.shard.ops.parallel_reshape import ReshapeDistributedOp23from hyper_parallel.core.shard.ops.parallel_reshape import ReshapeDistributedOp
24from hyper_parallel.core.dtensor.device_mesh import (24from hyper_parallel.core.dtensor.device_mesh import (
@@ -42,11 +42,13 @@ class TestParallelReshape(unittest.TestCase):
42 """42 """
43 EXISTING_COMM_GROUPS.clear()43 EXISTING_COMM_GROUPS.clear()
44 _DEVICE_MESH_MAP.clear()44 _DEVICE_MESH_MAP.clear()
45+ _LAYOUT_CACHE.clear()
45 46 
46 def tearDown(self):47 def tearDown(self):
47 """Clean up after each test method."""48 """Clean up after each test method."""
48 EXISTING_COMM_GROUPS.clear()49 EXISTING_COMM_GROUPS.clear()
49 _DEVICE_MESH_MAP.clear()50 _DEVICE_MESH_MAP.clear()
51+ _LAYOUT_CACHE.clear()
50 52 
51 def _setup_mock_platform(self, mock_platform, platform_type=None, world_size=8):53 def _setup_mock_platform(self, mock_platform, platform_type=None, world_size=8):
52 """Configure common mock-platform attributes used across tests.54 """Configure common mock-platform attributes used across tests.
@@ -79,6 +81,47 @@ class TestParallelReshape(unittest.TestCase):
79 self._setup_mock_platform(mock_platform, world_size=8)81 self._setup_mock_platform(mock_platform, world_size=8)
80 return init_device_mesh(device_type="npu", mesh_shape=(8,), mesh_dim_names=(mesh_dim_name,))82 return init_device_mesh(device_type="npu", mesh_shape=(8,), mesh_dim_names=(mesh_dim_name,))
81 83 
84+ def _infer_reshape(self, reshape_op, x_layout, dst_shape, src_shape):
85+ """Infer reshape layout using the new cache_values calling convention."""
86+ output_layouts, local_dst_shape = reshape_op.infer_layout([x_layout, dst_shape, src_shape])
87+ return output_layouts[0], local_dst_shape
88+ 
89+ @patch("hyper_parallel.core.dtensor.device_mesh.platform")
90+ def test_reshape_preprocess_builds_cache_values(self, mock_platform):
91+ """
92+ Feature: ReshapeDistributedOp preprocess.
93+ Description: Verify that preprocess converts DTensor input to local tensor and caches layout/shape metadata.
94+ Expectation: local args, kwargs, and cache values follow the new dispatch convention.
95+ """
96+ mesh = self._make_2x4_mesh(mock_platform)
97+ x_layout = _build_layout(mesh, (Shard(0), Replicate()), 2)
98+ local_tensor = MagicMock()
99+ mock_tensor = MagicMock()
100+ mock_tensor.layout = x_layout
101+ mock_tensor.shape = (8, 4)
102+ mock_tensor.to_local.return_value = local_tensor
103+ 
104+ local_args, local_kwargs, cache_values = op.preprocess((mock_tensor, (4, 8)), {})
105+ 
106+ assert local_args == (local_tensor, (4, 8))
107+ assert not local_kwargs
108+ assert cache_values == [x_layout, (4, 8), (8, 4)]
109+ 
110+ def test_reshape_get_expand_impl_uses_local_shape(self):
111+ """
112+ Feature: ReshapeDistributedOp get_expand_impl.
113+ Description: Verify that the wrapper replaces global target shape with inferred local target shape.
114+ Expectation: Underlying function receives local_dst_shape.
115+ """
116+ infer_result = ((MagicMock(),), [2, 8])
117+ 
118+ def fake_reshape(x, shape):
119+ return x, shape
120+ 
121+ impl = op.get_expand_impl(fake_reshape, infer_result, [MagicMock(), (4, 8), (8, 4)])
122+ 
123+ assert impl("local_x", (4, 8)) == ("local_x", [2, 8])
124+ 
82 @patch("hyper_parallel.core.dtensor.device_mesh.platform")125 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
83 def test_reshape_layout_not_change_sharded_axis(self, mock_platform):126 def test_reshape_layout_not_change_sharded_axis(self, mock_platform):
84 """127 """
@@ -91,7 +134,7 @@ class TestParallelReshape(unittest.TestCase):
91 src_shape = (1024, 512, 512)134 src_shape = (1024, 512, 512)
92 dst_shape = (1024, 2, 256, 512)135 dst_shape = (1024, 2, 256, 512)
93 136 
94- output_layout, local_dst_shape = op.infer_layout((x_layout,), (dst_shape, src_shape))137+ output_layout, local_dst_shape = self._infer_reshape(op, x_layout, dst_shape, src_shape)
95 expected_map = (1, -1, -1, -1)138 expected_map = (1, -1, -1, -1)
96 assert output_layout.tensor_map == expected_map, (139 assert output_layout.tensor_map == expected_map, (
97 f"Reshape do not change sharded axis failed. Expected {expected_map}, "140 f"Reshape do not change sharded axis failed. Expected {expected_map}, "
@@ -103,11 +146,10 @@ class TestParallelReshape(unittest.TestCase):
103 f"got {local_dst_shape}"146 f"got {local_dst_shape}"
104 )147 )
105 148 
106- # Since `get_expand_impl` is not overridden, it returns None by default.149+ # If func is None in a unit-level call, no runtime wrapper is needed.
107- # The same applies to other test classes, so it is unnecessary to test its return value.150+ assert op.get_expand_impl(None, ((output_layout,), local_dst_shape), [x_layout, dst_shape, src_shape]) is None, (
108- assert op.get_expand_impl(None, output_layout, (x_layout,), (dst_shape, src_shape)) is None, (
109 f"get_expand_impl test failed. Expected None, "151 f"get_expand_impl test failed. Expected None, "
110- f"got {op.get_expand_impl(None, output_layout, (x_layout,), (dst_shape, src_shape))}"152+ f"got {op.get_expand_impl(None, ((output_layout,), local_dst_shape), [x_layout, dst_shape, src_shape])}"
111 )153 )
112 154 
113 @patch("hyper_parallel.core.dtensor.device_mesh.platform")155 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
@@ -122,7 +164,7 @@ class TestParallelReshape(unittest.TestCase):
122 src_shape = (4, 4, 8)164 src_shape = (4, 4, 8)
123 dst_shape = (16, 8)165 dst_shape = (16, 8)
124 166 
125- output_layout, local_dst_shape = op.infer_layout((x_layout,), (dst_shape, src_shape))167+ output_layout, local_dst_shape = self._infer_reshape(op, x_layout, dst_shape, src_shape)
126 expected_map = (1, -1)168 expected_map = (1, -1)
127 assert output_layout.tensor_map == expected_map, (169 assert output_layout.tensor_map == expected_map, (
128 f"Reshape do not change sharded axis failed. Expected {expected_map}, "170 f"Reshape do not change sharded axis failed. Expected {expected_map}, "
@@ -146,7 +188,7 @@ class TestParallelReshape(unittest.TestCase):
146 src_shape = (32, 128)188 src_shape = (32, 128)
147 dst_shape = (4, 8, 128)189 dst_shape = (4, 8, 128)
148 190 
149- output_layout, local_dst_shape = op.infer_layout((x_layout,), (dst_shape, src_shape))191+ output_layout, local_dst_shape = self._infer_reshape(op, x_layout, dst_shape, src_shape)
150 expected_map = (1, -1, -1)192 expected_map = (1, -1, -1)
151 assert output_layout.tensor_map == expected_map, (193 assert output_layout.tensor_map == expected_map, (
152 f"Reshape do not change sharded axis failed. Expected {expected_map}, "194 f"Reshape do not change sharded axis failed. Expected {expected_map}, "
@@ -170,7 +212,7 @@ class TestParallelReshape(unittest.TestCase):
170 src_shape = (32, 6, 128, 28, 10)212 src_shape = (32, 6, 128, 28, 10)
171 dst_shape = (4, 8, 2, 384, 280)213 dst_shape = (4, 8, 2, 384, 280)
172 214 
173- output_layout, local_dst_shape = op.infer_layout((x_layout,), (dst_shape, src_shape))215+ output_layout, local_dst_shape = self._infer_reshape(op, x_layout, dst_shape, src_shape)
174 expected_map = (0, -1, 2, -1, 1)216 expected_map = (0, -1, 2, -1, 1)
175 assert output_layout.tensor_map == expected_map, (217 assert output_layout.tensor_map == expected_map, (
176 f"Reshape do not change sharded axis failed. Expected {expected_map}, "218 f"Reshape do not change sharded axis failed. Expected {expected_map}, "
@@ -195,7 +237,7 @@ class TestParallelReshape(unittest.TestCase):
195 dst_shape = (4, 8, 12, 4)237 dst_shape = (4, 8, 12, 4)
196 238 
197 with self.assertRaises(ValueError):239 with self.assertRaises(ValueError):
198- _, _ = op.infer_layout((x_layout,), (dst_shape, src_shape))240+ _, _ = self._infer_reshape(op, x_layout, dst_shape, src_shape)
199 241 
200 @patch("hyper_parallel.core.dtensor.device_mesh.platform")242 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
201 def test_reshape_layout_can_not_reshape2(self, mock_platform):243 def test_reshape_layout_can_not_reshape2(self, mock_platform):
@@ -210,7 +252,7 @@ class TestParallelReshape(unittest.TestCase):
210 dst_shape = (4, 8, 2, 42)252 dst_shape = (4, 8, 2, 42)
211 253 
212 with self.assertRaises(ValueError):254 with self.assertRaises(ValueError):
213- _, _ = op.infer_layout((x_layout,), (dst_shape, src_shape))255+ _, _ = self._infer_reshape(op, x_layout, dst_shape, src_shape)
214 256 
215 @patch("hyper_parallel.core.dtensor.device_mesh.platform")257 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
216 def test_reshape_layout_dynamic_shape1(self, mock_platform):258 def test_reshape_layout_dynamic_shape1(self, mock_platform):
@@ -224,7 +266,7 @@ class TestParallelReshape(unittest.TestCase):
224 src_shape = (1024, -1, 256, 512)266 src_shape = (1024, -1, 256, 512)
225 dst_shape = (1024, -1, 512)267 dst_shape = (1024, -1, 512)
226 268 
227- output_layout, local_dst_shape = op.infer_layout((x_layout,), (dst_shape, src_shape))269+ output_layout, local_dst_shape = self._infer_reshape(op, x_layout, dst_shape, src_shape)
228 expected_map = (1, -1, -1)270 expected_map = (1, -1, -1)
229 assert output_layout.tensor_map == expected_map, (271 assert output_layout.tensor_map == expected_map, (
230 f"Reshape do not change sharded axis failed. Expected {expected_map}, "272 f"Reshape do not change sharded axis failed. Expected {expected_map}, "
@@ -248,7 +290,7 @@ class TestParallelReshape(unittest.TestCase):
248 src_shape = (1024, -1, 512)290 src_shape = (1024, -1, 512)
249 dst_shape = (1024, -1, 256, 512)291 dst_shape = (1024, -1, 256, 512)
250 292 
251- output_layout, local_dst_shape = op.infer_layout((x_layout,), (dst_shape, src_shape))293+ output_layout, local_dst_shape = self._infer_reshape(op, x_layout, dst_shape, src_shape)
252 expected_map = (1, -1, -1, -1)294 expected_map = (1, -1, -1, -1)
253 assert output_layout.tensor_map == expected_map, (295 assert output_layout.tensor_map == expected_map, (
254 f"Reshape do not change sharded axis failed. Expected {expected_map}, "296 f"Reshape do not change sharded axis failed. Expected {expected_map}, "
@@ -272,7 +314,7 @@ class TestParallelReshape(unittest.TestCase):
272 src_shape = (-1, 256, 512)314 src_shape = (-1, 256, 512)
273 dst_shape = (-1, 512)315 dst_shape = (-1, 512)
274 316 
275- output_layout, local_dst_shape = op.infer_layout((x_layout,), (dst_shape, src_shape))317+ output_layout, local_dst_shape = self._infer_reshape(op, x_layout, dst_shape, src_shape)
276 expected_map = (1, -1)318 expected_map = (1, -1)
277 assert output_layout.tensor_map == expected_map, (319 assert output_layout.tensor_map == expected_map, (
278 f"Reshape do not change sharded axis failed. Expected {expected_map}, "320 f"Reshape do not change sharded axis failed. Expected {expected_map}, "
@@ -297,7 +339,7 @@ class TestParallelReshape(unittest.TestCase):
297 dst_shape = (-1, 256, 512)339 dst_shape = (-1, 256, 512)
298 340 
299 with self.assertRaises(ValueError):341 with self.assertRaises(ValueError):
300- _, _ = op.infer_layout((x_layout,), (dst_shape, src_shape))342+ _, _ = self._infer_reshape(op, x_layout, dst_shape, src_shape)
301 343 
302 @patch("hyper_parallel.core.dtensor.device_mesh.platform")344 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
303 def test_torch_reshape_basic_split(self, mock_platform):345 def test_torch_reshape_basic_split(self, mock_platform):
@@ -312,7 +354,7 @@ class TestParallelReshape(unittest.TestCase):
312 src_shape = (32, 64)354 src_shape = (32, 64)
313 dst_shape = (32, 16, 4)355 dst_shape = (32, 16, 4)
314 356 
315- output_layout, local_dst_shape = op_torch.infer_layout((x_layout,), (dst_shape, src_shape))357+ output_layout, local_dst_shape = self._infer_reshape(op_torch, x_layout, dst_shape, src_shape)
316 358 
317 expected_map = (1, 0, -1)359 expected_map = (1, 0, -1)
318 360 
@@ -338,7 +380,7 @@ class TestParallelReshape(unittest.TestCase):
338 src_shape = (32, 64)380 src_shape = (32, 64)
339 dst_shape = (32, -1)381 dst_shape = (32, -1)
340 382 
341- output_layout, local_dst_shape = op_torch.infer_layout((x_layout,), (dst_shape, src_shape))383+ output_layout, local_dst_shape = self._infer_reshape(op_torch, x_layout, dst_shape, src_shape)
342 384 
343 expected_map = (1, 0)385 expected_map = (1, 0)
344 assert output_layout.tensor_map == expected_map386 assert output_layout.tensor_map == expected_map
@@ -360,11 +402,11 @@ class TestParallelReshape(unittest.TestCase):
360 src_shape = (16, 4, 8)402 src_shape = (16, 4, 8)
361 dst_shape = (64, 8)403 dst_shape = (64, 8)
362 404 
363- output_layout, _ = op_torch.infer_layout((x_layout,), (dst_shape, src_shape))405+ output_layout, _ = self._infer_reshape(op_torch, x_layout, dst_shape, src_shape)
364 assert output_layout.tensor_map == (1, 0)406 assert output_layout.tensor_map == (1, 0)
365 407 
366 dst_shape_2 = (16, 2, 2, 8)408 dst_shape_2 = (16, 2, 2, 8)
367- output_layout_2, _ = op_torch.infer_layout((x_layout,), (dst_shape_2, src_shape))409+ output_layout_2, _ = self._infer_reshape(op_torch, x_layout, dst_shape_2, src_shape)
368 assert output_layout_2.tensor_map == (1, -1, -1, 0)410 assert output_layout_2.tensor_map == (1, -1, -1, 0)
369 411 
370 @patch("hyper_parallel.core.dtensor.device_mesh.platform")412 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
@@ -379,8 +421,8 @@ class TestParallelReshape(unittest.TestCase):
379 421 
380 dst_shape = (32, 64)422 dst_shape = (32, 64)
381 423 
382- with self.assertRaisesRegex(ValueError, "reshape requires output shape and input shape."):424+ with self.assertRaisesRegex(ValueError, "cache_values length"):
383- op_torch.infer_layout((x_layout,), (dst_shape,))425+ op_torch.infer_layout([x_layout, dst_shape])
384 426 
385 @patch("hyper_parallel.core.dtensor.device_mesh.platform")427 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
386 def test_torch_reshape_fail_mismatch_total_size(self, mock_platform):428 def test_torch_reshape_fail_mismatch_total_size(self, mock_platform):
@@ -396,7 +438,7 @@ class TestParallelReshape(unittest.TestCase):
396 dst_shape = (20, 20)438 dst_shape = (20, 20)
397 439 
398 with self.assertRaises(ValueError):440 with self.assertRaises(ValueError):
399- op_torch.infer_layout((x_layout,), (dst_shape, src_shape))441+ self._infer_reshape(op_torch, x_layout, dst_shape, src_shape)
400 442 
401 @patch("hyper_parallel.core.dtensor.device_mesh.platform")443 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
402 def test_view_layout_flatten_contiguous(self, mock_platform):444 def test_view_layout_flatten_contiguous(self, mock_platform):
@@ -412,7 +454,7 @@ class TestParallelReshape(unittest.TestCase):
412 src_shape = (32, 16, 8)454 src_shape = (32, 16, 8)
413 dst_shape = (32, 128)455 dst_shape = (32, 128)
414 456 
415- output_layout, local_dst_shape = op_view.infer_layout((x_layout,), (dst_shape, src_shape))457+ output_layout, local_dst_shape = self._infer_reshape(op_view, x_layout, dst_shape, src_shape)
416 458 
417 expected_map = (1, 0)459 expected_map = (1, 0)
418 assert output_layout.tensor_map == expected_map, (460 assert output_layout.tensor_map == expected_map, (
@@ -436,7 +478,7 @@ class TestParallelReshape(unittest.TestCase):
436 src_shape = (32, 128)478 src_shape = (32, 128)
437 dst_shape = (32, 16, 8)479 dst_shape = (32, 16, 8)
438 480 
439- output_layout, local_dst_shape = op_view.infer_layout((x_layout,), (dst_shape, src_shape))481+ output_layout, local_dst_shape = self._infer_reshape(op_view, x_layout, dst_shape, src_shape)
440 482 
441 expected_map = (1, 0, -1)483 expected_map = (1, 0, -1)
442 assert output_layout.tensor_map == expected_map, (484 assert output_layout.tensor_map == expected_map, (
@@ -459,7 +501,7 @@ class TestParallelReshape(unittest.TestCase):
459 src_shape = (8, 4, 16)501 src_shape = (8, 4, 16)
460 dst_shape = (8, -1)502 dst_shape = (8, -1)
461 503 
462- output_layout, local_dst_shape = op_view.infer_layout((x_layout,), (dst_shape, src_shape))504+ output_layout, local_dst_shape = self._infer_reshape(op_view, x_layout, dst_shape, src_shape)
463 505 
464 expected_map = (1, 0)506 expected_map = (1, 0)
465 assert output_layout.tensor_map == expected_map507 assert output_layout.tensor_map == expected_map
@@ -481,7 +523,7 @@ class TestParallelReshape(unittest.TestCase):
481 dst_shape = (10, 30)523 dst_shape = (10, 30)
482 524 
483 with self.assertRaisesRegex(ValueError, "total elements number"):525 with self.assertRaisesRegex(ValueError, "total elements number"):
484- op_view.infer_layout((x_layout,), (dst_shape, src_shape))526+ self._infer_reshape(op_view, x_layout, dst_shape, src_shape)
485 527 
486 @patch("hyper_parallel.core.dtensor.device_mesh.platform")528 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
487 def test_reshape_with_partial_basic(self, mock_platform):529 def test_reshape_with_partial_basic(self, mock_platform):
@@ -498,7 +540,7 @@ class TestParallelReshape(unittest.TestCase):
498 src_shape = (8, 16)540 src_shape = (8, 16)
499 dst_shape = (4, 4, 8)541 dst_shape = (4, 4, 8)
500 542 
501- output_layout, _ = op.infer_layout((x_layout,), (dst_shape, src_shape))543+ output_layout, _ = self._infer_reshape(op, x_layout, dst_shape, src_shape)
502 544 
503 assert output_layout.is_partial(), "Output layout should preserve partial status"545 assert output_layout.is_partial(), "Output layout should preserve partial status"
504 assert output_layout.get_partial_by_dev_id("dp") == "sum", (546 assert output_layout.get_partial_by_dev_id("dp") == "sum", (
@@ -518,7 +560,7 @@ class TestParallelReshape(unittest.TestCase):
518 src_shape = (8, 16)560 src_shape = (8, 16)
519 dst_shape = (1, -1, 16)561 dst_shape = (1, -1, 16)
520 562 
521- output_layout, _ = op.infer_layout((x_layout,), (dst_shape, src_shape))563+ output_layout, _ = self._infer_reshape(op, x_layout, dst_shape, src_shape)
522 564 
523 assert output_layout.is_partial(), "Output layout should preserve partial status"565 assert output_layout.is_partial(), "Output layout should preserve partial status"
524 assert output_layout.get_partial_by_dev_id("mp") == "sum", (566 assert output_layout.get_partial_by_dev_id("mp") == "sum", (
@@ -538,7 +580,7 @@ class TestParallelReshape(unittest.TestCase):
538 src_shape = (4, 8)580 src_shape = (4, 8)
539 dst_shape = (2, 16)581 dst_shape = (2, 16)
540 582 
541- output_layout, _ = op.infer_layout((x_layout,), (dst_shape, src_shape))583+ output_layout, _ = self._infer_reshape(op, x_layout, dst_shape, src_shape)
542 584 
543 assert output_layout.is_partial(), "Output layout should preserve partial status"585 assert output_layout.is_partial(), "Output layout should preserve partial status"
544 assert output_layout.get_partial_by_dev_id("dp") == "sum"586 assert output_layout.get_partial_by_dev_id("dp") == "sum"
@@ -558,9 +600,7 @@ class TestParallelReshape(unittest.TestCase):
558 src_shape = (4, 8)600 src_shape = (4, 8)
559 dst_shape = (2, 16)601 dst_shape = (2, 16)
560 602 
561- output_layout, _ = op_torch.infer_layout(603+ output_layout, _ = self._infer_reshape(op_torch, x_layout, dst_shape, src_shape)
562- (x_layout,), (dst_shape, src_shape)
563- )
564 604 
565 assert output_layout.is_partial(), "Output layout should preserve partial status"605 assert output_layout.is_partial(), "Output layout should preserve partial status"
566 assert output_layout.get_partial_by_dev_id("dp") == "sum"606 assert output_layout.get_partial_by_dev_id("dp") == "sum"
@@ -577,7 +617,7 @@ class TestParallelReshape(unittest.TestCase):
577 src_shape = (2,)617 src_shape = (2,)
578 dst_shape = (-1,)618 dst_shape = (-1,)
579 619 
580- output_layout, local_dst_shape = op_torch.infer_layout((x_layout,), (dst_shape, src_shape))620+ output_layout, local_dst_shape = self._infer_reshape(op_torch, x_layout, dst_shape, src_shape)
581 expected_map = (-1,)621 expected_map = (-1,)
582 assert output_layout.tensor_map == expected_map, (622 assert output_layout.tensor_map == expected_map, (
583 f"Replicate 1D reshape to self failed. Expected {expected_map}, "623 f"Replicate 1D reshape to self failed. Expected {expected_map}, "
@@ -603,9 +643,7 @@ class TestParallelReshape(unittest.TestCase):
603 src_shape = (4, 8)643 src_shape = (4, 8)
604 dst_shape = (2, 16)644 dst_shape = (2, 16)
605 645 
606- output_layout, _ = op_view.infer_layout(646+ output_layout, _ = self._infer_reshape(op_view, x_layout, dst_shape, src_shape)
607- (x_layout,), (dst_shape, src_shape)
608- )
609 647 
610 assert output_layout.is_partial(), "Output layout should preserve partial status"648 assert output_layout.is_partial(), "Output layout should preserve partial status"
611 assert output_layout.get_partial_by_dev_id("dp") == "sum"649 assert output_layout.get_partial_by_dev_id("dp") == "sum"
@@ -15,10 +15,10 @@
15"""test ut for parallel slice infer_layout"""15"""test ut for parallel slice infer_layout"""
16import os16import os
17import unittest17import unittest
18-from unittest.mock import patch18+from unittest.mock import MagicMock, patch
19import numpy as np19import numpy as np
20 20 
21-from hyper_parallel.core.dtensor.dtensor import _build_layout21+from hyper_parallel.core.dtensor.dtensor import _build_layout, _LAYOUT_CACHE
22from hyper_parallel.core.dtensor.placement_types import Shard, Replicate22from hyper_parallel.core.dtensor.placement_types import Shard, Replicate
23from hyper_parallel.core.shard.ops.parallel_slice import SliceDistributedOp23from hyper_parallel.core.shard.ops.parallel_slice import SliceDistributedOp
24from hyper_parallel.core.dtensor.device_mesh import (24from hyper_parallel.core.dtensor.device_mesh import (
@@ -40,11 +40,13 @@ class TestParallelSlice(unittest.TestCase):
40 """40 """
41 EXISTING_COMM_GROUPS.clear()41 EXISTING_COMM_GROUPS.clear()
42 _DEVICE_MESH_MAP.clear()42 _DEVICE_MESH_MAP.clear()
43+ _LAYOUT_CACHE.clear()
43 44 
44 def tearDown(self):45 def tearDown(self):
45 """Clean up after each test method."""46 """Clean up after each test method."""
46 EXISTING_COMM_GROUPS.clear()47 EXISTING_COMM_GROUPS.clear()
47 _DEVICE_MESH_MAP.clear()48 _DEVICE_MESH_MAP.clear()
49+ _LAYOUT_CACHE.clear()
48 50 
49 def _setup_mock_platform(self, mock_platform, platform_type=None, world_size=8):51 def _setup_mock_platform(self, mock_platform, platform_type=None, world_size=8):
50 """Configure common mock-platform attributes used across tests.52 """Configure common mock-platform attributes used across tests.
@@ -82,18 +84,22 @@ class TestParallelSlice(unittest.TestCase):
82 mesh = self._make_2x4_mesh(mock_platform)84 mesh = self._make_2x4_mesh(mock_platform)
83 x_layout = _build_layout(mesh, (Shard(0), Replicate()), 2)85 x_layout = _build_layout(mesh, (Shard(0), Replicate()), 2)
84 86 
85- output_info = op.infer_layout((x_layout,), ((0, 0), (8, 4), (8, 8)))87+ cache_values = [x_layout, (0, 0), (8, 4), (8, 8)]
86- assert x_layout == output_info[0]88+ output_layouts, extra_info = op.infer_layout(cache_values)
87- assert output_info[1] == (0, 0)89+ assert output_layouts == (x_layout,)
88- assert output_info[2] == (4, 4)90+ assert extra_info[0] == (0, 0)
91+ assert extra_info[1] == (4, 4)
89 92 
90- # Since `get_expand_impl` is not overridden, it returns None by default.93+ calls = []
91- # The same applies to other test classes, so it is unnecessary to test its return value.94+ 
92- assert op.get_expand_impl(None, output_info, (x_layout,), 95+ def fake_slice(x, begin, end):
93- ((0, 0), (8, 4), (8, 8))) is None, (96+ calls.append((x, begin, end))
94- f"get_expand_impl test failed. Expected None, "97+ return "sliced"
95- f"got {op.get_expand_impl(None, output_info, (x_layout,), ((0, 0), (8, 4), (8, 8)))}"98+ 
96- )99+ impl = op.get_expand_impl(fake_slice, (output_layouts, extra_info), cache_values)
100+ assert impl is not None
101+ assert impl("local_x", (0, 0), (8, 4)) == "sliced"
102+ assert calls == [("local_x", (0, 0), (4, 4))]
97 103 
98 @patch("hyper_parallel.core.dtensor.device_mesh.platform")104 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
99 def test_slice_layout_2(self, mock_platform):105 def test_slice_layout_2(self, mock_platform):
@@ -106,8 +112,28 @@ class TestParallelSlice(unittest.TestCase):
106 x_layout = _build_layout(mesh, (Shard(0), Replicate()), 2)112 x_layout = _build_layout(mesh, (Shard(0), Replicate()), 2)
107 113 
108 with self.assertRaises(ValueError):114 with self.assertRaises(ValueError):
109- _ = op.infer_layout((x_layout,), ((0, 0), (4, 4), (8, 8)))115+ _ = op.infer_layout([x_layout, (0, 0), (4, 4), (8, 8)])
110 116 
117+ @patch("hyper_parallel.core.dtensor.device_mesh.platform")
118+ def test_slice_preprocess(self, mock_platform):
119+ """
120+ Feature: Slice preprocess
121+ Description: Verify local args and cache values are built in new dispatch format
122+ Expectation: Success
123+ """
124+ mesh = self._make_2x4_mesh(mock_platform)
125+ x_layout = _build_layout(mesh, (Shard(0), Replicate()), 2)
126+ local_tensor = MagicMock()
127+ input_tensor = MagicMock()
128+ input_tensor.layout = x_layout
129+ input_tensor.shape = (8, 8)
130+ input_tensor.to_local.return_value = local_tensor
131+ 
132+ local_args, local_kwargs, cache_values = op.preprocess((input_tensor, (0, 0), (8, 4)), {})
133+ 
134+ assert local_args == (local_tensor, (0, 0), (8, 4))
135+ assert not local_kwargs
136+ assert cache_values == [x_layout, (0, 0), (8, 4), (8, 8)]
111 137 
112if __name__ == "__main__":138if __name__ == "__main__":
113 unittest.main()139 unittest.main()
@@ -13,12 +13,11 @@
13# limitations under the License.13# limitations under the License.
14# ============================================================================14# ============================================================================
15"""parallel_squeeze test"""15"""parallel_squeeze test"""
16-import os
17import unittest16import unittest
18-from unittest.mock import patch17+from unittest.mock import MagicMock, patch
19import numpy as np18import numpy as np
20 19 
21-from hyper_parallel.core.dtensor.dtensor import _build_layout20+from hyper_parallel.core.dtensor.dtensor import _build_layout, _LAYOUT_CACHE
22from hyper_parallel.core.dtensor.placement_types import Shard, Replicate21from hyper_parallel.core.dtensor.placement_types import Shard, Replicate
23from hyper_parallel.core.shard.ops.parallel_squeeze import SqueezeDistributedOp22from hyper_parallel.core.shard.ops.parallel_squeeze import SqueezeDistributedOp
24from hyper_parallel.core.dtensor.device_mesh import (23from hyper_parallel.core.dtensor.device_mesh import (
@@ -38,12 +37,14 @@ class TestParallelSqueeze(unittest.TestCase):
38 """37 """
39 EXISTING_COMM_GROUPS.clear()38 EXISTING_COMM_GROUPS.clear()
40 _DEVICE_MESH_MAP.clear()39 _DEVICE_MESH_MAP.clear()
40+ _LAYOUT_CACHE.clear()
41 self.op = SqueezeDistributedOp("Squeeze")41 self.op = SqueezeDistributedOp("Squeeze")
42 42 
43 def tearDown(self):43 def tearDown(self):
44 """Clean up after each test method."""44 """Clean up after each test method."""
45 EXISTING_COMM_GROUPS.clear()45 EXISTING_COMM_GROUPS.clear()
46 _DEVICE_MESH_MAP.clear()46 _DEVICE_MESH_MAP.clear()
47+ _LAYOUT_CACHE.clear()
47 48 
48 def _setup_mock_platform(self, mock_platform, platform_type=None, world_size=8):49 def _setup_mock_platform(self, mock_platform, platform_type=None, world_size=8):
49 """Configure common mock-platform attributes used across tests.50 """Configure common mock-platform attributes used across tests.
@@ -73,12 +74,9 @@ class TestParallelSqueeze(unittest.TestCase):
73 74 
74 def _run_scenario(self, x_layout, expected_map, axis, input_shape):75 def _run_scenario(self, x_layout, expected_map, axis, input_shape):
75 """Infer layout of Squeeze operator"""76 """Infer layout of Squeeze operator"""
76- if axis is not None:77+ cache_values = [x_layout, axis, input_shape]
77- extra_args = [axis, [input_shape]]78+ output_layouts, extra_info = self.op.infer_layout(cache_values)
78- else:79+ output_layout = output_layouts[0]
79- extra_args = [[input_shape]]
80- 
81- output_layout = self.op.infer_layout((x_layout,), extra_args)
82 actual_map = output_layout.tensor_map80 actual_map = output_layout.tensor_map
83 assert actual_map == expected_map, (81 assert actual_map == expected_map, (
84 f"Squeeze failed. Expected {expected_map}, got {actual_map}"82 f"Squeeze failed. Expected {expected_map}, got {actual_map}"
@@ -86,11 +84,35 @@ class TestParallelSqueeze(unittest.TestCase):
86 84 
87 # Since `get_expand_impl` is not overridden, it returns None by default.85 # Since `get_expand_impl` is not overridden, it returns None by default.
88 # The same applies to other test classes, so it is unnecessary to test its return value.86 # The same applies to other test classes, so it is unnecessary to test its return value.
89- assert self.op.get_expand_impl(None, output_layout, (x_layout,), extra_args) is None, (87+ infer_result = (output_layouts, extra_info)
88+ assert self.op.get_expand_impl(None, infer_result, cache_values) is None, (
90 f"get_expand_impl test failed. Expected None, "89 f"get_expand_impl test failed. Expected None, "
91- f"got {self.op.get_expand_impl(None, output_layout, (x_layout,), extra_args)}"90+ f"got {self.op.get_expand_impl(None, infer_result, cache_values)}"
92 )91 )
93 92 
93+ def test_squeeze_preprocess(self):
94+ """
95+ Feature: preprocess.
96+ Description: convert DTensor inputs to local args and collect layout, axis, and shape cache.
97+ Expectation: local args are positional and kwargs are empty.
98+ """
99+ x_layout = MagicMock()
100+ local_tensor = MagicMock()
101+ input_tensor = MagicMock()
102+ input_tensor.layout = x_layout
103+ input_tensor.shape = (8, 1, 4)
104+ input_tensor.to_local.return_value = local_tensor
105+ 
106+ local_args, local_kwargs, cache_values = self.op.preprocess((input_tensor,), {})
107+ assert local_args == (local_tensor,)
108+ assert not local_kwargs
109+ assert cache_values == [x_layout, None, (8, 1, 4)]
110+ 
111+ local_args, local_kwargs, cache_values = self.op.preprocess((input_tensor, 1), {})
112+ assert local_args == (local_tensor, 1)
113+ assert not local_kwargs
114+ assert cache_values == [x_layout, 1, (8, 1, 4)]
115+ 
94 @patch("hyper_parallel.core.dtensor.device_mesh.platform")116 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
95 def test_squeeze_data_parallel_1(self, mock_platform):117 def test_squeeze_data_parallel_1(self, mock_platform):
96 """118 """
@@ -301,13 +323,16 @@ class TestParallelSqueeze(unittest.TestCase):
301 x_layout = _build_layout(mesh, (Shard(0), Shard(1), Shard(2)), 3)323 x_layout = _build_layout(mesh, (Shard(0), Shard(1), Shard(2)), 3)
302 324 
303 with self.assertRaises(ValueError):325 with self.assertRaises(ValueError):
304- self.op.infer_layout((x_layout,), [])326+ self.op.infer_layout([])
305 327 
306 with self.assertRaises(ValueError):328 with self.assertRaises(ValueError):
307- self.op.infer_layout((x_layout,), [1])329+ self.op.infer_layout([x_layout, 1])
308 330 
309 with self.assertRaises(ValueError):331 with self.assertRaises(ValueError):
310- self.op.infer_layout((x_layout,), None)332+ self.op.infer_layout([x_layout, None, None])
333+ 
334+ with self.assertRaises(ValueError):
335+ self.op.infer_layout([x_layout, None, 1])
311 336 
312 @patch("hyper_parallel.core.dtensor.device_mesh.platform")337 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
313 def test_squeeze_scalar_with_axis_17(self, mock_platform):338 def test_squeeze_scalar_with_axis_17(self, mock_platform):
@@ -321,7 +346,7 @@ class TestParallelSqueeze(unittest.TestCase):
321 input_shape = []346 input_shape = []
322 347 
323 with self.assertRaises(ValueError):348 with self.assertRaises(ValueError):
324- self.op.infer_layout((x_layout,), [0, [input_shape]])349+ self.op.infer_layout([x_layout, 0, input_shape])
325 350 
326 351 
327if __name__ == "__main__":352if __name__ == "__main__":
@@ -13,13 +13,12 @@
13# limitations under the License.13# limitations under the License.
14# ============================================================================14# ============================================================================
15"""Unit tests for TupleElementWiseDistributedOp"""15"""Unit tests for TupleElementWiseDistributedOp"""
16-import os
17import unittest16import unittest
18-from unittest.mock import patch17+from unittest.mock import MagicMock, patch
19 18 
20import numpy as np19import numpy as np
21 20 
22-from hyper_parallel.core.dtensor.dtensor import _build_layout21+from hyper_parallel.core.dtensor.dtensor import _build_layout, _LAYOUT_CACHE
23from hyper_parallel.core.dtensor.placement_types import Shard, Replicate22from hyper_parallel.core.dtensor.placement_types import Shard, Replicate
24from hyper_parallel.core.dtensor.device_mesh import init_device_mesh, _DEVICE_MESH_MAP23from hyper_parallel.core.dtensor.device_mesh import init_device_mesh, _DEVICE_MESH_MAP
25from hyper_parallel.core.shard.ops.parallel_tuple_elementwise import TupleElementWiseDistributedOp24from hyper_parallel.core.shard.ops.parallel_tuple_elementwise import TupleElementWiseDistributedOp
@@ -42,6 +41,16 @@ def _make_mesh(mock_platform, mesh_shape, mesh_dim_names):
42 )41 )
43 42 
44 43 
44+def _infer_layout(op, cache_values):
45+ """Infer layouts with the new preprocess/infer_layout protocol."""
46+ result = op.infer_layout(cache_values)
47+ if result is None:
48+ return None
49+ output_layouts, extra_info = result
50+ assert extra_info is None
51+ return output_layouts
52+ 
53+ 
45class TestTupleElementWiseDistributedOpInferLayout(unittest.TestCase):54class TestTupleElementWiseDistributedOpInferLayout(unittest.TestCase):
46 """55 """
47 Feature: TupleElementWiseDistributedOp.infer_layout56 Feature: TupleElementWiseDistributedOp.infer_layout
@@ -52,16 +61,18 @@ class TestTupleElementWiseDistributedOpInferLayout(unittest.TestCase):
52 def setUp(self):61 def setUp(self):
53 EXISTING_COMM_GROUPS.clear()62 EXISTING_COMM_GROUPS.clear()
54 _DEVICE_MESH_MAP.clear()63 _DEVICE_MESH_MAP.clear()
64+ _LAYOUT_CACHE.clear()
55 65 
56 def tearDown(self):66 def tearDown(self):
57 EXISTING_COMM_GROUPS.clear()67 EXISTING_COMM_GROUPS.clear()
58 _DEVICE_MESH_MAP.clear()68 _DEVICE_MESH_MAP.clear()
69+ _LAYOUT_CACHE.clear()
59 70 
60 @patch("hyper_parallel.core.dtensor.device_mesh.platform")71 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
61 def test_empty_layouts_returns_none(self, mock_platform):72 def test_empty_layouts_returns_none(self, mock_platform):
62 """Empty input layouts returns None."""73 """Empty input layouts returns None."""
63 op = TupleElementWiseDistributedOp("tuple_elementwise_empty")74 op = TupleElementWiseDistributedOp("tuple_elementwise_empty")
64- result = op.infer_layout(())75+ result = op.infer_layout([])
65 self.assertIsNone(result)76 self.assertIsNone(result)
66 77 
67 @patch("hyper_parallel.core.dtensor.device_mesh.platform")78 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
@@ -69,10 +80,10 @@ class TestTupleElementWiseDistributedOpInferLayout(unittest.TestCase):
69 """Single input layout is returned unchanged as the layouts object."""80 """Single input layout is returned unchanged as the layouts object."""
70 mesh = _make_mesh(mock_platform, (2,), ("dp",))81 mesh = _make_mesh(mock_platform, (2,), ("dp",))
71 layout = _build_layout(mesh, (Replicate(),), 2)82 layout = _build_layout(mesh, (Replicate(),), 2)
72- inputs = (layout,)
73 op = TupleElementWiseDistributedOp("tuple_elementwise_single")83 op = TupleElementWiseDistributedOp("tuple_elementwise_single")
74- result = op.infer_layout(inputs)84+ result = _infer_layout(op, [layout])
75- self.assertIs(result, inputs)85+ self.assertEqual(result, (layout,))
86+ self.assertIsNot(result[0], layout)
76 87 
77 @patch("hyper_parallel.core.dtensor.device_mesh.platform")88 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
78 def test_multiple_layouts_returned_as_is(self, mock_platform):89 def test_multiple_layouts_returned_as_is(self, mock_platform):
@@ -80,10 +91,9 @@ class TestTupleElementWiseDistributedOpInferLayout(unittest.TestCase):
80 mesh = _make_mesh(mock_platform, (2, 2), ("dp", "mp"))91 mesh = _make_mesh(mock_platform, (2, 2), ("dp", "mp"))
81 layout1 = _build_layout(mesh, (Replicate(), Replicate()), 2)92 layout1 = _build_layout(mesh, (Replicate(), Replicate()), 2)
82 layout2 = _build_layout(mesh, (Shard(0), Replicate()), 2)93 layout2 = _build_layout(mesh, (Shard(0), Replicate()), 2)
83- inputs = (layout1, layout2)
84 op = TupleElementWiseDistributedOp("tuple_elementwise_multi")94 op = TupleElementWiseDistributedOp("tuple_elementwise_multi")
85- result = op.infer_layout(inputs)95+ result = _infer_layout(op, [layout1, layout2])
86- self.assertIs(result, inputs)96+ self.assertEqual(result, (layout1, layout2))
87 97 
88 @patch("hyper_parallel.core.dtensor.device_mesh.platform")98 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
89 def test_sharded_layout_returned(self, mock_platform):99 def test_sharded_layout_returned(self, mock_platform):
@@ -91,17 +101,17 @@ class TestTupleElementWiseDistributedOpInferLayout(unittest.TestCase):
91 mesh = _make_mesh(mock_platform, (4,), ("dp",))101 mesh = _make_mesh(mock_platform, (4,), ("dp",))
92 layout = _build_layout(mesh, (Shard(0),), 2)102 layout = _build_layout(mesh, (Shard(0),), 2)
93 op = TupleElementWiseDistributedOp("tuple_elementwise_sharded")103 op = TupleElementWiseDistributedOp("tuple_elementwise_sharded")
94- result = op.infer_layout((layout,))104+ result = _infer_layout(op, [layout])
95 self.assertEqual(result, (layout,))105 self.assertEqual(result, (layout,))
96 106 
97 @patch("hyper_parallel.core.dtensor.device_mesh.platform")107 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
98- def test_extra_args_ignored(self, mock_platform):108+ def test_none_layout_in_inputs(self, mock_platform):
99- """extra_args parameter is ignored for tuple element-wise ops."""109+ """None layout in inputs is returned as part of the result."""
100 mesh = _make_mesh(mock_platform, (2,), ("dp",))110 mesh = _make_mesh(mock_platform, (2,), ("dp",))
101 layout = _build_layout(mesh, (Replicate(),), 2)111 layout = _build_layout(mesh, (Replicate(),), 2)
102- op = TupleElementWiseDistributedOp("tuple_elementwise_extra_args")112+ op = TupleElementWiseDistributedOp("tuple_elementwise_none")
103- result = op.infer_layout((layout,), extra_args=(42, "some_arg"))113+ result = _infer_layout(op, [layout, None])
104- self.assertEqual(result, (layout,))114+ self.assertEqual(result, (layout, None))
105 115 
106 @patch("hyper_parallel.core.dtensor.device_mesh.platform")116 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
107 def test_three_layouts_returned(self, mock_platform):117 def test_three_layouts_returned(self, mock_platform):
@@ -110,20 +120,19 @@ class TestTupleElementWiseDistributedOpInferLayout(unittest.TestCase):
110 layout1 = _build_layout(mesh, (Replicate(), Replicate(), Replicate()), 3)120 layout1 = _build_layout(mesh, (Replicate(), Replicate(), Replicate()), 3)
111 layout2 = _build_layout(mesh, (Shard(0), Replicate(), Replicate()), 3)121 layout2 = _build_layout(mesh, (Shard(0), Replicate(), Replicate()), 3)
112 layout3 = _build_layout(mesh, (Replicate(), Replicate(), Shard(2)), 3)122 layout3 = _build_layout(mesh, (Replicate(), Replicate(), Shard(2)), 3)
113- inputs = (layout1, layout2, layout3)
114 op = TupleElementWiseDistributedOp("tuple_elementwise_three")123 op = TupleElementWiseDistributedOp("tuple_elementwise_three")
115- result = op.infer_layout(inputs)124+ result = _infer_layout(op, [layout1, layout2, layout3])
116- self.assertIs(result, inputs)125+ self.assertEqual(result, (layout1, layout2, layout3))
117 126 
118 @patch("hyper_parallel.core.dtensor.device_mesh.platform")127 @patch("hyper_parallel.core.dtensor.device_mesh.platform")
119- def test_none_layout_in_inputs(self, mock_platform):128+ def test_partial_input_raises_error(self, mock_platform):
120- """None layout in inputs is returned as part of the result."""129+ """Partial input layout is rejected."""
121 mesh = _make_mesh(mock_platform, (2,), ("dp",))130 mesh = _make_mesh(mock_platform, (2,), ("dp",))
122 layout = _build_layout(mesh, (Replicate(),), 2)131 layout = _build_layout(mesh, (Replicate(),), 2)
123- inputs = (layout, None)132+ layout.set_partial_by_dev_axis("dp", "sum")
124- op = TupleElementWiseDistributedOp("tuple_elementwise_none")133+ op = TupleElementWiseDistributedOp("tuple_elementwise_partial")
125- result = op.infer_layout(inputs)134+ with self.assertRaisesRegex(ValueError, "Partial status"):
126- self.assertIs(result, inputs)135+ op.infer_layout([layout])
127 136 
128 137 
129class TestTupleElementWiseInheritance(unittest.TestCase):138class TestTupleElementWiseInheritance(unittest.TestCase):
@@ -134,15 +143,36 @@ class TestTupleElementWiseInheritance(unittest.TestCase):
134 Expectation: Inherited methods behave as defined in DistributedOp.143 Expectation: Inherited methods behave as defined in DistributedOp.
135 """144 """
136 145 
137- def test_preprocess_returns_none(self):146+ def test_preprocess_expands_tuple_and_unwraps_dtensors(self):
138- """Inherited preprocess returns None (legacy fallback)."""147+ """preprocess unwraps local tensors and expands tuple/list layouts for cache values."""
148+ layout1 = object()
149+ layout2 = object()
150+ local1 = object()
151+ local2 = object()
152+ tensor1 = MagicMock()
153+ tensor1._layout = layout1
154+ tensor1.layout = layout1
155+ tensor1.to_local.return_value = local1
156+ tensor2 = MagicMock()
157+ tensor2._layout = layout2
158+ tensor2.layout = layout2
159+ tensor2.to_local.return_value = local2
160+ plain_slot = object()
161+ 
139 op = TupleElementWiseDistributedOp("tuple_elementwise_preprocess")162 op = TupleElementWiseDistributedOp("tuple_elementwise_preprocess")
140- self.assertIsNone(op.preprocess((1, 2), {}))163+ local_args, local_kwargs, cache_values = op.preprocess(
164+ ((tensor1, plain_slot, tensor2),),
165+ {"kw": tensor1}
166+ )
167+ 
168+ self.assertEqual(local_args, ((local1, plain_slot, local2),))
169+ self.assertEqual(local_kwargs, {"kw": local1})
170+ self.assertEqual(cache_values, [layout1, None, layout2, layout1])
141 171 
142 def test_get_expand_impl_returns_none(self):172 def test_get_expand_impl_returns_none(self):
143 """Inherited get_expand_impl returns None."""173 """Inherited get_expand_impl returns None."""
144 op = TupleElementWiseDistributedOp("tuple_elementwise_expand")174 op = TupleElementWiseDistributedOp("tuple_elementwise_expand")
145- self.assertIsNone(op.get_expand_impl(None, None, ()))175+ self.assertIsNone(op.get_expand_impl(None, ((object(),), None), [object()]))
146 176 
147 177 
148if __name__ == "__main__":178if __name__ == "__main__":
@@ -1067,7 +1067,7 @@ class TestDispatchNew(unittest.TestCase):
1067 return "py_output"1067 return "py_output"
1068 1068 
1069 d = object.__new__(OpDispatcher)1069 d = object.__new__(OpDispatcher)
1070- result = d._dispatch_new(mock_func, mock_dist_op, ([], {}, cache_values))1070+ result = d._dispatch_new(mock_func, mock_dist_op, None, ([], {}, cache_values))
1071 self.assertEqual(result, "wrapped_output")1071 self.assertEqual(result, "wrapped_output")
1072 mock_dist_op.infer_layout.assert_called_once()1072 mock_dist_op.infer_layout.assert_called_once()
1073 mock_dist_op.wrap_output.assert_called_once()1073 mock_dist_op.wrap_output.assert_called_once()
@@ -1101,7 +1101,7 @@ class TestDispatchNew(unittest.TestCase):
1101 return "should_not_be_called"1101 return "should_not_be_called"
1102 1102 
1103 d = object.__new__(OpDispatcher)1103 d = object.__new__(OpDispatcher)
1104- result = d._dispatch_new(mock_func, mock_dist_op, (["arg"], {}, cache_values))1104+ result = d._dispatch_new(mock_func, mock_dist_op, None, (["arg"], {}, cache_values))
1105 mock_dist_op.infer_layout.assert_not_called()1105 mock_dist_op.infer_layout.assert_not_called()
1106 mock_impl.assert_called_once()1106 mock_impl.assert_called_once()
1107 1107 
@@ -1683,10 +1683,11 @@ class TestWithLayoutInfer(unittest.TestCase):
1683 prim_obj = MagicMock()1683 prim_obj = MagicMock()
1684 func = MagicMock(return_value=MagicMock())1684 func = MagicMock(return_value=MagicMock())
1685 1685 
1686- # packed form: (prim_obj, "OpName", (arg1, arg2))1686+ # NOTE: aclop args are now normalized upstream by _normalize_aclop_args.
1687- result = d._with_layout_infer(func, prim_obj, "UnpackOp", (10, 20))1687+ # Pass unpacked real args positionally and _packed_call via keyword.
1688+ result = d._with_layout_infer(func, 10, 20, _packed_call=(prim_obj, "UnpackOp"))
1688 1689 
1689- # op_impl should be called with the packed form1690+ # op_impl should be called with the re-packed form
1690 func.assert_called_once()1691 func.assert_called_once()
1691 mock_dtensor_cls.from_local.assert_called_once()1692 mock_dtensor_cls.from_local.assert_called_once()
1692 1693