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

307 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-07-13 05:07 +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 

25_INPLACE_ELEMENTWISE_OPS = frozenset({ 

26 "add_", "sub_", "InplaceAddExt", "InplaceSubExt", 

27}) 

28 

29 

30def _partial_signature(layout, mesh_ndim: int) -> tuple: 

31 """Return one Partial entry per output mesh dimension.""" 

32 if layout is None: 

33 return (None,) * mesh_ndim 

34 partial = tuple(layout.partial) 

35 if len(partial) != mesh_ndim: 

36 raise ValueError( 

37 f"Input and output mesh dimensions must match, but got " 

38 f"input={len(partial)} and output={mesh_ndim}." 

39 ) 

40 return partial 

41 

42 

43def _contributes_to_partial_output(input_layout, output_layout) -> bool: 

44 """Return whether this rank contributes an input to a Partial output. 

45 

46 An input that is not Partial on one of the output's Partial axes is 

47 replicated along that axis. Only coordinate zero may contribute that 

48 replicated value, otherwise the eventual reduction would count it once 

49 per rank on the added axis. 

50 """ 

51 output_partial = tuple(output_layout.partial) 

52 input_partial = _partial_signature(input_layout, len(output_partial)) 

53 for mesh_dim, output_partial_type in enumerate(output_partial): 

54 if ( 

55 output_partial_type is not None 

56 and input_partial[mesh_dim] is None 

57 and output_layout.mesh.get_local_rank(mesh_dim) != 0 

58 ): 

59 return False 

60 return True 

61 

62 

63def _zero_contribution(value): 

64 """Create a strict zero while retaining a floating tensor's grad edge.""" 

65 is_complex = getattr(value, "is_complex", None) 

66 if callable(is_complex) and is_complex(): 

67 return value * 0 

68 if hasattr(value, "clamp"): 

69 # ``value * 0`` turns +/-Inf into NaN. Clamp first so the forward value 

70 # is finite, then multiply by zero to make the derivative zero even at 

71 # the clamp boundary. 

72 return value.clamp(0, 0) * 0 

73 if isinstance(value, (bool, int, float, complex)): 

74 return type(value)(0) 

75 return value * 0 

76 

77 

78def _unwrap_local_value(value): 

79 """Convert DTensor-like values to local tensors while preserving containers.""" 

80 if hasattr(value, "_layout"): 

81 return value.to_local() 

82 if isinstance(value, tuple): 

83 return tuple(_unwrap_local_value(item) for item in value) 

84 if isinstance(value, list): 

85 return [_unwrap_local_value(item) for item in value] 

86 return value 

87 

88 

89def _collect_layout_and_shape(value): 

90 """Collect layout and shape from one argument for layout inference cache.""" 

91 layout = value.layout if hasattr(value, "_layout") else None 

92 shape = value.shape if hasattr(value, "shape") else None 

93 return layout, shape 

94 

95 

96def _build_elementwise_cache_values(args, kwargs): 

97 """Build cache_values from the real element-wise input arguments.""" 

98 input_layouts = [] 

99 input_shapes = [] 

100 for value in args: 

101 layout, shape = _collect_layout_and_shape(value) 

102 input_layouts.append(layout) 

103 input_shapes.append(shape) 

104 for value in kwargs.values(): 

105 layout, shape = _collect_layout_and_shape(value) 

106 input_layouts.append(layout) 

107 input_shapes.append(shape) 

108 return [*input_layouts, input_shapes] 

109 

110 

111class ElementWiseDistributedOp(DistributedOp): 

112 """ 

113 Base class for distributed element-wise operators. 

114 

115 Supports broadcasting following broadcasting rules and handles 

116 distributed tensor layouts with proper sharding strategy inference. 

117 

118 Args: 

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

120 """ 

121 

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

123 """ 

124 Preprocess arguments for element-wise operators. 

125 

126 NOTE: aclop packed-args normalization (for MindSpore aclop operators 

127 like Mod, StopGradient that pack args as ``(prim, name, (real_args...))``) 

128 is handled upstream in ``OpDispatcher._dispatch_layout_infer`` via 

129 ``_normalize_aclop_args``. This method receives clean unpacked args. 

130 

131 Args: 

132 args (tuple): Positional arguments passed to the operator. 

133 kwargs (dict): Keyword arguments passed to the operator. 

134 

135 Returns: 

136 tuple: (local_args, local_kwargs, cache_values) 

137 """ 

