Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / core / shard / ops / parallel_reshape.py: 83%

182 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-07-06 05:41 +0800

1# Copyright 2025-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 Reshape operator. 

17""" 

18 

19from typing import Callable, Optional, Tuple 

20 

21from hyper_parallel.core.dtensor.layout import Layout 

22from hyper_parallel.platform import get_platform 

23from .parallel_ops import DistributedOp 

24platform = get_platform() 

25Tensor = platform.Tensor 

26 

27 

28def _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 

43def _filter_none_split_tensor_map(tensor_map, mesh_shape): 

44 """ 

45 Filter out the elements in tensor_map where the size of the corresponding dimension in device_matrix is 1. 

46 

47 Args: 

48 tensor_map (list): A list of tensor mappings, which may contain integers or tuples. 

49 device_matrix (list): A device matrix representing the device distribution across each dimension. 

50 

51 Returns: 

52 list: The filtered list of tensor mappings, where invalid mappings are replaced with -1 or valid mappings are 

53 retained. 

54 """ 

55 filtered_tensor_map = [] 

56 for item in tensor_map: 

57 if isinstance(item, tuple): 

58 filtered = [] 

59 for i in item: 

60 if mesh_shape[-1 - i] != 1: 

61 filtered.append(i) 

62 if len(filtered) == 0: 

63 filtered_tensor_map.append(-1) 

64 elif len(filtered) == 1: 

65 filtered_tensor_map.append(filtered[0]) 

66 else: 

67 filtered_tensor_map.append(tuple(filtered)) 

68 else: 

69 filtered_tensor_map.append(item if mesh_shape[-1 - item] != 1 else -1) 

70 return filtered_tensor_map 

71 

72 

73class ReshapeDistributedOp(DistributedOp): 

74 """Distributed implementation for Reshape operator.""" 

75 

76 def __init__(self, op_name): 

77 super().__init__(op_name) 

78 self._allow_partial_inputs = True 

79 

80 def _get_dynamic_shape_info(self, shape): 

81 total_size = 1 

82 dynamic_axis = -1 

83 for axis, s in enumerate(shape): 

84 total_size *= s 

85 if s < 0: 

86 dynamic_axis = axis 

87 return total_size < 0, dynamic_axis, total_size 

88 

89 def _handle_dynamic_shape(self, input_shape, output_shape): 

90 """ 

91 Check dynamic shape. Calculate unknown axis if one of input and output shape is known. If both are unknown, 

92 calculate the relative multiple. 

93 [2, -1, 8], [4, -1, 8] -> [2, -2, 8], [4, -1, 8] 

94 """ 

95 input_shape = list(input_shape) 

96 output_shape = list(output_shape) 

97 is_input_dynamic, input_dynamic_axis, input_total_size = self._get_dynamic_shape_info(input_shape) 

98 is_output_dynamic, output_dynamic_axis, output_total_size = self._get_dynamic_shape_info(output_shape) 

99 dynamic_can_shard = False 

100 if not is_input_dynamic and not is_output_dynamic: 

101 if input_total_size != output_total_size: 

102 raise ValueError(f"The total elements number of input shape {input_shape} and output shape " 

103 f"{output_shape} are different.") 

104 return input_shape, output_shape, dynamic_can_shard 

105 

106 if not is_input_dynamic: 

107 accurate_output_shape = output_shape 

108 accurate_output_shape[output_dynamic_axis] = -input_total_size // output_total_size 

109 return input_shape, accurate_output_shape, dynamic_can_shard 

110 

111 if not is_output_dynamic: 

112 accurate_input_shape = input_shape 

113 accurate_input_shape[input_dynamic_axis] = -output_total_size // input_total_size 

114 return accurate_input_shape, output_shape, dynamic_can_shard 

115 

116 if output_total_size >= input_total_size: 

117 output_shape[output_dynamic_axis] = -(input_total_size // output_total_size) 

118 dynamic_can_shard = True 

119 else: 

120 input_shape[input_dynamic_axis] = -(output_total_size // input_total_size) 

121 return input_shape, output_shape, dynamic_can_shard 

122 

123 def _merge_unshared_axis(self, global_shape, tensor_map): 

124 """ 

