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

255 statements  

« 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 Element-wise operator. 

17""" 

18 

19import copy 

20from typing import Callable, Optional, Tuple 

21 

22from .parallel_ops import DistributedOp 

23 

24 

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

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

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

58class ElementWiseDistributedOp(DistributedOp): 

59 """ 

60 Base class for distributed element-wise operators. 

61 

62 Supports broadcasting following broadcasting rules and handles 

63 distributed tensor layouts with proper sharding strategy inference. 

64 

65 Args: 

66 op_name (str): Name of the operator to register. 

67 """ 

68 

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) 

89 return local_args, local_kwargs, cache_values 

90 

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

92 """ 

93 Infer output layouts for element-wise operations with broadcasting support. 

94 

95 Rules: 

96 1. Inputs must not have Partial status unless the operator explicitly allows it. 

97 2. Input shapes must be broadcast-compatible when shape information is available. 

98 3. Broadcasting dimensions cannot be sharded. 

99 4. Non-broadcast sharding patterns must be compatible across inputs. 

100 5. Output layout uses the merged sharding strategy and merged Partial status. 

101 

102 Args: 

103 cache_values (list): [input_layout_0, ..., input_layout_n, input_shapes]. 

104 

105 Returns: 

106 tuple: ((output_layout,), None) 

107 

108 Raises: 

109 ValueError: If input layouts are not compatible for broadcasting. 

110 """ 

111 layouts = tuple(cache_values[:-1]) 

112 input_shapes = cache_values[-1] if cache_values else None 

113 

114 if not layouts: 

115 return None 

116 

117 valid_layouts = [layout for layout in layouts if layout is not None] 

118 

119 if not valid_layouts: 

120 return None 

121 

122 if not self._allow_partial_inputs: 

123 self._check_partial_inputs(layouts) 

124 

125 if len(valid_layouts) == 1: 

126 return ((copy.deepcopy(valid_layouts[0]),), None) 

127 

128 if not input_shapes: 

129 return ((self._handle_no_input_shapes(valid_layouts),), None) 

130 

131 aligned_layouts, aligned_shapes = self._align_layouts_and_shapes(layouts, input_shapes) 

132 

133 if len(aligned_layouts) <= 1 or len(aligned_layouts) != len(aligned_shapes): 

134 return ((copy.deepcopy(valid_layouts[0]),), None) 

135 

136 output_shape = self._compute_output_shape(aligned_shapes) 

137 merged_tensor_map, merged_partial = self._merge_all_layouts( 

138 aligned_layouts, 

139 aligned_shapes, 

140 output_shape, 

141 layouts 

142 ) 

143 

144 self._check_all_inputs_broadcasts_and_partial(aligned_layouts, aligned_shapes, output_shape) 

145 

146 output_layout = self._create_output_layout(aligned_layouts[0], merged_tensor_map, merged_partial) 

147 return ((output_layout,), None) 

148 

149 def _handle_no_input_shapes(self, valid_layouts): 

150 """ 

151 Handle the case when input shapes are not available. 

152 """ 

153 first_layout = valid_layouts[0] 

154 first_alias_map = first_layout.alias_tensor_map 

155 for layout in valid_layouts[1:]: 

156 if layout.alias_tensor_map != first_alias_map: 

157 raise ValueError( 

158 f"For {self.op_name}, cannot infer layout without shapes: " 

159 f"mismatched alias_tensor_map {first_alias_map} vs {layout.alias_tensor_map}." 

160 ) 

161 return copy.deepcopy(first_layout) 

162 

163 def _align_layouts_and_shapes(self, layouts, input_shapes): 

164 """ 

165 Align layouts with shapes by position, skipping None layouts. 

166 """ 

167 aligned_layouts = [] 

168 aligned_shapes = [] 

169 for layout, shape in zip(layouts, input_shapes): 

170 if layout is None: 

171 continue 

172 aligned_layouts.append(layout) 

173 aligned_shapes.append(shape) 

174 return aligned_layouts, aligned_shapes 

175 

176 def _compute_output_shape(self, aligned_shapes): 

177 """ 

178 Compute broadcasted output shape from all input shapes. 

179 """ 

180 output_shape = aligned_shapes[0] 

181 for shape in aligned_shapes[1:]: 

182 output_shape = self._broadcast_shapes(output_shape, shape) 

183 return output_shape 

184 

185 def _merge_all_layouts(self, aligned_layouts, aligned_shapes, output_shape, layouts): 

186 """ 

187 Merge all input layouts sequentially to get final tensor_map and partial status. 

