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

198 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 __setitem__ operator (PyTorch only). 

17 

18__setitem__ (tensor[key] = value) is an in-place operation. 

19The LHS key follows the same classification as __getitem__: 

20BASIC (int/slice/None/Ellipsis), ADVANCED (list/LongTensor), BOOL_MASK (not supported). 

21""" 

22from typing import Any, Tuple 

23 

24from hyper_parallel.core.dtensor.dtensor import DTensor, _build_layout 

25from hyper_parallel.core.dtensor.placement_types import Replicate 

26from hyper_parallel.platform import get_platform 

27from .parallel_getitem import ( 

28 _BASIC, 

29 _BOOL_MASK, 

30 _broadcast_shapes, 

31 _key_cache_descriptor, 

32 _descriptor_to_expanded_actions, 

33 _unwrap_key_for_local, 

34 GetItemDistributedOp, 

35) 

36from .parallel_ops import DistributedOp 

37 

38platform = get_platform() 

39Tensor = platform.Tensor 

40 

41 

42def _normalize___setitem___args(self_t, key, value): 

43 """Normalize __setitem__ arguments to canonical positional form. 

44 

45 __setitem__ is always called as func(self, key, value) with empty kwargs. 

46 

47 Args: 

48 self_t: The DTensor self argument. 

49 key: The indexing key. 

50 value: The value to assign. 

51 

52 Returns: 

53 tuple: ((self_t, key, value), {}) 

54 """ 

55 return (self_t, key, value), {} 

56 

57 

58def _compute_lhs_shape(global_shape, expanded_actions, kind, op_name="__setitem__"): 

59 """Compute the shape of the LHS slice that __getitem__ would produce. 

60 

61 This is used to validate that the RHS value can broadcast to the LHS slice. 

62 

63 Args: 

64 global_shape: Tuple of self tensor's global shape. 

65 expanded_actions: Expanded key actions from _descriptor_to_expanded_actions. 

66 kind: "basic" or "advanced". 

67 op_name: Operator name for error messages. 

68 

69 Returns: 

70 tuple: Shape of the LHS slice. 

71 """ 

72 if kind == _BASIC: 

73 return _compute_basic_lhs_shape(global_shape, expanded_actions, op_name=op_name) 

74 return _compute_advanced_lhs_shape(global_shape, expanded_actions, op_name=op_name) 

75 

76 

77def _compute_basic_lhs_shape(global_shape, expanded_actions, op_name="__setitem__"): 

78 """Compute LHS shape for basic indexing. 

79 

80 Raises ValueError if step is not None or 1. 

81 """ 

82 lhs_dims = [] 

83 for action in expanded_actions: 

84 action_type = action[0] 

85 if action_type == "newaxis": 

86 lhs_dims.append(1) 

87 elif action_type == "slice": 

88 start, stop, step = action[1], action[2], action[3] 

89 step = step if step is not None else 1 

90 if step != 1: 

91 raise ValueError( 

92 f"For {op_name}, slice step must be None or 1, but got {step}." 

93 ) 

94 input_dim = action[4] 

95 dim_size = global_shape[input_dim] 

96 # Compute slice length 

97 start_val = start if start is not None else 0 

98 if start_val < 0: 

99 start_val += dim_size 

100 stop_val = stop if stop is not None else dim_size 

101 if stop_val < 0: 

102 stop_val += dim_size 

103 # Clamp to valid range 

104 start_val = max(0, min(start_val, dim_size)) 

105 stop_val = max(0, min(stop_val, dim_size)) 

106 slice_len = max(0, (stop_val - start_val + step - 1) // step) 

107 lhs_dims.append(slice_len) 

108 # int actions are skipped (dimension removed) 

109 return tuple(lhs_dims) if lhs_dims else () 

110 

111 

112def _compute_slice_len(action, global_shape, op_name="__setitem__"): 

113 """Compute output dim length for a slice action. 

114 

115 Raises ValueError if step is not None or 1. 