125 Merge those axes that are not sharded to the high dimension which is shared. 

126 shape[4, 2, 6, 8], tensor map[-1, -1, 0, -1] -> merged shape[8, 48] 

127 

128 Returns: 

129 tuple: (merged_shape, merge_tensor_map). 

130 merge_tensor_map may contain -1 for merged unsharded axis groups. 

131 """ 

132 merged_size = 1 

133 merged_shape = [] 

134 merged_tensor_map = [] 

135 for axis in range(len(global_shape) - 1, -1, -1): 

136 merged_size *= global_shape[axis] 

137 if tensor_map[axis] != -1: 

138 merged_shape.insert(0, merged_size) 

139 merged_tensor_map.insert(0, tensor_map[axis]) 

140 merged_size = 1 

141 if tensor_map[0] == -1: 

142 merged_shape.insert(0, merged_size) 

143 merged_tensor_map.insert(0, -1) 

144 return merged_shape, merged_tensor_map 

145 

146 

147 def _cal_output_layout_and_dst_shape(self, output_tensor_map, dst_shape, x_dict): 

148 """ 

149 calculate output layout tensor map and local dst shape. 

150 """ 

151 x_mesh_shape = x_dict["mesh_shape"] 

152 output_map = [] 

153 local_dst_shape = [] 

154 for idx, map_id in enumerate(output_tensor_map): 

155 if isinstance(map_id, tuple): 

156 shard_size = 1 

157 map_idx = [] 

158 for shard_id in map_id: 

159 map_idx.append(x_dict["alias_name"][-1 - shard_id]) 

160 shard_size *= x_mesh_shape[-1 - shard_id] 

161 output_map.append(tuple(map_idx)) 

162 local_dst_shape.append(dst_shape[idx] // shard_size if dst_shape[idx] > 0 else -1) 

163 continue 

164 if map_id < 0: 

165 output_map.append("None") 

166 local_dst_shape.append(dst_shape[idx] if dst_shape[idx] > 0 else -1) 

167 else: 

168 output_map.append(x_dict["alias_name"][-1 - map_id]) 

169 local_dst_shape.append(dst_shape[idx] // x_mesh_shape[-1 - map_id] if dst_shape[idx] > 0 else -1) 

170 return output_map, local_dst_shape 

171 

172 def _normalize_shape(self, dst_shape): 

173 """Normalize dst_shape to list format.""" 

174 if isinstance(dst_shape, Tensor): 

175 dst_shape = dst_shape.tolist() 

176 if not isinstance(dst_shape, (list, tuple)): 

177 raise ValueError("Shape should be a tensor or a tuple or a list.") 

178 return dst_shape 

179 

180 def _compute_output_tensor_map(self, merged_shape, merge_tensor_map, dst_shape, x_mesh_shape, dynamic_can_shard, 

181 input_shape, x_map): 

182 """Compute output tensor_map from merged information. 

183 

184 Args: 

185 merged_shape: Merged shape from _merge_unshared_axis 

186 merge_tensor_map: Merged tensor_map from _merge_unshared_axis 

187 dst_shape: Target shape 

188 x_mesh_shape: Mesh shape 

189 dynamic_can_shard: Whether dynamic shape can be sharded 

190 input_shape: Original input shape 

191 x_map: Input tensor_map 

192 

193 Returns: 

194 list: Output tensor_map 