188 """ 

189 base_layout = aligned_layouts[0] 

190 

191 merged_tensor_map = self._merge_tensor_maps_for_broadcast( 

192 aligned_layouts[0], 

193 aligned_layouts[1], 

194 aligned_shapes[0], 

195 aligned_shapes[1], 

196 output_shape 

197 ) 

198 

199 merged_partial = self._merge_partial_status( 

200 base_layout.partial, 

201 aligned_layouts[1].partial, 

202 merged_tensor_map, 

203 aligned_layouts[0].tensor_map if aligned_layouts[0].tensor_map else tuple(), 

204 aligned_layouts[1].tensor_map if aligned_layouts[1].tensor_map else tuple(), 

205 layouts 

206 ) 

207 

208 for i in range(2, len(aligned_layouts)): 

209 temp_layout = self._create_output_layout(base_layout, merged_tensor_map, merged_partial) 

210 merged_tensor_map = self._merge_tensor_maps_for_broadcast( 

211 temp_layout, 

212 aligned_layouts[i], 

213 output_shape, 

214 aligned_shapes[i], 

215 output_shape 

216 ) 

217 merged_partial = self._merge_partial_status( 

218 merged_partial, 

219 aligned_layouts[i].partial, 

220 merged_tensor_map, 

221 temp_layout.tensor_map if temp_layout.tensor_map else tuple(), 

222 aligned_layouts[i].tensor_map if aligned_layouts[i].tensor_map else tuple(), 

223 layouts 

224 ) 

225 

226 return merged_tensor_map, merged_partial 

227 

228 def _merge_partial_status(self, partial1, partial2, merged_tensor_map, tensor_map1, tensor_map2, layouts): 

229 """ 

230 Merge partial status from two inputs. 

231 

232 Rules: 

233 1. Both None → None 

234 2. One None → Use the other 

235 3. Both not None and same → Use it 

236 4. Both not None and different → Error 

237 5. Check Shard + Partial conflicts for each input 

238 

239 Args: 

240 partial1: Partial status list from first input 

241 partial2: Partial status list from second input 

242 merged_tensor_map: Merged tensor map for output 

243 tensor_map1: Tensor map of first input 

244 tensor_map2: Tensor map of second input 

245 

246 Returns: 

247 List: Merged partial status 

248 

249 Raises: 

250 ValueError: If partial operations conflict or Shard+Partial conflict found 

251 """ 

252 # Check Shard + Partial conflicts for input1 

253 self._check_shard_partial_conflict(tensor_map1, partial1, layouts) 

254 

255 # Check Shard + Partial conflicts for input2 

256 self._check_shard_partial_conflict(tensor_map2, partial2, layouts) 

257 

258 # Determine mesh dimension from partial lists 

259 mesh_dim = max(len(partial1) if partial1 else 0, len(partial2) if partial2 else 0) 

260 

261 merged_partial = [None] * mesh_dim 

262 

263 for i in range(mesh_dim): 

264 op1 = partial1[i] if partial1 and i < len(partial1) else None 

265 op2 = partial2[i] if partial2 and i < len(partial2) else None 

266 

267 # Both have partial status with different operations 

268 if op1 is not None and op2 is not None and op1 != op2: 

269 raise ValueError( 

270 f"For {self.op_name}, partial operations should be same for device axis {i}, " 

271 f"but got {op1} and {op2}" 

272 ) 

273 

274 # Merge: prefer non-None, or either if both same 

275 if op1 is not None: 

276 merged_partial[i] = op1 

277 elif op2 is not None: 

278 merged_partial[i] = op2 

279 

280 # Check final output for Shard + Partial conflicts 

281 self._check_shard_partial_conflict(merged_tensor_map, merged_partial, layouts) 

282 

283 return merged_partial 

284 

285 def _check_shard_partial_conflict(self, tensor_map, partial_list, layouts): 

286 """ 

287 Check for conflicts between Shard and Partial on same device axis. 

288 

289 Args: 

290 tensor_map: Tensor map to check 

291 partial_list: Partial status list 

292 

293 Raises: 

294 ValueError: If Shard and Partial conflict found 