116 """ 

117 start, stop, step = action[1], action[2], action[3] 

118 step = step if step is not None else 1 

119 if step != 1: 

120 raise ValueError( 

121 f"For {op_name}, slice step must be None or 1, but got {step}." 

122 ) 

123 input_dim = action[4] 

124 dim_size = global_shape[input_dim] 

125 start_val = start if start is not None else 0 

126 if start_val < 0: 

127 start_val += dim_size 

128 stop_val = stop if stop is not None else dim_size 

129 if stop_val < 0: 

130 stop_val += dim_size 

131 start_val = max(0, min(start_val, dim_size)) 

132 stop_val = max(0, min(stop_val, dim_size)) 

133 return max(0, (stop_val - start_val + step - 1) // step) 

134 

135 

136def _append_non_advanced_shape(action, global_shape, result, op_name="__setitem__"): 

137 """Append output dim length for a non-advanced action (newaxis, slice; skip int).""" 

138 if action[0] == "newaxis": 

139 result.append(1) 

140 elif action[0] == "slice": 

141 result.append(_compute_slice_len(action, global_shape, op_name=op_name)) 

142 # int: dimension removed, skip 

143 

144 

145def _compute_advanced_lhs_shape(global_shape, expanded_actions, op_name="__setitem__"): 

146 """Compute LHS shape for advanced indexing. 

147 

148 Walks expanded_actions positionally: newaxis → 1, slice → slice_len, 

149 int → skip, advanced block → replaced by broadcast shape B. 

150 """ 

151 # Collect advanced index shapes and their positions 

152 advanced_shapes = [] 

153 advanced_positions = [] 

154 for i, action in enumerate(expanded_actions): 

155 if action[0] == "idx_list": 

156 advanced_shapes.append((len(action[1]),)) 

157 advanced_positions.append(i) 

158 elif action[0] == "idx_tensor": 

159 advanced_shapes.append(tuple(action[1]["shape"])) 

160 advanced_positions.append(i) 

161 

162 bcast_shape = _broadcast_shapes(advanced_shapes, op_name=op_name) if advanced_shapes else () 

163 

164 advanced_pos_set = set(advanced_positions) 

165 are_consecutive = ( 

166 len(advanced_positions) > 0 

167 and advanced_positions == list(range(advanced_positions[0], 

168 advanced_positions[-1] + 1)) 

169 ) 

170 

171 result = [] 

172 

173 if are_consecutive and advanced_positions: 

174 first_adv_pos = advanced_positions[0] 

175 last_adv_pos = advanced_positions[-1] 

176 

177 # Actions before the advanced block 

178 for i in range(first_adv_pos): 

179 _append_non_advanced_shape(expanded_actions[i], global_shape, result, op_name=op_name) 

180 

181 # B dims replace the advanced block 

182 result.extend(bcast_shape) 

183 

184 # Actions after the advanced block 

185 for i in range(last_adv_pos + 1, len(expanded_actions)): 

186 _append_non_advanced_shape(expanded_actions[i], global_shape, result, op_name=op_name) 

187 else: 

188 # Non-consecutive: B goes at position 0 

189 result.extend(bcast_shape) 

190 

191 for i, action in enumerate(expanded_actions): 

192 if i in advanced_pos_set: 

193 continue 

194 _append_non_advanced_shape(action, global_shape, result) 

195 

196 return tuple(result) if result else () 

197 

198 

199def _compute_lhs_layout(self_layout, expanded_actions, kind, op_name="__setitem__"): 

200 """Compute the LHS layout (getitem output layout) for the given key. 

201 

202 Args: 

203 self_layout: Layout of self tensor. 

204 expanded_actions: Expanded key actions. 

205 kind: "basic" or "advanced". 

206 op_name: Operator name for error messages. 

207 

208 Returns: 

209 Layout: The layout that __getitem__ would produce for this key. 

210 """ 

211 if kind == _BASIC: 

212 return GetItemDistributedOp._infer_basic_output_layout( # pylint: disable=W0212 

213 self_layout, expanded_actions 

214 ) 

215 return GetItemDistributedOp._infer_advanced_output_layout( # pylint: disable=W0212 

216 self_layout, expanded_actions, op_name=op_name 

217 ) 

218 

219 

220def _build_broadcast_value_layout(mesh, lhs_layout, value_shape, lhs_shape): 

221 """Build a layout for the value tensor with broadcast dims forced to Replicate. 

222 

223 When a value tensor has a smaller shape that needs to broadcast to the LHS 

