Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / core / shard / ops / parallel_squeeze.py: 90%
102 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-07-06 05:41 +0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-07-06 05:41 +0800
1# Copyright 2026 Huawei Technologies Co., Ltd
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14# ============================================================================
15"""
16Distributed implementation for Squeeze operator.
17"""
18from copy import deepcopy
19from typing import Tuple
21from hyper_parallel.core.dtensor.layout import Layout
22from .parallel_ops import DistributedOp
25def _normalize_squeeze_args(x, axis=None):
26 return (x, axis), {}
29class SqueezeDistributedOp(DistributedOp):
30 """Distributed implementation for Squeeze operator."""
32 def preprocess(self, args: tuple, kwargs: dict) -> tuple:
33 args, _ = _normalize_squeeze_args(*args, **kwargs)
34 input_tensor, axis = args[0], args[1]
36 if axis is None:
37 local_args = (input_tensor.to_local(),)
38 else:
39 local_args = (input_tensor.to_local(), axis)
41 input_shape = getattr(input_tensor, "shape", None)
42 cache_values = [input_tensor.layout, axis, input_shape]
43 return local_args, {}, cache_values
45 def infer_layout(self, cache_values: list) -> Tuple[tuple, None]: # pylint: disable=W0221
46 """
47 Infer output layout for Squeeze operator.
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.
58 Args:
59 cache_values (list): [input_layout, axis, input_shape]
61 Returns:
62 tuple: ((output_layout,), None)
64 Raises:
65 ValueError: If input has Partial status, input_shape is missing, axis
66 is invalid, or a requested squeeze dimension is sharded.
67 """
68 if not cache_values:
69 raise ValueError(
70 f"For {self.op_name}, cache_values should contain input layout, "
71 f"but got empty cache_values."
72 )
74 x_layout = cache_values[0]
75 if not self._allow_partial_inputs:
76 self._check_partial_inputs([x_layout])
78 if x_layout.mesh_shape is None:
79 raise ValueError(
80 f"For {self.op_name}, input layout mesh_shape should not be None, "
81 f"but got None."
82 )
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
86 if input_shape is None:
87 raise ValueError(
88 f"For {self.op_name}, input_shape should be provided in cache_values, "
89 f"but got None."
90 )
91 if not isinstance(input_shape, (list, tuple)):
92 raise ValueError(
93 f"For {self.op_name}, input_shape should be list or tuple, "
94 f"but got {type(input_shape)}."
95 )
97 output_layout = self._compute_squeeze_layout(x_layout, axis, input_shape)
98 return ((output_layout,), None)
100 def _compute_squeeze_layout(self, x_layout, axis, input_shape):
101 """Compute the squeezed layout."""
102 # Handle scalar case
103 if not input_shape:
104 return self._handle_scalar_case(x_layout, axis)
106 # Validate input_shape matches layout rank
107 self._validate_input_shape(x_layout, input_shape)
109 # Find dimensions to squeeze
110 dims_to_squeeze = self._get_dims_to_squeeze(x_layout, axis, input_shape)
112 # Create output layout
113 return self._create_output_layout(x_layout, dims_to_squeeze)
115 def _handle_scalar_case(self, x_layout, axis):
116 """Handle scalar input case."""
117 if axis is not None and axis != [] and axis != ():
118 raise ValueError(
119 f"For {self.op_name}, axis should be None for scalar input, "
120 f"but got {axis}."
121 )
123 return deepcopy(x_layout)
125 def _validate_input_shape(self, x_layout, input_shape):
126 """Validate that input shape matches layout rank."""
127 x_map = list(x_layout.alias_tensor_map)
128 in_rank = len(x_map)
130 if len(input_shape) != in_rank:
131 raise ValueError(
132 f"For {self.op_name}, input shape rank should match layout rank, "
133 f"but got {len(input_shape)} and {in_rank}."
134 )
136 def _get_dims_to_squeeze(self, x_layout, axis, input_shape):
137 """Get list of dimensions to squeeze."""
138 x_map = list(x_layout.alias_tensor_map)
139 in_rank = len(x_map)
141 if axis is None:
142 return self._get_all_squeezable_dims(x_map, input_shape)
143 return self._get_specified_dims_to_squeeze(x_map, axis, input_shape, in_rank)
145 def _get_all_squeezable_dims(self, x_map, input_shape):
146 """Get all squeezable dimensions when axis is None."""
147 dims_to_squeeze = []
148 for i, shape in enumerate(input_shape):
149 if shape == 1 and x_map[i] == "None":
150 dims_to_squeeze.append(i)
151 return dims_to_squeeze
153 def _get_specified_dims_to_squeeze(self, x_map, axis, input_shape, in_rank):
154 """Get dimensions to squeeze when axis is specified."""
155 # Convert axis to list if it's a single integer
156 if isinstance(axis, int):
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 )
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 )
172 # Convert negative indices to positive
173 axis = [ax if ax >= 0 else ax + in_rank for ax in axis]
175 # Validate axis range
176 self._validate_axis_range(axis, in_rank)
178 # Check all specified axes
179 for ax in axis:
180 self._validate_axis_for_squeeze(x_map, input_shape, ax)
182 # Return sorted unique axes
183 return sorted(set(axis))
185 def _validate_axis_range(self, axis, in_rank):
186 """Validate axis values are within range."""
187 for ax in axis:
188 if ax < 0 or ax >= in_rank:
189 raise ValueError(
190 f"For {self.op_name}, axis should be in range [{-in_rank}, {in_rank-1}], "
191 f"but got {ax}."
192 )
194 def _validate_axis_for_squeeze(self, x_map, input_shape, ax):
195 """Validate a specific axis can be squeezed."""
196 # Check shape == 1
197 if input_shape[ax] != 1:
198 raise ValueError(
199 f"For {self.op_name}, dimension should have size 1, "
200 f"but got shape {input_shape[ax]} at dimension {ax}."
201 )
203 # Check mapping is "None" (not distributed)
204 if x_map[ax] != "None":
205 raise ValueError(
206 f"For {self.op_name}, dimension should not be distributed, "
207 f"but got dimension {ax} mapped to device axis {x_map[ax]}."
208 )
210 def _create_output_layout(self, x_layout, dims_to_squeeze):
211 """Create output layout after squeezing dimensions."""
212 if not dims_to_squeeze:
213 return deepcopy(x_layout)
215 # Get current alias tensor map
216 x_map = list(x_layout.alias_tensor_map)
218 # Sort in descending order for safe removal
219 dims_to_squeeze = sorted(set(dims_to_squeeze), reverse=True)
221 # Remove specified dimensions
222 for dim in dims_to_squeeze:
223 del x_map[dim]
225 new_map = x_map
227 # Create output layout with new mapping
228 output_layout = Layout(
229 mesh_shape=x_layout.mesh_shape,
230 alias_name=x_layout.alias_name,
231 rank_list=x_layout.rank_list
232 )
234 if new_map:
235 output_layout = output_layout(*new_map)
236 else:
237 # For scalar result
238 output_layout = output_layout()
240 # Copy partial operations from input layout
241 self._copy_partial_operations(x_layout, output_layout, new_map)
243 return output_layout
245 def _copy_partial_operations(self, x_layout, output_layout, new_map):
246 """Copy partial operations from input to output layout."""
247 for i, partial_op in enumerate(x_layout.partial):
248 if partial_op is not None:
249 dev_axis_name = x_layout.alias_name[i]
250 # Check if this device axis is still used in the output
251 if dev_axis_name in new_map:
252 output_layout.set_partial_by_dev_axis(dev_axis_name, partial_op)