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

308 statements  

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

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

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

129 

130 Returns: 

131 tuple: (local_args, local_kwargs, cache_values) 

132 """ 

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

134 

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

136 cache_values = _build_elementwise_cache_values(args, kwargs) 

137 return local_args, local_kwargs, cache_values 

138 

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

140 """ 

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

142 

143 Rules: 

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

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

146 3. Broadcasting dimensions cannot be sharded. 

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

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

149 

150 Args: 

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

152 

153 Returns: 

154 tuple: ((output_layout,), None) 

155 

156 Raises: 

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

158 """ 

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

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

161 

162 if not layouts: 

163 return None 

164 

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

166 

167 if not valid_layouts: 

168 return None 

169 

170 if not self._allow_partial_inputs: 

171 self._check_partial_inputs(layouts) 

172 

173 if len(valid_layouts) == 1: 

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

175 

176 if not input_shapes: 

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

178 

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

180 

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

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

183 

184 output_shape = self._compute_output_shape(aligned_shapes) 

185 merged_tensor_map, merged_partial = self._merge_all_layouts( 

186 aligned_layouts, 

187 aligned_shapes, 

188 output_shape, 

189 layouts 

190 ) 

191 

192 self._check_all_inputs_broadcasts_and_partial(aligned_layouts, aligned_shapes, output_shape) 

193 

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

195 return ((output_layout,), None) 

196 

197 def _handle_no_input_shapes(self, valid_layouts): 

198 """ 

199 Handle the case when input shapes are not available. 

200 """ 

201 first_layout = valid_layouts[0] 

202 first_alias_map = first_layout.alias_tensor_map 

203 for layout in valid_layouts[1:]: 

204 if layout.alias_tensor_map != first_alias_map: 

205 raise ValueError( 

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

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

208 ) 

209 return copy.deepcopy(first_layout) 

210 

211 def _align_layouts_and_shapes(self, layouts, input_shapes): 

212 """ 

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

214 """ 

215 aligned_layouts = [] 

216 aligned_shapes = [] 

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

218 if layout is None: 

219 continue 

220 aligned_layouts.append(layout) 

221 aligned_shapes.append(shape) 

222 return aligned_layouts, aligned_shapes 

223 

224 def _compute_output_shape(self, aligned_shapes): 

225 """ 

226 Compute broadcasted output shape from all input shapes. 

227 """ 

228 output_shape = aligned_shapes[0] 

229 for shape in aligned_shapes[1:]: 

230 output_shape = self._broadcast_shapes(output_shape, shape) 

231 return output_shape 

232 

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

234 """ 

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

236 """ 

237 base_layout = aligned_layouts[0] 

238 

239 merged_tensor_map = self._merge_tensor_maps_for_broadcast( 

240 aligned_layouts[0], 

241 aligned_layouts[1], 

242 aligned_shapes[0], 

243 aligned_shapes[1], 

244 output_shape 

245 ) 

246 

247 merged_partial = self._merge_partial_status( 

248 base_layout.partial, 

249 aligned_layouts[1].partial, 

250 merged_tensor_map, 

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

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

253 layouts 

254 ) 

255 

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

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

258 merged_tensor_map = self._merge_tensor_maps_for_broadcast( 

259 temp_layout, 

260 aligned_layouts[i], 

261 output_shape, 

262 aligned_shapes[i], 

263 output_shape 

264 ) 

265 merged_partial = self._merge_partial_status( 

266 merged_partial, 

267 aligned_layouts[i].partial, 

268 merged_tensor_map, 

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

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

271 layouts 

272 ) 

273 

274 return merged_tensor_map, merged_partial 

275 

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

277 """ 

278 Merge partial status from two inputs. 

279 

280 Rules: 

281 1. Both None → None 

282 2. One None → Use the other 

283 3. Both not None and same → Use it 

284 4. Both not None and different → Error 

285 5. Check Shard + Partial conflicts for each input 