138 local_kwargs = {key: _unwrap_local_value(value) for key, value in kwargs.items()} 

139 

140 local_args = tuple(_unwrap_local_value(arg) for arg in args) 

141 cache_values = _build_elementwise_cache_values(args, kwargs) 

142 return local_args, local_kwargs, cache_values 

143 

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

145 """ 

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

147 

148 Rules: 

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

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

151 3. Broadcasting dimensions cannot be sharded. 

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

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

154 

155 Args: 

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

157 

158 Returns: 

159 tuple: ((output_layout,), None) 

160 

161 Raises: 

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

163 """ 

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

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

166 

167 if not layouts: 

168 return None 

169 

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

171 

172 if not valid_layouts: 

173 return None 

174 

175 if not self._allow_partial_inputs: 

176 self._check_partial_inputs(layouts) 

177 

178 if len(valid_layouts) == 1: 

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

180 

181 if not input_shapes: 

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

183 

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

185 

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

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

188 

189 output_shape = self._compute_output_shape(aligned_shapes) 

190 merged_tensor_map, merged_partial = self._merge_all_layouts( 

191 aligned_layouts, 

192 aligned_shapes, 

193 output_shape, 

194 layouts 

195 ) 

196 

197 self._check_all_inputs_broadcasts_and_partial(aligned_layouts, aligned_shapes, output_shape) 

198 

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

200 return ((output_layout,), None) 

201 

202 def _handle_no_input_shapes(self, valid_layouts): 

203 """ 

204 Handle the case when input shapes are not available. 

205 """ 

206 first_layout = valid_layouts[0] 

207 first_alias_map = first_layout.alias_tensor_map 

208 for layout in valid_layouts[1:]: 

209 if layout.alias_tensor_map != first_alias_map: 

210 raise ValueError( 

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

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

213 ) 

214 return copy.deepcopy(first_layout) 

215 

216 def _align_layouts_and_shapes(self, layouts, input_shapes): 

217 """ 

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

219 """ 

220 aligned_layouts = [] 

221 aligned_shapes = [] 

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

223 if layout is None: 

224 continue 

225 aligned_layouts.append(layout) 

226 aligned_shapes.append(shape) 

227 return aligned_layouts, aligned_shapes 

228 

229 def _compute_output_shape(self, aligned_shapes): 

230 """ 

231 Compute broadcasted output shape from all input shapes. 

232 """ 

233 output_shape = aligned_shapes[0] 

234 for shape in aligned_shapes[1:]: 

235 output_shape = self._broadcast_shapes(output_shape, shape) 

236 return output_shape 

237 

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

239 """ 

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

241 """ 

242 base_layout = aligned_layouts[0] 

243 

244 merged_tensor_map = self._merge_tensor_maps_for_broadcast( 

245 aligned_layouts[0], 

246 aligned_layouts[1], 

247 aligned_shapes[0], 

248 aligned_shapes[1], 

249 output_shape 

250 ) 

251 

252 merged_partial = self._merge_partial_status( 

253 base_layout.partial, 

254 aligned_layouts[1].partial, 

255 merged_tensor_map, 

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

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

258 layouts 

259 ) 

260 

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

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

263 merged_tensor_map = self._merge_tensor_maps_for_broadcast( 

264 temp_layout, 

265 aligned_layouts[i], 

266 output_shape, 

267 aligned_shapes[i], 

268 output_shape 

269 ) 

270 merged_partial = self._merge_partial_status( 

271 merged_partial, 

272 aligned_layouts[i].partial, 

273 merged_tensor_map, 

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

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

276 layouts 

277 ) 

278 

279 return merged_tensor_map, merged_partial 

280 

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

282 """ 

283 Merge partial status from two inputs. 

284 

285 Rules: 

286 1. Both None → None 

287 2. One None → Use the other 

288 3. Both not None and same → Use it 

289 4. Both not None and different → Error 

290 5. Check Shard + Partial conflicts for each input 

291 

292 Args: 

293 partial1: Partial status list from first input 

294 partial2: Partial status list from second input 

295 merged_tensor_map: Merged tensor map for output 

296 tensor_map1: Tensor map of first input 

297 tensor_map2: Tensor map of second input 

298 

299 Returns: 

300 List: Merged partial status 

301 

302 Raises: 

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

304 """ 

305 # Check Shard + Partial conflicts for input1 

306 self._check_shard_partial_conflict(tensor_map1, partial1, layouts) 

307 