295 """ 

296 if not partial_list: 

297 return 

298 

299 mesh_dim = len(partial_list) 

300 

301 # Collect all device axis used for sharding 

302 sharded_axis = set() 

303 if tensor_map: 

304 for map_val in tensor_map: 

305 if isinstance(map_val, tuple): 

306 for sub_val in map_val: 

307 if sub_val != -1: 

308 # Convert to device axis index 

309 axis_idx = mesh_dim - 1 - sub_val 

310 sharded_axis.add(axis_idx) 

311 elif map_val != -1: 

312 axis_idx = mesh_dim - 1 - map_val 

313 sharded_axis.add(axis_idx) 

314 

315 # Check if any sharded axis has partial status 

316 for axis_idx in sharded_axis: 

317 if 0 <= axis_idx < len(partial_list) and partial_list[axis_idx] is not None: 

318 raise ValueError( 

319 f"For {self.op_name}, Shard and Partial should not coexist on same device axis " 

320 f"{axis_idx}, but got Partial({partial_list[axis_idx]}). " 

321 f"Please check layouts: {layouts}." 

322 ) 

323 

324 def _check_all_inputs_broadcasts_and_partial(self, layouts, input_shapes, output_shape): 

325 """ 

326 Check if any input broadcasts and has Partial status. 

327 """ 

328 for i, (layout, input_shape) in enumerate(zip(layouts, input_shapes)): 

329 if layout is None: 

330 continue 

331 

332 input_name = f"input{i+1}" 

333 

334 input_len = len(input_shape) 

335 output_len = len(output_shape) 

336 

337 if input_len < output_len: 

338 aligned_input_shape = (1,) * (output_len - input_len) + tuple(input_shape) 

339 else: 

340 aligned_input_shape = input_shape 

341 

342 broadcasts = False 

343 for in_dim, out_dim in zip(aligned_input_shape, output_shape): 

344 if in_dim == 1 and out_dim > 1: 

345 broadcasts = True 

346 break 

347 

348 if broadcasts and layout.is_partial(): 

349 raise ValueError( 

350 f"For {self.op_name}, {input_name} has Partial status and broadcasts. " 

351 f"Should be without Partial status for broadcasting without communication" 

352 ) 

353 

354 def _broadcast_shapes(self, shape1, shape2): 

355 """ 

356 Calculate the broadcasted shape of two shapes according to broadcasting rules. 

357 

358 Broadcasting rules: 

359 1. If two arrays have different numbers of dimensions, pad the shape of the 

360 lower-dimensional array with 1s on the left until both shapes have the same length. 

361 2. If two arrays have the same number of dimensions but different lengths in some 

362 dimensions, dimensions with length 1 will be expanded to match the other array's 

363 dimension length. 

364 3. If two arrays have the same number of dimensions but any dimension has different 

365 lengths and neither is 1, raise an error. 

366 

367 Args: 

368 shape1 (tuple): Shape of the first tensor, e.g., (3, 1, 5) 

369 shape2 (tuple): Shape of the second tensor, e.g., (4, 5) 

370 

371 Returns: 

372 tuple: Broadcasted shape, e.g., (3, 4, 5) 

373 

374 Raises: 

375 ValueError: If shapes cannot be broadcast together. 

376 """ 

377 # Rule 1: Right-align, pad with 1s on the left to make dimensions equal 

378 len1, len2 = len(shape1), len(shape2) 

379 max_len = max(len1, len2) 

380 

381 padded_shape1 = (1,) * (max_len - len1) + tuple(shape1) 

382 padded_shape2 = (1,) * (max_len - len2) + tuple(shape2) 

383 

384 # Rules 2 and 3: Check if each dimension can be broadcast 

385 result_shape = [] 

386 for dim1, dim2 in zip(padded_shape1, padded_shape2): 

387 if dim1 == dim2: 

388 # Dimensions are the same, use directly 

389 result_shape.append(dim1) 

390 elif dim1 == 1: 

391 # First shape has 1 in this dimension, expand to dim2 

392 result_shape.append(dim2) 

393 elif dim2 == 1: 

394 # Second shape has 1 in this dimension, expand to dim1 

395 result_shape.append(dim1) 

396 else: 

397 # Rule 3: Dimensions are different and neither is 1, cannot broadcast 

398 raise ValueError( 

399 f"For {self.op_name}, shapes {shape1} and {shape2} cannot be broadcast together. " 

400 f"Dimension mismatch: {dim1} vs {dim2}" 

401 ) 

402 

403 return tuple(result_shape) 

404 

405 def _align_tensor_maps_for_broadcast(self, layout1, layout2, shape1, shape2): 

406 """ 

407 Align tensor_maps of two layouts to support broadcasting. 

408 

409 When two tensors have different dimensions, the tensor_map of the 

410 lower-dimensional tensor is padded with -1 (indicating no sharding) at the front. 