224 slice shape, any dimension where value size is 1 and LHS size > 1 must be 

225 Replicate (cannot be sharded). Non-broadcast dims inherit LHS placements. 

226 

227 Args: 

228 mesh: Device mesh. 

229 lhs_layout: Expected LHS layout (getitem output layout). 

230 value_shape: Shape of the value tensor. 

231 lhs_shape: Expected LHS slice shape. 

232 

233 Returns: 

234 Layout: Value layout adjusted for broadcasting. 

235 """ 

236 ndim_diff = len(lhs_shape) - len(value_shape) 

237 padded_value_shape = (1,) * ndim_diff + value_shape 

238 

239 placements = [] 

240 for v_size, l_size, lhs_placement in zip( 

241 padded_value_shape, lhs_shape, lhs_layout.alias_placements 

242 ): 

243 if v_size == 1 and l_size > 1: 

244 placements.append(Replicate()) 

245 else: 

246 placements.append(lhs_placement) 

247 

248 return _build_layout(mesh, tuple(placements), len(placements)) 

249 

250 

251class SetItemDistributedOp(DistributedOp): 

252 """Distributed implementation for tensor.__setitem__. 

253 

254 __setitem__ (tensor[key] = value) is an in-place operation. 

255 The LHS key follows the same validation rules as __getitem__. 

256 RHS value can be a scalar, Tensor, or DTensor. 

257 

258 Sharding constraints: 

259 - Same LHS constraints as __getitem__ (sharded dims cannot be indexed). 

260 - BoolTensor masks are rejected. 

261 - RHS DTensor must have mesh consistent with self and broadcast-compatible 

262 layout (non-broadcast dims must match LHS layout, broadcast dims must be 

263 Replicate). 

264 - RHS Tensor/scalar must be broadcastable to LHS slice shape. 

265 """ 

266 

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

268 """Preprocess arguments for __setitem__. 

269 

270 Args: 

271 args: (self_tensor, key, value) 

272 kwargs: Empty dict for __setitem__. 

273 

274 Returns: 

275 tuple: (local_args, local_kwargs, cache_values) 

276 """ 

277 norm_args = _normalize___setitem___args(*args, **kwargs)[0] 

278 self_t, key, value = norm_args 

279 

280 local_key, key_cache, lhs_shape, lhs_layout = self._preprocess_setitem_key( 

281 self_t, key, self.op_name 

282 ) 

283 

284 local_value, value_desc = self._preprocess_setitem_value( 

285 value, key_cache[3], lhs_shape, lhs_layout, key_cache[0].mesh, self.op_name, 

286 ) 

287 

288 cache_values = key_cache + [value_desc, lhs_shape] 

289 return ((self_t.to_local(), local_key, local_value), {}, cache_values) 

290 

291 @staticmethod 

292 def _preprocess_setitem_key(self_t, key, op_name): 

293 """Process key and compute LHS metadata for __setitem__ preprocess. 

294 

295 Args: 

296 self_t: The self DTensor. 

297 key: Raw indexing key. 

298 op_name: Operator name for error messages. 

299 

300 Returns: 

301 tuple: (local_key, key_cache, lhs_shape, lhs_layout) 

302 key_cache is [self_layout, key_desc, global_shape, kind]. 

303 """ 

304 self_layout = self_t.layout 

305 global_shape = tuple(self_t.shape) 

306 key_desc, kind = _key_cache_descriptor(key, op_name=op_name) 

307 local_key = _unwrap_key_for_local(key) 

308 key_cache = [self_layout, key_desc, global_shape, kind] 

309 

310 if kind == _BOOL_MASK: 

311 return local_key, key_cache, None, None 

312 

313 expanded_actions = _descriptor_to_expanded_actions( 

314 key_desc, len(global_shape), op_name=op_name 

315 ) 

316 lhs_shape = _compute_lhs_shape(global_shape, expanded_actions, kind, op_name=op_name) 

317 lhs_layout = _compute_lhs_layout(self_layout, expanded_actions, kind, op_name=op_name) 

318 

319 return local_key, key_cache, lhs_shape, lhs_layout 

320 

321 @staticmethod 

322 def _preprocess_setitem_value(value, kind, lhs_shape, lhs_layout, self_mesh, op_name): 

323 """Process the value argument for __setitem__ preprocess. 