308 # Check Shard + Partial conflicts for input2 

309 self._check_shard_partial_conflict(tensor_map2, partial2, layouts) 

310 

311 # Determine mesh dimension from partial lists 

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

313 

314 merged_partial = [None] * mesh_dim 

315 

316 for i in range(mesh_dim): 

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

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

319 

320 # Both have partial status with different operations 

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

322 raise ValueError( 

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

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

325 ) 

326 

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

328 if op1 is not None: 

329 merged_partial[i] = op1 

330 elif op2 is not None: 

331 merged_partial[i] = op2 

332 

333 # Check final output for Shard + Partial conflicts 

334 self._check_shard_partial_conflict(merged_tensor_map, merged_partial, layouts) 

335 

336 return merged_partial 

337 

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

339 """ 

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

341 

342 Args: 

343 tensor_map: Tensor map to check 

344 partial_list: Partial status list 

345 

346 Raises: 

347 ValueError: If Shard and Partial conflict found 

348 """ 

349 if not partial_list: 

350 return 

351 

352 mesh_dim = len(partial_list) 

353 

354 # Collect all device axis used for sharding 

355 sharded_axis = set() 

356 if tensor_map: 

357 for map_val in tensor_map: 

358 if isinstance(map_val, tuple): 

359 for sub_val in map_val: 

360 if sub_val != -1: 

361 # Convert to device axis index 

362 axis_idx = mesh_dim - 1 - sub_val 

363 sharded_axis.add(axis_idx) 

364 elif map_val != -1: 

365 axis_idx = mesh_dim - 1 - map_val 

366 sharded_axis.add(axis_idx) 

367 

368 # Check if any sharded axis has partial status 

369 for axis_idx in sharded_axis: 

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

371 raise ValueError( 

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

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

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

375 ) 

376 

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

378 """ 

379 Check if any input broadcasts and has Partial status. 

380 """ 

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

382 if layout is None: 

383 continue 

384 

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

386 

387 input_len = len(input_shape) 

388 output_len = len(output_shape) 

389 

390 if input_len < output_len: 

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

392 else: 

393 aligned_input_shape = input_shape 

394 

395 broadcasts = False 

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

397 if in_dim == 1 and out_dim > 1: 

398 broadcasts = True 

399 break 

400 

401 if broadcasts and layout.is_partial(): 

402 raise ValueError( 

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

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

405 ) 

406 

407 def _broadcast_shapes(self, shape1, shape2): 

408 """ 

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

410 

411 Broadcasting rules: 

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

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

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

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

416 dimension length. 

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

418 lengths and neither is 1, raise an error. 

419 

420 Args: 

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

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

423 

424 Returns: 

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

426 

427 Raises: 

428 ValueError: If shapes cannot be broadcast together. 

429 """ 

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

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

432 max_len = max(len1, len2) 

433 

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

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

436 

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

438 result_shape = [] 

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

440 if dim1 == dim2: 

441 # Dimensions are the same, use directly 

442 result_shape.append(dim1) 

443 elif dim1 == 1: 

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

445 result_shape.append(dim2) 

446 elif dim2 == 1: 

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

448 result_shape.append(dim1) 

449 else: 

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

451 raise ValueError( 

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

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

454 ) 

455 

456 return tuple(result_shape) 

457 

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

459 """ 

460 Align tensor_maps of two layouts to support broadcasting. 

461 

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

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

464 

465 Args: 

466 layout1: Layout of the first tensor 

467 layout2: Layout of the second tensor 

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

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

470 

471 Returns: 

472 tuple: (aligned_map1, aligned_map2) - Aligned tensor_maps 

473 """ 

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

475 max_len = max(len1, len2) 

476 

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

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

479 

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

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

482 

483 return aligned_map1, aligned_map2 

484 

485 def _normalize_tensor_map_element(self, map_element): 

486 """ 

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

488 

489 Args: 

490 map_element: Element from tensor_map, can be: 

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

492 - tuple: multiple device axis 

493 

494 Returns: 

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

496 """ 

497 if map_element == -1: 

498 return () 

499 if isinstance(map_element, int): 

500 return (map_element,) 

501 if isinstance(map_element, tuple): 

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

503 return () 

504 

505 def _denormalize_tensor_map_element(self, device_axis_tuple): 

506 """ 

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

508 

509 Args: 

510 device_axis_tuple (tuple): Tuple of device axis 

511 

512 Returns: 

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

514 """ 

515 if not device_axis_tuple: 