411 

412 Args: 

413 layout1: Layout of the first tensor 

414 layout2: Layout of the second tensor 

415 shape1 (tuple): Global shape of the first tensor 

416 shape2 (tuple): Global shape of the second tensor 

417 

418 Returns: 

419 tuple: (aligned_map1, aligned_map2) - Aligned tensor_maps 

420 """ 

421 len1, len2 = len(shape1), len(shape2) 

422 max_len = max(len1, len2) 

423 

424 map1 = layout1.tensor_map if layout1.tensor_map else tuple([-1] * len1) 

425 map2 = layout2.tensor_map if layout2.tensor_map else tuple([-1] * len2) 

426 

427 aligned_map1 = (-1,) * (max_len - len1) + map1 

428 aligned_map2 = (-1,) * (max_len - len2) + map2 

429 

430 return aligned_map1, aligned_map2 

431 

432 def _normalize_tensor_map_element(self, map_element): 

433 """ 

434 Normalize a tensor_map element to a tuple of device axis for unified processing. 

435 

436 Args: 

437 map_element: Element from tensor_map, can be: 

438 - int: -1 (no sharding) or device axis index 

439 - tuple: multiple device axis 

440 

441 Returns: 

442 tuple: Tuple of device axis (empty tuple if not sharded) 

443 """ 

444 if map_element == -1: 

445 return () 

446 if isinstance(map_element, int): 

447 return (map_element,) 

448 if isinstance(map_element, tuple): 

449 return tuple(dim for dim in map_element if dim != -1) 

450 return () 

451 

452 def _denormalize_tensor_map_element(self, device_axis_tuple): 

453 """ 

454 Convert a tuple of device axis back to tensor_map element format. 

455 

456 Args: 

457 device_axis_tuple (tuple): Tuple of device axis 

458 

459 Returns: 

460 int or tuple: -1 if empty, single int if one element, tuple if multiple elements 

461 """ 

462 if not device_axis_tuple: 

463 return -1 

464 if len(device_axis_tuple) == 1: 

465 return device_axis_tuple[0] 

466 return device_axis_tuple 

467 

468 def _merge_tensor_maps_for_broadcast(self, layout1, layout2, shape1, shape2, output_shape): 

469 """ 

470 Merge tensor_maps of two inputs to generate output tensor_map. 

471 

472 This method handles both simple int-type and complex tuple-type tensor_map elements, 

473 ensuring correct sharding strategy for the broadcasted output. 

474 

475 Args: 

476 layout1: Layout of the first input 

477 layout2: Layout of the second input 

478 shape1 (tuple): Global shape of the first input 

479 shape2 (tuple): Global shape of the second input 

480 output_shape (tuple): Global shape of the output 

481 

482 Returns: 

483 tuple: Merged tensor_map for the output 

484 

485 Raises: 

486 ValueError: If sharding strategies conflict or broadcasting dimension is sharded 

487 """ 

488 map1, map2 = self._align_tensor_maps_for_broadcast(layout1, layout2, shape1, shape2) 

489 

490 len1, len2 = len(shape1), len(shape2) 

491 max_len = len(output_shape) 

492 padded_shape1 = (1,) * (max_len - len1) + tuple(shape1) 

493 padded_shape2 = (1,) * (max_len - len2) + tuple(shape2) 

494 

495 merged_map = [] 

496 for i, (dim1, dim2, out_dim) in enumerate(zip(padded_shape1, padded_shape2, output_shape)): 

497 m1, m2 = map1[i], map2[i] 

498 

499 m1_axis = self._normalize_tensor_map_element(m1) 

500 m2_axis = self._normalize_tensor_map_element(m2) 

501 

502 m1_axis_for_compare = frozenset(m1_axis) 

503 m2_axis_for_compare = frozenset(m2_axis) 

504 

505 m1_is_sharded = bool(m1_axis) 

506 m2_is_sharded = bool(m2_axis) 

507 

508 if not m1_is_sharded and not m2_is_sharded: 

509 merged_map.append(-1) 

510 

511 elif not m1_is_sharded: 

512 if dim2 == 1 and out_dim > 1: 

513 raise ValueError( 

514 f"For {self.op_name}, dimension {i} of second input has size 1 " 

515 f"but is sharded on device axis {m2_axis}. " 

516 f"Broadcasting dimension cannot be sharded." 

517 ) 

518 merged_map.append(self._denormalize_tensor_map_element(m2_axis)) 

519 

520 elif not m2_is_sharded: 

521 if dim1 == 1 and out_dim > 1: 

522 raise ValueError( 

523 f"For {self.op_name}, dimension {i} of first input has size 1 " 

524 f"but is sharded on device axis {m1_axis}. " 

525 f"Broadcasting dimension cannot be sharded." 

526 ) 

527 merged_map.append(self._denormalize_tensor_map_element(m1_axis)) 

528 

529 else: 

530 if m1_axis_for_compare != m2_axis_for_compare: 

531 raise ValueError( 

532 f"For {self.op_name}, inputs should have same sharding pattern, " 

533 f"but got confilcting sharding at dimension {i}, " 

534 f"input1 shaded on {m1_axis} and input2 shaded on {m2_axis}." 

535 ) 

536 

537 if (dim1 == 1 or dim2 == 1) and dim1 != dim2: 

538 raise ValueError( 

539 f"For {self.op_name}, dimension {i} is broadcast from size 1 " 

540 f"to {out_dim} but is sharded on device axis {m1_axis}. " 

541 f"Broadcasting dimension cannot be sharded." 

542 ) 

543 

544 merged_map.append(self._denormalize_tensor_map_element(m1_axis)) 

545 

546 return tuple(merged_map) 

547 

548 def _create_output_layout(self, base_layout, output_tensor_map, partial_list=None): 

549 """ 