324 

325 Handles DTensor unwrapping, plain-tensor broadcast validation and sharding, 

326 and scalar passthrough. 

327 

328 Args: 

329 value: The raw value tensor (DTensor, Tensor, or scalar). 

330 kind: "basic", "advanced", or "bool_mask". 

331 lhs_shape: Computed LHS slice shape (tuple). 

332 lhs_layout: Computed LHS slice Layout. 

333 self_mesh: DeviceMesh of the self tensor. 

334 op_name: Operator name for error messages. 

335 

336 Returns: 

337 (local_value, value_desc): Processed local tensor and cache descriptor. 

338 """ 

339 if isinstance(value, DTensor): 

340 value_shape = tuple(value.shape) 

341 local_value = value.to_local() 

342 if not value_shape: 

343 return local_value, None # 0-D DTensor treated as scalar 

344 return local_value, ("dtensor", value.layout, value_shape) 

345 

346 if isinstance(value, Tensor): 

347 value_desc = ("plain_tensor", tuple(value.shape)) 

348 if value.ndim > 0 and kind != _BOOL_MASK: 

349 _validate_value_broadcast(op_name, tuple(value.shape), lhs_shape) 

350 # pylint: disable=C0415 

351 from hyper_parallel.core.dtensor.layout import _get_slice_tensor_by_layout 

352 value_layout = _build_broadcast_value_layout( 

353 self_mesh, lhs_layout, tuple(value.shape), lhs_shape 

354 ) 

355 ndim_diff = len(lhs_shape) - value.ndim 

356 if ndim_diff > 0: 

357 padded_value = value.reshape((1,) * ndim_diff + tuple(value.shape)) 

358 else: 

359 padded_value = value 

360 local_value = _get_slice_tensor_by_layout(padded_value, value_layout) 

361 return local_value, value_desc 

362 return value, value_desc 

363 

364 return value, None 

365 

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

367 """Infer output layout for __setitem__. 

368 

369 __setitem__ is in-place, so output layout equals self layout. 

370 Validates LHS key constraints and RHS value compatibility. 

371 

372 Rules: 

373 1. self must not be in Partial status. 

374 2. BoolTensor mask indexing is not supported. 

375 3. Any dimension being written must be replicated. 

376 4. slice step must be None or 1. 

377 5. Advanced index tensors must be replicated. 

378 6. RHS DTensor must have mesh consistent with self and 

379 broadcast-compatible layout (non-broadcast dims match LHS 

380 layout, broadcast dims must be Replicate). 

381 7. RHS Tensor/scalar must be broadcastable to LHS slice shape. 

382 8. Output layout equals self_layout (in-place). 

383 

384 Args: 

385 cache_values: [self_layout, key_desc, global_shape, kind, value_desc, lhs_shape] 

386 

387 Returns: 

388 tuple: ((self_layout,), None) 

389 

390 Raises: 

391 ValueError: If any constraint is violated. 