516 return -1 

517 if len(device_axis_tuple) == 1: 

518 return device_axis_tuple[0] 

519 return device_axis_tuple 

520 

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

522 """ 

523 Merge tensor_maps of two inputs to generate output tensor_map. 

524 

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

526 ensuring correct sharding strategy for the broadcasted output. 

527 

528 Args: 

529 layout1: Layout of the first input 

530 layout2: Layout of the second input 

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

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

533 output_shape (tuple): Global shape of the output 

534 

535 Returns: 

536 tuple: Merged tensor_map for the output 

537 

538 Raises: 

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

540 """ 

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

542 

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

544 max_len = len(output_shape) 

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

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

547 

548 merged_map = [] 

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

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

551 

552 m1_axis = self._normalize_tensor_map_element(m1) 

553 m2_axis = self._normalize_tensor_map_element(m2) 

554 

555 m1_axis_for_compare = frozenset(m1_axis) 

556 m2_axis_for_compare = frozenset(m2_axis) 

557 

558 m1_is_sharded = bool(m1_axis) 

559 m2_is_sharded = bool(m2_axis) 

560 

561 if not m1_is_sharded and not m2_is_sharded: 

562 merged_map.append(-1) 

563 

564 elif not m1_is_sharded: 

565 if dim2 == 1 and out_dim > 1: 

566 raise ValueError( 

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

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

569 f"Broadcasting dimension cannot be sharded." 

570 ) 

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

572 

573 elif not m2_is_sharded: 

574 if dim1 == 1 and out_dim > 1: 

575 raise ValueError( 

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

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

578 f"Broadcasting dimension cannot be sharded." 

579 ) 

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

581 

582 else: 

583 if m1_axis_for_compare != m2_axis_for_compare: 

584 raise ValueError( 

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

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

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

588 ) 

589 

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

591 raise ValueError( 

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

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

594 f"Broadcasting dimension cannot be sharded." 

595 ) 

596 

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

598 

599 return tuple(merged_map) 

600 

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

602 """ 

603 Create output layout based on input layout. 

604 

605 Args: 

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

607 output_tensor_map (tuple): Tensor_map for the output 

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

609 

610 Returns: 

611 Layout: New Layout object with updated tensor_map and alias_tensor_map 

612 """ 

613 new_layout = copy.deepcopy(base_layout) 

614 new_layout.set_tensor_map(output_tensor_map) 

615 

616 alias_tensor_map = [] 

617 for tensor_dim in output_tensor_map: 

618 if tensor_dim == -1: 

619 alias_tensor_map.append("None") 

620 elif isinstance(tensor_dim, tuple): 

621 alias_tuple = tuple( 

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

623 for dim in tensor_dim 

624 if dim != -1 

625 ) 

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

627 else: 

628 alias_tensor_map.append( 

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

630 ) 

631 

632 new_layout.set_alias_tensor_map(tuple(alias_tensor_map)) 

633 

634 # Set partial status if provided 

635 if partial_list: 

636 for i, partial_op in enumerate(partial_list): 

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

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

639 

640 return new_layout 

641 

642 

643class ElementWiseWithPartialDistributedOp(ElementWiseDistributedOp): 

644 """ 

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

646 """ 

647 def __init__(self, op_name): 

648 super().__init__(op_name) 

649 self._allow_partial_inputs = True 

650 

651 

652class AddDistributedOp(ElementWiseWithPartialDistributedOp): 

653 """ 

654 Distributed implementation for Add operator. 

655 

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

657 which is useful for operations like gradient accumulation where partial 

658 results need to be preserved through the computation graph. 

659 """ 

660 

661 @staticmethod 

662 def _canonicalize_binary_operands(args: tuple, kwargs: dict) -> tuple[tuple, dict]: 

663 """Move semantic ``input`` and ``other`` operands before option values.""" 

664 local_kwargs = dict(kwargs) 

665 if args: 

666 if "input" in local_kwargs: 

667 raise ValueError("Binary elementwise input was provided both positionally and by keyword.") 

668 lhs = args[0] 

669 trailing_args = args[1:] 

670 elif "input" in local_kwargs: 

671 lhs = local_kwargs.pop("input") 

672 trailing_args = () 

673 else: 

674 return args, local_kwargs 

675 

676 if trailing_args: 

677 if "other" in local_kwargs: 

678 raise ValueError("Binary elementwise other was provided both positionally and by keyword.") 

679 rhs = trailing_args[0] 