286 

287 Args: 

288 partial1: Partial status list from first input 

289 partial2: Partial status list from second input 

290 merged_tensor_map: Merged tensor map for output 

291 tensor_map1: Tensor map of first input 

292 tensor_map2: Tensor map of second input 

293 

294 Returns: 

295 List: Merged partial status 

296 

297 Raises: 

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

299 """ 

300 # Check Shard + Partial conflicts for input1 

301 self._check_shard_partial_conflict(tensor_map1, partial1, layouts) 

302 

303 # Check Shard + Partial conflicts for input2 

304 self._check_shard_partial_conflict(tensor_map2, partial2, layouts) 

305 

306 # Determine mesh dimension from partial lists 

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

308 

309 merged_partial = [None] * mesh_dim 

310 

311 for i in range(mesh_dim): 

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

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

314 

315 # Both have partial status with different operations 

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

317 raise ValueError( 

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

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

320 ) 

321 

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

323 if op1 is not None: 

324 merged_partial[i] = op1 

325 elif op2 is not None: 

326 merged_partial[i] = op2 

327 

328 # Check final output for Shard + Partial conflicts 

329 self._check_shard_partial_conflict(merged_tensor_map, merged_partial, layouts) 

330 

331 return merged_partial 

332 

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

334 """ 

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

336 

337 Args: 

338 tensor_map: Tensor map to check 

339 partial_list: Partial status list 

340 

341 Raises: 

342 ValueError: If Shard and Partial conflict found 

343 """ 

344 if not partial_list: 

345 return 

346 

347 mesh_dim = len(partial_list) 

348 

349 # Collect all device axis used for sharding 

350 sharded_axis = set() 

351 if tensor_map: 

352 for map_val in tensor_map: 

353 if isinstance(map_val, tuple): 

354 for sub_val in map_val: 

355 if sub_val != -1: 

356 # Convert to device axis index 

357 axis_idx = mesh_dim - 1 - sub_val 

358 sharded_axis.add(axis_idx) 

359 elif map_val != -1: 

360 axis_idx = mesh_dim - 1 - map_val 

361 sharded_axis.add(axis_idx) 

362 

363 # Check if any sharded axis has partial status 

364 for axis_idx in sharded_axis: 

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

366 raise ValueError( 

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

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

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

370 ) 

371 

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

373 """ 

374 Check if any input broadcasts and has Partial status. 

375 """ 

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

377 if layout is None: 

378 continue 

379 

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

381 

382 input_len = len(input_shape) 

383 output_len = len(output_shape) 

384 

385 if input_len < output_len: 

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

387 else: 

388 aligned_input_shape = input_shape 

389 

390 broadcasts = False 

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

392 if in_dim == 1 and out_dim > 1: 

393 broadcasts = True 

394 break 

395 

396 if broadcasts and layout.is_partial(): 

397 raise ValueError( 

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

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

400 ) 

401 

402 def _broadcast_shapes(self, shape1, shape2): 

403 """ 

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

405 

406 Broadcasting rules: 

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

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

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

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

411 dimension length. 

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

413 lengths and neither is 1, raise an error. 

414 

415 Args: 

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

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

418 

419 Returns: 

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

421 

422 Raises: 

423 ValueError: If shapes cannot be broadcast together. 

424 """ 

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

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

427 max_len = max(len1, len2) 

428 

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

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

431 

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

433 result_shape = [] 

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

435 if dim1 == dim2: 

436 # Dimensions are the same, use directly 

437 result_shape.append(dim1) 

438 elif dim1 == 1: 

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

440 result_shape.append(dim2) 

441 elif dim2 == 1: 

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

443 result_shape.append(dim1) 

444 else: 

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

446 raise ValueError( 

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

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

449 ) 

450 

451 return tuple(result_shape) 

452 

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

454 """ 

455 Align tensor_maps of two layouts to support broadcasting. 