392 """ 

393 self_layout = cache_values[0] 

394 key_desc = cache_values[1] 

395 global_shape = cache_values[2] 

396 kind = cache_values[3] 

397 value_desc = cache_values[4] 

398 lhs_shape = cache_values[5] 

399 

400 # key_desc -> expanded_actions (for LHS validation) 

401 expanded_actions = _descriptor_to_expanded_actions( 

402 key_desc, len(global_shape), op_name=self.op_name 

403 ) 

404 

405 if not self._allow_partial_inputs: 

406 self._check_partial_inputs([self_layout]) 

407 self._validate_input_layouts( 

408 self_layout, expanded_actions, global_shape, kind, value_desc, lhs_shape 

409 ) 

410 return ((self_layout,), None) 

411 

412 @staticmethod 

413 def _validate_input_layouts(self_layout, expanded_actions, global_shape, kind, value_desc, lhs_shape): 

414 """Validate sharding constraints for __setitem__.""" 

415 op_name = "__setitem__" 

416 

417 GetItemDistributedOp._validate_input_layouts( # pylint: disable=W0212 

418 self_layout, expanded_actions, global_shape, kind, op_name=op_name 

419 ) 

420 

421 if value_desc is None: 

422 return 

423 

424 val_kind = value_desc[0] 

425 if val_kind == "plain_tensor": 

426 _validate_value_broadcast(op_name, value_desc[1], lhs_shape) 

427 elif val_kind == "dtensor": 

428 SetItemDistributedOp._validate_dtensor_value( 

429 self_layout, expanded_actions, kind, value_desc[1], value_desc[2], lhs_shape, op_name, 

430 ) 

431 else: 

432 raise ValueError( 

433 f"For {op_name}, unsupported value descriptor kind: {val_kind}." 

434 ) 

435 

436 @staticmethod 

437 def _validate_dtensor_value( 

438 self_layout, expanded_actions, kind, val_layout, val_shape, lhs_shape, op_name): 

439 """Validate DTensor RHS mesh and layout.""" 

440 _validate_value_broadcast(op_name, val_shape, lhs_shape) 

441 

442 if val_layout.mesh.to_hash() != self_layout.mesh.to_hash(): 

443 raise ValueError( 

444 f"For {op_name}, value DTensor must be on same mesh as self, " 

445 f"but got mesh {val_layout.mesh_shape} " 

446 f"vs {self_layout.mesh_shape}." 

447 ) 

448 

449 expected_layout = _compute_lhs_layout( 

450 self_layout, expanded_actions, kind, op_name=op_name 

451 ) 

452 SetItemDistributedOp._validate_value_layout( 

453 val_layout, expected_layout, val_shape, lhs_shape, op_name, 

454 ) 

455 

456 @staticmethod 

457 def _validate_value_layout(val_layout, expected_layout, val_shape, lhs_shape, op_name): 

458 """Validate RHS DTensor placement against expected LHS placement.""" 

459 ndim_diff = len(lhs_shape) - len(val_shape) 

460 val_alias = val_layout.alias_tensor_map 

461 expected_alias = expected_layout.alias_tensor_map 

462 

463 for i, v_size in enumerate(val_shape): 

464 lhs_index = ndim_diff + i 

465 e_size = lhs_shape[lhs_index] 

466 v_alias = val_alias[i] 

467 e_alias = expected_alias[lhs_index] 

468 

469 if v_size == 1 and e_size > 1: 

470 if v_alias != "None": 

471 raise ValueError( 

472 f"For {op_name}, value broadcast dim {i} (size {v_size}) " 

473 f"must be Replicate, but got {v_alias}." 

474 ) 

475 elif v_alias != e_alias: 

476 raise ValueError( 

477 f"For {op_name}, value layout mismatch at dim {i}: " 

478 f"expected {e_alias}, but got {v_alias}." 

479 ) 

480 

481 def wrap_output(self, py_output: Any, output_layouts: tuple) -> None: 

482 """Override wrap_output for __setitem__. 

483 

484 __setitem__ is in-place and returns None in PyTorch. 

485 The default wrap_output would try to wrap None into a DTensor and fail. 

486 

487 Args: 

488 py_output: The output from the local function call (None). 

489 output_layouts: Output layouts from infer_layout. 

490 

491 Returns: 

492 None: __setitem__ has no return value. 

493 """ 

494 return None 

495 

496 

497def _validate_value_broadcast(op_name, val_shape, lhs_shape): 

498 """Validate that value shape can broadcast to LHS slice shape. 

499 

500 Args: 

501 op_name: Operator name for error messages. 

502 val_shape: Shape of RHS value. 

503 lhs_shape: Expected LHS slice shape. 

504 

505 Raises: 

506 ValueError: If shapes are not broadcastable. 

507 """ 

508 if len(val_shape) > len(lhs_shape): 

509 raise ValueError( 

510 f"For {op_name}, value shape {val_shape} cannot broadcast " 

511 f"to target shape {lhs_shape}." 

512 ) 

513 

514 # Pad val_shape with leading 1s 

515 padded = (1,) * (len(lhs_shape) - len(val_shape)) + val_shape 

516 for v, t in zip(padded, lhs_shape): 

517 if v not in (1, t): 

518 raise ValueError( 

519 f"For {op_name}, value shape {val_shape} cannot broadcast " 

520 f"to target shape {lhs_shape}." 

521 )