550 Create output layout based on input layout. 

551 

552 Args: 

553 base_layout: Base layout (usually from the first input) 

554 output_tensor_map (tuple): Tensor_map for the output 

555 partial_list (list): Partial status list for the output 

556 

557 Returns: 

558 Layout: New Layout object with updated tensor_map and alias_tensor_map 

559 """ 

560 new_layout = copy.deepcopy(base_layout) 

561 new_layout.set_tensor_map(output_tensor_map) 

562 

563 alias_tensor_map = [] 

564 for tensor_dim in output_tensor_map: 

565 if tensor_dim == -1: 

566 alias_tensor_map.append("None") 

567 elif isinstance(tensor_dim, tuple): 

568 alias_tuple = tuple( 

569 base_layout.alias_name[len(base_layout.alias_name) - 1 - dim] 

570 for dim in tensor_dim 

571 if dim != -1 

572 ) 

573 alias_tensor_map.append(alias_tuple if alias_tuple else "None") 

574 else: 

575 alias_tensor_map.append( 

576 base_layout.alias_name[len(base_layout.alias_name) - 1 - tensor_dim] 

577 ) 

578 

579 new_layout.set_alias_tensor_map(tuple(alias_tensor_map)) 

580 

581 # Set partial status if provided 

582 if partial_list: 

583 for i, partial_op in enumerate(partial_list): 

584 if partial_op is not None and i < len(new_layout.alias_name): 

585 new_layout.set_partial_by_dev_axis(new_layout.alias_name[i], partial_op) 

586 

587 return new_layout 

588 

589 

590class ElementWiseWithPartialDistributedOp(ElementWiseDistributedOp): 

591 """ 

592 Base class for elementwise operations that support partial status propagation. 

593 """ 

594 def __init__(self, op_name): 

595 super().__init__(op_name) 

596 self._allow_partial_inputs = True 

597 

598 

599class AddDistributedOp(ElementWiseWithPartialDistributedOp): 

600 """ 

601 Distributed implementation for Add operator. 

602 

603 This operator supports partial status propagation from inputs to output, 

604 which is useful for operations like gradient accumulation where partial 

605 results need to be preserved through the computation graph. 

606 """ 

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

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

640 cache_values: list) -> Optional[Callable]: 

641 """ 

642 Get expand implementation for the operator 

643 """ 

644 layouts = tuple(cache_values[:-1]) 

645 x1_layout = layouts[0] 

646 x2_layout = layouts[1] 

647 x1_partial = x1_layout.is_partial() if x1_layout is not None else False 

648 x2_partial = x2_layout.is_partial() if x2_layout is not None else False 

649 

650 if x1_partial == x2_partial: 

651 return None 

652 

653 output_layout = infer_result[0][0] 

654 scaling_factor = 1 

655 for i, partial_type in enumerate(output_layout.partial): 

656 if partial_type == "sum": 

657 scaling_factor *= output_layout.mesh_shape[i] 

658 

659 # use expand_impl only when one of x1 and x2 is with partial placement. 

660 def _expand_impl1(x1, x2, *args, **kwargs): 

661 add_out = func(x1 / scaling_factor, x2, *args, **kwargs) 

662 return add_out 

663 

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