195 """ 

196 output_tensor_map = [] 

197 cur_axis = len(merged_shape) - 1 

198 cur_size = merged_shape[cur_axis] 

199 

200 for shape in reversed(dst_shape): 

201 if cur_size % shape != 0: 

202 raise ValueError(f"Can not reshape {input_shape} to {dst_shape} with tensor map {x_map}") 

203 cur_size = cur_size // shape 

204 

205 if cur_size == 1: 

206 map_val = merge_tensor_map[cur_axis] 

207 if map_val != -1: 

208 self._validate_reshape_shard( 

209 map_val, x_mesh_shape, shape, 

210 dynamic_can_shard, input_shape, x_map, dst_shape 

211 ) 

212 output_tensor_map.insert(0, map_val) 

213 cur_axis -= 1 

214 cur_size = merged_shape[cur_axis] 

215 else: 

216 output_tensor_map.insert(0, -1) 

217 

218 return output_tensor_map 

219 

220 def _validate_reshape_shard(self, map_val, x_mesh_shape, shape, 

221 dynamic_can_shard, input_shape, x_map, dst_shape): 

222 """Validate that a sharded axis can be reshaped to the target shape dimension.""" 

223 if isinstance(map_val, tuple): 

224 shard_size = 1 

225 for axis in map_val: 

226 shard_size *= x_mesh_shape[-axis - 1] 

227 else: 

228 shard_size = x_mesh_shape[-map_val - 1] 

229 

230 if shape < 0: 

231 if not dynamic_can_shard: 

232 raise ValueError(f"Can not reshape {input_shape} to {dst_shape} with tensor map {x_map}") 

233 elif shard_size > shape or shape % shard_size != 0: 

234 raise ValueError(f"Can not reshape {input_shape} to {dst_shape} with tensor map {x_map}") 

235 

236 def _apply_partial_status(self, x_layout, out_layout): 

237 """Apply partial status from input to output layout.""" 

238 if x_layout.is_partial(): 

239 input_partial = x_layout.partial 

240 for i, partial_op in enumerate(input_partial): 

241 if partial_op is not None and i < len(out_layout.alias_name): 

242 out_layout.set_partial_by_dev_axis(out_layout.alias_name[i], partial_op) 

243 

244 def preprocess(self, args: tuple, kwargs: dict) -> tuple: 

245 """ 

246 Preprocess arguments for Reshape operator. 

247 

248 Args: 

249 args (tuple): Input tensor followed by target shape arguments. 

250 kwargs (dict): Keyword arguments. 

251 

252 Returns: 

253 tuple: (local_args, local_kwargs, cache_values) 

254 """ 

255 args, _ = _normalize_reshape_args(*args, **kwargs) 

256 input_tensor = args[0] 

257 dst_shape = args[1:] if len(args) > 2 else args[1] 

258 

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() 

267 dst_shape = self._normalize_shape(dst_shape) 

268 

269 x_map = _filter_none_split_tensor_map(x_dict["tensor_map"], x_dict["mesh_shape"]) 

270 x_mesh_shape = x_dict["mesh_shape"] 

271 

272 input_shape, dst_shape, dynamic_can_shard = self._handle_dynamic_shape(input_shape, dst_shape) 

273 merged_shape, merge_tensor_map = self._merge_unshared_axis(input_shape, x_map) 

274 

275 output_tensor_map = self._compute_output_tensor_map( 

276 merged_shape, merge_tensor_map, dst_shape, x_mesh_shape, dynamic_can_shard, input_shape, x_map 

277 ) 

278 

279 output_layout = Layout( 

280 mesh_shape=x_mesh_shape, 

281 alias_name=x_layout.alias_name, 

282 rank_list=x_layout.rank_list 

283 ) 

284 output_map, local_dst_shape = self._cal_output_layout_and_dst_shape(output_tensor_map, dst_shape, x_dict) 

285 out_layout = output_layout(*output_map) 

286 

287 self._apply_partial_status(x_layout, out_layout) 

288 

289 return out_layout, local_dst_shape 

290 

291 def infer_layout(self, cache_values: list) -> Tuple[tuple, list]: # pylint: disable=W0221 

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, # pylint: disable=W0221 

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