456 

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

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

459 

460 Args: 

461 layout1: Layout of the first tensor 

462 layout2: Layout of the second tensor 

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

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

465 

466 Returns: 

467 tuple: (aligned_map1, aligned_map2) - Aligned tensor_maps 

468 """ 

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

470 max_len = max(len1, len2) 

471 

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

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

474 

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

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

477 

478 return aligned_map1, aligned_map2 

479 

480 def _normalize_tensor_map_element(self, map_element): 

481 """ 

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

483 

484 Args: 

485 map_element: Element from tensor_map, can be: 

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

487 - tuple: multiple device axis 

488 

489 Returns: 

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

491 """ 

492 if map_element == -1: 

493 return () 

494 if isinstance(map_element, int): 

495 return (map_element,) 

496 if isinstance(map_element, tuple): 

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

498 return () 

499 

500 def _denormalize_tensor_map_element(self, device_axis_tuple): 

501 """ 

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

503 

504 Args: 

505 device_axis_tuple (tuple): Tuple of device axis 

506 

507 Returns: 

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

509 """ 

510 if not device_axis_tuple: 

511 return -1 

512 if len(device_axis_tuple) == 1: 

513 return device_axis_tuple[0] 

514 return device_axis_tuple 

515 

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

517 """ 

518 Merge tensor_maps of two inputs to generate output tensor_map. 

519 

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

521 ensuring correct sharding strategy for the broadcasted output. 

522 

523 Args: 

524 layout1: Layout of the first input 

525 layout2: Layout of the second input 

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

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

528 output_shape (tuple): Global shape of the output 

529 

530 Returns: 

531 tuple: Merged tensor_map for the output 

532 

533 Raises: 

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

535 """ 

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

537 

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

539 max_len = len(output_shape) 

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

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

542 

543 merged_map = [] 

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

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

546 

547 m1_axis = self._normalize_tensor_map_element(m1) 

548 m2_axis = self._normalize_tensor_map_element(m2) 

549 

550 m1_axis_for_compare = frozenset(m1_axis) 

551 m2_axis_for_compare = frozenset(m2_axis) 

552 

553 m1_is_sharded = bool(m1_axis) 

554 m2_is_sharded = bool(m2_axis) 

555 

556 if not m1_is_sharded and not m2_is_sharded: 

557 merged_map.append(-1) 

558 

559 elif not m1_is_sharded: 

560 if dim2 == 1 and out_dim > 1: 

561 raise ValueError( 

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

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

564 f"Broadcasting dimension cannot be sharded." 

565 ) 

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

567 

568 elif not m2_is_sharded: 

569 if dim1 == 1 and out_dim > 1: 

570 raise ValueError( 

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

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

573 f"Broadcasting dimension cannot be sharded." 

574 ) 

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

576 

577 else: 

578 if m1_axis_for_compare != m2_axis_for_compare: 

579 raise ValueError( 

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

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

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

583 ) 

584 

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

586 raise ValueError( 

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

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

589 f"Broadcasting dimension cannot be sharded." 

590 ) 

591 

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

593 

594 return tuple(merged_map) 

595 

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

597 """ 

598 Create output layout based on input layout. 

599 

600 Args: 

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

602 output_tensor_map (tuple): Tensor_map for the output 

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

604 

605 Returns: 

606 Layout: New Layout object with updated tensor_map and alias_tensor_map 

607 """ 

608 new_layout = copy.deepcopy(base_layout) 

609 new_layout.set_tensor_map(output_tensor_map) 

610 

611 alias_tensor_map = [] 

612 for tensor_dim in output_tensor_map: 

613 if tensor_dim == -1: 

614 alias_tensor_map.append("None") 

615 elif isinstance(tensor_dim, tuple): 

616 alias_tuple = tuple( 

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

618 for dim in tensor_dim 

619 if dim != -1 

620 ) 

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

622 else: 

623 alias_tensor_map.append( 

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

625 ) 

626 

627 new_layout.set_alias_tensor_map(tuple(alias_tensor_map)) 

628 new_layout.tensor_map_to_placement() 

629 

630 # Set partial status if provided 

631 if partial_list: 

632 for i, partial_op in enumerate(partial_list): 

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

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

635 

636 return new_layout 

637 

638 

639class ElementWiseWithPartialDistributedOp(ElementWiseDistributedOp): 

640 """ 

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