680 trailing_args = trailing_args[1:] 

681 elif "other" in local_kwargs: 

682 rhs = local_kwargs.pop("other") 

683 else: 

684 return args, local_kwargs 

685 return (lhs, rhs, *trailing_args), local_kwargs 

686 

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

688 """Canonicalize binary operands before local unwrapping and layout caching. 

689 

690 Args: 

691 args: Positional backend arguments. 

692 kwargs: Keyword backend arguments, potentially including 

693 ``input``, ``other``, and options such as ``alpha``. 

694 

695 Returns: 

696 Local arguments, local keyword arguments, and layout cache values 

697 with the semantic left and right operands in the first two slots. 

698 """ 

699 canonical_args, canonical_kwargs = self._canonicalize_binary_operands(args, kwargs) 

700 return super().preprocess(canonical_args, canonical_kwargs) 

701 

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

703 """ 

704 Infer output layout for Add operator. 

705 

706 Rules: 

707 1. Follow element-wise broadcasting and sharding merge rules. 

708 2. Partial status is allowed and propagated. 

709 3. Propagated Partial status must be "sum" or None. 

710 

711 Args: 

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

713 

714 Returns: 

715 tuple: ((output_layout,), None) 

716 

717 Raises: 

718 ValueError: If propagated Partial status is not "sum" or None. 

719 """ 

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

721 if self.op_name in _INPLACE_ELEMENTWISE_OPS and len(layouts) >= 2: 

722 mesh_ndim = max( 

723 (len(layout.partial) for layout in layouts[:2] if layout is not None), 

724 default=0, 

725 ) 

726 partial_signatures = tuple( 

727 _partial_signature(layout, mesh_ndim) 

728 for layout in layouts[:2] 

729 ) 

730 if partial_signatures[0] != partial_signatures[1]: 

731 raise ValueError( 

732 f"For {self.op_name}, input Partial placements should be identical " 

733 f"for in-place execution, but got {partial_signatures}." 

734 ) 

735 

736 infer_result = super().infer_layout(cache_values) 

737 if infer_result is None: 

738 return infer_result 

739 

740 output_layout = infer_result[0][0] 

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

742 if partial_type is not None and partial_type != "sum": 

743 raise ValueError( 

744 f"For {self.op_name}, inputs partial status should be 'sum' or None, " 

745 f"but got {partial_type} at index {i}." 

746 ) 

747 

748 return infer_result 

749 

750 def get_expand_impl(self, func: Optional[Callable], infer_result: tuple, # pylint: disable=W0221 

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

752 """Build a rank-local implementation that avoids repeated values. 

753 

754 Args: 

755 func: Local backend add or subtract implementation. 

756 infer_result: Inferred output layout and auxiliary result. 

757 cache_values: Input layouts followed by input shapes. 

758 

759 Returns: 

760 A contribution-aware local implementation, or ``None`` when both 

761 inputs already have identical Partial placements. 

762 """ 

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

764 x1_layout = layouts[0] 

765 x2_layout = layouts[1] 

766 output_layout = infer_result[0][0] 

767 output_mesh_ndim = len(output_layout.partial) 

768 x1_partial = _partial_signature(x1_layout, output_mesh_ndim) 

769 x2_partial = _partial_signature(x2_layout, output_mesh_ndim) 

770 

771 if x1_partial == x2_partial: 

772 return None 

773 

774 x1_contributes = _contributes_to_partial_output(x1_layout, output_layout) 

775 x2_contributes = _contributes_to_partial_output(x2_layout, output_layout) 

776 

777 # ``*args``/``**kwargs`` forward add-specific extras such as ``alpha``. 

778 # A strict zero retains a valid zero-gradient autograd edge. 

779 def _expand_impl(x1, x2, *args, **kwargs): 

780 local_x1 = x1 if x1_contributes else _zero_contribution(x1) 

781 local_x2 = x2 if x2_contributes else _zero_contribution(x2) 

782 return func(local_x1, local_x2, *args, **kwargs) 

783 

784 return _expand_impl 

785 

786 

787class SubDistributedOp(AddDistributedOp): 

788 """Distributed subtraction with correct replicated-minus-partial signs. 

789 

790 When the first operand is replicated and the second is Partial(sum), only 

791 the first Partial rank may contribute the replicated value. Every other 

792 rank must contribute ``0 - x2_local`` so reducing the output reconstructs 

793 ``x1 - sum(x2_local)``. 

794 """