642 """ 

643 def __init__(self, op_name): 

644 super().__init__(op_name) 

645 self._allow_partial_inputs = True 

646 

647 

648class AddDistributedOp(ElementWiseWithPartialDistributedOp): 

649 """ 

650 Distributed implementation for Add operator. 

651 

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

653 which is useful for operations like gradient accumulation where partial 

654 results need to be preserved through the computation graph. 

655 """ 

656 

657 @staticmethod 

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

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

660 local_kwargs = dict(kwargs) 

661 if args: 

662 if "input" in local_kwargs: 

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

664 lhs = args[0] 

665 trailing_args = args[1:] 

666 elif "input" in local_kwargs: 

667 lhs = local_kwargs.pop("input") 

668 trailing_args = () 

669 else: 

670 return args, local_kwargs 

671 

672 if trailing_args: 

673 if "other" in local_kwargs: 

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

675 rhs = trailing_args[0] 

676 trailing_args = trailing_args[1:] 

677 elif "other" in local_kwargs: 

678 rhs = local_kwargs.pop("other") 

679 else: 

680 return args, local_kwargs 

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

682 

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

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

685 

686 Args: 

687 args: Positional backend arguments. 

688 kwargs: Keyword backend arguments, potentially including 

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

690 

691 Returns: 

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

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

694 """ 

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

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

697 

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

699 """ 

700 Infer output layout for Add operator. 

701 

702 Rules: 

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

704 2. Partial status is allowed and propagated. 

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

706 

707 Args: 

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

709 

710 Returns: 

711 tuple: ((output_layout,), None) 

712 

713 Raises: 

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

715 """ 

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

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

718 mesh_ndim = max( 

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

720 default=0, 

721 ) 

722 partial_signatures = tuple( 

723 _partial_signature(layout, mesh_ndim) 

724 for layout in layouts[:2] 

725 ) 

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

727 raise ValueError( 

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

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

730 ) 

731 

732 infer_result = super().infer_layout(cache_values) 

733 if infer_result is None: 

734 return infer_result 

735 

736 output_layout = infer_result[0][0] 

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

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

739 raise ValueError( 

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

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

742 ) 

743 

744 return infer_result 

745 

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

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

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

749 

750 Args: 

751 func: Local backend add or subtract implementation. 

752 infer_result: Inferred output layout and auxiliary result. 

753 cache_values: Input layouts followed by input shapes. 

754 

755 Returns: 

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

757 inputs already have identical Partial placements. 

758 """ 

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

760 x1_layout = layouts[0] 

761 x2_layout = layouts[1] 

762 output_layout = infer_result[0][0] 

763 output_mesh_ndim = len(output_layout.partial) 

764 x1_partial = _partial_signature(x1_layout, output_mesh_ndim) 

765 x2_partial = _partial_signature(x2_layout, output_mesh_ndim) 

766 

767 if x1_partial == x2_partial: 

768 return None 

769 

770 x1_contributes = _contributes_to_partial_output(x1_layout, output_layout) 

771 x2_contributes = _contributes_to_partial_output(x2_layout, output_layout) 

772 

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

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

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

776 local_x1 = x1 if x1_contributes else _zero_contribution(x1) 

777 local_x2 = x2 if x2_contributes else _zero_contribution(x2) 

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

779 

780 return _expand_impl 

781 

782 

783class SubDistributedOp(AddDistributedOp): 

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

785 

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

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

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

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

790 """