Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / core / dtensor / tensor_redistribution.py: 80%

381 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-11-15 07:18 +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"""tensor_redistribution""" 

16# pylint: disable=C9006,C9007 

17import logging 

18 

19import torch 

20import torch.distributed as dist 

21 

22from hyper_parallel.core.dtensor import _utils 

23from hyper_parallel.core.dtensor._ragged_utils import ( 

24 _compute_ragged_all_to_all_splits, 

25 _compute_ragged_slice, 

26 _compute_ragged_splits, 

27) 

28from hyper_parallel.core.dtensor.dtensor import DTensor 

29from hyper_parallel.core.dtensor.layout import Layout, RaggedShardInfo, infer_slice_area_by_rank 

30from hyper_parallel.core.dtensor.placement_types import Replicate 

31from hyper_parallel.core.dtensor.redistribute_infer import RedistributionOperatorInfer 

32 

33logger = logging.getLogger(__name__) 

34 

35 

36def _construct_layout_tuple_for_transform_operator_list(from_layout, to_layout, from_full_shape): 

37 """_construct_layout_tuple_for_transform_operator_list""" 

38 from_layout_dict = from_layout.to_dict() 

39 to_layout_dict = to_layout.to_dict() 

40 from_layout_tuple = ( 

41 from_layout_dict["mesh_shape"], from_layout_dict["tensor_map"], list(from_full_shape) 

42 ) 

43 # NOTE: consider reshape scenario when to_full_shape differs from from_full_shape 

44 to_layout_tuple = ( 

45 to_layout_dict["mesh_shape"], to_layout_dict["tensor_map"], list(from_full_shape) 

46 ) 

47 return from_layout_tuple, to_layout_tuple 

48 

49 

50class TensorRedistribution: 

51 """ 

52 TensorRedistribution. 

53 """ 

54 def __init__(self): 

55 self.is_init = False 

56 self.rank_id = None # current rank_id (global) 

57 self._transform_cache = {} 

58 self._construct_op_operator = { 

59 "Reshape": self._construct_reshape, 

60 "AllConcat": self._construct_all_concat, 

61 "StridedSlice": self._construct_strided_slice, 

62 "all_concat": TensorRedistribution._construct_all_concat_new, 

63 "all_split": self._construct_all_split, 

64 "all_to_all": self._construct_all_to_all 

65 } 

66 

67 @staticmethod 

68 def _construct_reshape(x, *args): 

69 """args: (*shape)""" 

70 return x.view(args) 

71 

72 @staticmethod 

73 def _construct_all_concat(x, *args): 

74 """args: (*rank_list, concat_dim)""" 

75 rank_list = args[0:-1] 

76 concat_dim = args[-1] 

77 group = _utils.create_group(rank_list) 

78 concat_size = len(rank_list) 

79 logger.debug( 

80 "differentiable_all_gather_concat: input_shape=%s, concat_dim=%d, " 

81 "concat_size=%d, rank_list=%s", 

82 tuple(x.shape), concat_dim, concat_size, rank_list, 

83 ) 

84 return _utils.differentiable_all_gather_concat(x, group, concat_size, concat_dim, rank_list) 

85 

86 

87 @staticmethod 

88 def _construct_strided_slice(x, *args): 

89 """args: (begin, end, strides)""" 

90 dims = len(args) // 3 

91 return _utils.construct_strided_slice(x, args[0: dims], args[dims: 2 * dims], args[2 * dims:]) 

92 

93 @staticmethod 

94 def _construct_all_concat_new(x, *args): 

95 """args: (concat_dim, concat_size, group)""" 

96 rank_list = args[2] 

97 concat_dim = args[0] 

98 concat_size = args[1] 

99 group = _utils.create_group(rank_list) 

100 logger.debug( 

101 "differentiable_all_gather_concat: input_shape=%s, concat_dim=%d, " 

102 "concat_size=%d, rank_list=%s", 

103 tuple(x.shape), concat_dim, concat_size, rank_list, 

104 ) 

105 return _utils.differentiable_all_gather_concat(x, group, concat_size, concat_dim, rank_list) 

106 

107 def _construct_all_split(self, x, *args): 

108 """args: (split_dim, split_size, group)""" 

109 rank_list = list(args[2]) 

110 split_dim = args[0] 

111 split_size = args[1] 

112 idx = rank_list.index(self.rank_id) 

113 return torch.chunk(x, split_size, dim=split_dim)[idx] 

114 

115 @staticmethod 

116 def _construct_all_to_all(x, *args): 

117 """args: (split_dim, concat_dim, permute_size, group)""" 

118 split_dim, concat_dim, split_count, rank_list = args 

119 group = _utils.create_group(rank_list) 

120 logger.debug( 

121 "differentiable_all_to_all: input_shape=%s, split_dim=%d, " 

122 "concat_dim=%d, split_count=%d, rank_list=%s", 

123 tuple(x.shape), split_dim, concat_dim, split_count, rank_list, 

124 ) 

125 original_shape = x.shape 

126 

127 dim_size = original_shape[split_dim] 

128 if dim_size % split_count != 0: 

129 raise ValueError(f"Dimension {split_dim} with size {dim_size} " 

130 f"cannot be evenly split into {split_count} parts") 

131 

132 split_size = dim_size // split_count 

133 final_shape = list(original_shape) 

134 if split_dim != concat_dim: 

135 final_shape[split_dim] = split_size 

136 final_shape[concat_dim] = final_shape[concat_dim] * split_count 

137 final_shape = tuple(final_shape) 

138 

139 pre_special_handle = all(original_shape[i] == 1 for i in range(split_dim)) 

140 if pre_special_handle: 

141 reshape_shape = (split_count * split_size,) + original_shape[split_dim + 1:] 

142 x_reshaped = x.view(reshape_shape) 

143 else: 

144 reshape_dims = list(original_shape) 

145 reshape_dims[split_dim] = split_count 

146 reshape_dims.insert(split_dim + 1, split_size) 

147 

148 trans_dims = list(range(len(reshape_dims))) 

149 trans_dims.remove(split_dim) 

150 trans_dims.insert(0, split_dim) 

151 

152 x_reshaped = x.reshape(reshape_dims).permute(trans_dims).contiguous() 

153 

154 reshape_shape = list(x_reshaped.shape) 

155 reshape_shape[0] = reshape_shape[0] * reshape_shape[1] 

156 reshape_shape.pop(1) 

157 reshape_shape = tuple(reshape_shape) 

158 x_reshaped = x_reshaped.reshape(reshape_shape) 

159 x_reshaped = x_reshaped.contiguous() 

160 output_tensor = _utils.differentiable_all_to_all( 

161 input_data=x_reshaped, 

162 output_shape=reshape_shape, 

163 group=group 

164 ) 

165 

166 post_special_handle = all(final_shape[i] == 1 for i in range(concat_dim)) 

167 if post_special_handle: 

168 return output_tensor.view(final_shape) 

169 

170 # When pre_special_handle collapsed leading size-1 dims, the A2A was executed 

171 # in a reduced-rank space where the effective concat axis is shifted left by 

172 # split_dim positions. Use recon_concat_dim for all post-A2A reshaping so 

173 # that split_count is merged into the correct dimension. 

174 recon_concat_dim = (concat_dim - split_dim) if pre_special_handle else concat_dim 

175 

176 output_reshape = list(output_tensor.shape) 

177 output_reshape[0] = split_count 

178 output_reshape.insert(1, output_tensor.shape[0] // split_count) 

179 

180 out_trans_dims = list(range(len(output_reshape))) 

181 first_dim = out_trans_dims.pop(0) 

182 if recon_concat_dim >= len(out_trans_dims): 

183 out_trans_dims.append(first_dim) 

184 else: 

185 out_trans_dims.insert(recon_concat_dim, first_dim) 

186 

187 final_output = output_tensor.reshape(output_reshape).permute(out_trans_dims).contiguous() 

188 

189 final_reshape = list(final_output.shape) 

190 if recon_concat_dim < len(final_reshape) - 1: 

191 final_reshape[recon_concat_dim] = ( 

192 final_reshape[recon_concat_dim] * final_reshape[recon_concat_dim + 1] 

193 ) 

194 final_reshape.pop(recon_concat_dim + 1) 

195 

196 result = final_output.reshape(final_reshape) 

197 if pre_special_handle: 

198 result = result.view(final_shape) 

199 return result 

200 

201 @staticmethod 

202 def _apply_eazy_redistribute(src_layout, dst_layout): 

203 """_apply_eazy_redistribute""" 

204 if (src_layout.mesh_shape != dst_layout.mesh_shape or 

205 src_layout.rank_list != dst_layout.rank_list): 

206 return False 

207 

208 tensor_map_size = len(src_layout.tensor_map) 

209 if len(dst_layout.tensor_map) != tensor_map_size: 

210 return False 

211 return True 

212 

213 def _redistribution_without_shape(self, local_x, src_layout, dst_layout, key, rank_list): 

214 """_redistribution_without_shape""" 

215 inferrer = RedistributionOperatorInfer( 

216 dev_mat=src_layout.mesh_shape, 

217 in_tensor_map=list(src_layout.tensor_map), 

218 out_tensor_map=list(dst_layout.tensor_map) 

219 ) 

220 op_list = inferrer.infer_ops_list(self.rank_id, rank_list) 

221 self._transform_cache[key] = op_list 

222 for op in op_list: 

223 local_x = self._construct_op_operator[op[0]](local_x, *op[1]) 

224 return local_x 

225 

226 @staticmethod 

227 def _to_normal_layout(layout: Layout, tensor_dim: int) -> Layout: 

228 """Build the normal view consumed by the legacy redistribution path.""" 

229 normal_layout = Layout.from_device_mesh(layout.mesh) 

230 normal_layout.set_placements(layout.normal_placements) 

231 normal_layout.placement_to_tensor_map(tensor_dim) 

232 return normal_layout 

233 

234 def _redistribute_ragged(self, input_x: DTensor, to_layout: Layout) -> DTensor: 

235 """Adapt RaggedShard layouts to the supported redistribution primitives.""" 

236 from_layout = input_x.layout 

237 source_info = from_layout.ragged_shard 

238 target_info = to_layout.ragged_shard 

239 source_is_ragged = isinstance(source_info, RaggedShardInfo) 

240 target_is_ragged = isinstance(target_info, RaggedShardInfo) 

241 tensor_dim = len(input_x.shape) 

242 

243 if source_is_ragged and target_is_ragged: 

244 source_normal_layout = self._to_normal_layout(from_layout, tensor_dim) 

245 target_normal_layout = self._to_normal_layout(to_layout, tensor_dim) 

246 if ( 

247 source_info.mesh_dim == target_info.mesh_dim 

248 and source_info.placement.dims == target_info.placement.dims 

249 and source_normal_layout == target_normal_layout 

250 ): 

251 return self.ragged_to_ragged(input_x, to_layout) 

252 return self.ragged_to_ragged_via_replicate( 

253 input_x, 

254 source_normal_layout, 

255 to_layout, 

256 ) 

257 

258 if source_is_ragged: 

259 source_normal_layout = self._to_normal_layout(from_layout, tensor_dim) 

260 normal = self.ragged_to_normal(input_x, source_normal_layout) 

261 if normal.layout == to_layout: 

262 return normal 

263 return self._redistribution_normal(normal, to_layout) 

264 

265 target_normal_layout = self._to_normal_layout(to_layout, tensor_dim) 

266 normal = input_x 

267 if from_layout != target_normal_layout: 

268 normal = self._redistribution_normal(normal, target_normal_layout) 

269 return self.normal_to_ragged(normal, to_layout) 

270 

271 def redistribution(self, input_x: DTensor, to_layout: Layout) -> DTensor: 

272 """Redistribute a tensor to the requested layout. 

273 

274 Args: 

275 input_x: Distributed input tensor. 

276 to_layout: Requested output layout on the same rank list. 

277 """ 

278 x_layout = input_x.layout 

279 if (x_layout.has_uneven_shard 

280 and not isinstance(x_layout.ragged_shard, RaggedShardInfo) 

281 and not isinstance(to_layout.ragged_shard, RaggedShardInfo)): 

282 # Validate the export-only path before any collective or layout rewrite. 

283 if x_layout.rank_list != to_layout.rank_list: 

284 raise ValueError("Uneven redistribution requires the same rank list") 

285 return self._gather_uneven_to_replicate(input_x, to_layout) 

286 x = input_x 

287 if input_x.layout.is_partial(): 

288 # Solve partial status first 

289 if input_x.layout.mesh_shape == to_layout.mesh_shape: 

290 x = self.reduce_partial(input_x, to_layout) 

291 else: 

292 x = self.reduce_partial(input_x, x_layout) 

293 

294 from_layout = x.layout 

295 if from_layout.rank_list != to_layout.rank_list: 

296 raise ValueError(f"The from_layout rank list: {from_layout.rank_list} is not equal to " 

297 f"to_layout rank list: {to_layout.rank_list}") 

298 if isinstance(from_layout.ragged_shard, RaggedShardInfo) or isinstance( 

299 to_layout.ragged_shard, RaggedShardInfo 

300 ): 

301 return self._redistribute_ragged(x, to_layout) 

302 if from_layout.has_uneven_shard or to_layout.has_uneven_shard: 

303 return self._gather_uneven_to_replicate(x, to_layout) 

304 return self._redistribution_normal(x, to_layout) 

305 

306 @staticmethod 

307 @torch.no_grad() 

308 def _padded_all_gather(local_tensor, sizes, tensor_dim, rank_list): 

309 """Gather export buffers; return independent storage without padding.""" 

310 if len(rank_list) == 1: 

311 return local_tensor.detach().clone() 

312 max_size = max(sizes) 

313 if tensor_dim != 0 and any(size != max_size for size in sizes): 

314 raise NotImplementedError("Uneven export communication only supports dim=0") 

315 padded = local_tensor.detach().contiguous() 

316 if tensor_dim == 0 and local_tensor.shape[0] != max_size: 

317 padded_shape = (max_size, *local_tensor.shape[1:]) 

318 padded = local_tensor.new_zeros(padded_shape) 

319 padded[:local_tensor.shape[0]].copy_(local_tensor) 

320 if padded.numel() == 0: 

321 output_shape = list(padded.shape) 

322 output_shape[tensor_dim] = sum(sizes) 

323 return padded.new_empty(output_shape) 

324 group = _utils.create_group(rank_list) 

325 group_ranks = dist.get_process_group_ranks(group) 

326 gathered = [torch.empty_like(padded) for _ in group_ranks] 

327 dist.all_gather(gathered, padded, group=group) 

328 group_indices = {rank: index for index, rank in enumerate(group_ranks)} 

329 # cat copies every trimmed shard into owned storage. Returning a view 

330 # (or contiguous(), which may be a no-op) would retain padded buffers. 

331 return torch.cat([ 

332 gathered[group_indices[rank]].narrow(tensor_dim, 0, size) 

333 for rank, size in zip(rank_list, sizes) 

334 ], dim=tensor_dim) 

335 

336 @staticmethod 

337 def _validate_uneven_local_shape(input_x: DTensor) -> None: 

338 """Reject padded storage masquerading as a true local shard.""" 

339 source = input_x.layout 

340 area = infer_slice_area_by_rank( 

341 source.mesh_shape, source.tensor_map, source.rank_list.index(source.mesh.rank), 

342 tuple(input_x.shape), source.uneven_shard_mesh_dims, 

343 ) 

344 expected_shape = tuple(end - start for start, end in area) 

345 local_shape = tuple(input_x.to_local().shape) 

346 if local_shape != expected_shape: 

347 raise ValueError(f"Uneven shard local shape {local_shape} does not match logical slice {expected_shape}") 

348 

349 @staticmethod 

350 def _gather_uneven_to_replicate(input_x: DTensor, to_layout: Layout) -> DTensor: 

351 """Reconstruct detached FSDP weights, including empty dim-zero shards. 

352 

353 Undo each tensor dimension's sharding from innermost to outermost. 

354 This respects both ceil-chunk FSDP shards and balanced TP shards, 

355 including the effective ordering encoded for StridedShard layouts. 

356 This is an export-only path, not a differentiable redistribution. 

357 Uneven nonzero axes and Partial placements remain unsupported. 

358 """ 

359 source = input_x.layout 

360 if (source.mesh_shape != to_layout.mesh_shape 

361 or not all(isinstance(placement, Replicate) for placement in to_layout.placements)): 

362 raise NotImplementedError( 

363 "Uneven chunk-sharded redistribution only supports a fully replicated target on the same mesh." 

364 ) 

365 full_shape = tuple(input_x.shape) 

366 rank = source.mesh.rank 

367 tensor_map = list(source.tensor_map) 

368 uneven_dims = source.uneven_shard_mesh_dims 

369 local_tensor = input_x.to_local() 

370 if source.is_partial(): 

371 raise NotImplementedError("Uneven FSDP weight export does not support Partial placements") 

372 if any(source.placements[axis].dim != 0 for axis in uneven_dims): 

373 raise NotImplementedError("Uneven FSDP weight export only supports dim=0") 

374 if torch.is_grad_enabled() and local_tensor.requires_grad: 

375 raise RuntimeError("Uneven FSDP weight export is not differentiable; use torch.no_grad() or detach()") 

376 TensorRedistribution._validate_uneven_local_shape(input_x) 

377 for tensor_dim, mapping in enumerate(source.tensor_map): 

378 axes = list(mapping if isinstance(mapping, tuple) else (mapping,)) 

379 axes = [axis for axis in axes if axis != -1] 

380 while axes: 

381 mesh_dim = len(source.mesh_shape) - 1 - axes[-1] 

382 ranks = source.mesh.get_devices_for_axis(mesh_dim, rank) 

383 areas = [ 

384 infer_slice_area_by_rank( 

385 source.mesh_shape, tensor_map, source.rank_list.index(peer), full_shape, uneven_dims, 

386 ) for peer in ranks 

387 ] 

388 sizes = [area[tensor_dim][1] - area[tensor_dim][0] for area in areas] 

389 local_tensor = TensorRedistribution._padded_all_gather(local_tensor, sizes, tensor_dim, ranks) 

390 axes.pop() 

391 tensor_map[tensor_dim] = tuple(axes) if axes else -1 

392 return DTensor.from_local_with_layout(local_tensor, to_layout, shape=full_shape) 

393 

394 def _redistribution_normal(self, input_x: DTensor, to_layout: Layout) -> DTensor: 

395 """Run the existing redistribution path for normal layouts.""" 

396 from_layout = input_x.layout 

397 x = input_x 

398 if not self.is_init: 

399 self.rank_id = dist.get_rank() 

400 self.is_init = True 

401 key = from_layout.compact_str + to_layout.compact_str + str(self.rank_id) 

402 if key in self._transform_cache: 

403 x = x.to_local() 

404 transform_operator_list = self._transform_cache[key] 

405 for transform_operator in transform_operator_list: 

406 x = self._construct_op_operator[transform_operator[0]](x, *transform_operator[1]) 

407 return DTensor.from_local(x, to_layout.mesh, to_layout.alias_placements) 

408 

409 full_shape = x.shape 

410 key_and_shape = key + str(full_shape) 

411 x = x.to_local() 

412 if key_and_shape in self._transform_cache: 

413 transform_operator_list = self._transform_cache[key_and_shape] 

414 for transform_operator in transform_operator_list: 

415 x = self._construct_op_operator[transform_operator[0]](x, *transform_operator[1]) 

416 return DTensor.from_local(x, to_layout.mesh, to_layout.alias_placements) 

417 

418 rank_list = from_layout.rank_list 

419 if self._apply_eazy_redistribute(from_layout, to_layout): 

420 if from_layout.is_partial(): 

421 from_layout.reset_partial() 

422 x = self._redistribution_without_shape(x, from_layout, to_layout, key, rank_list) 

423 else: 

424 transform_operator_list = self._infer_transform_operator_list(from_layout, to_layout, 

425 full_shape, key_and_shape, rank_list) 

426 for transform_operator in transform_operator_list: 

427 x = self._construct_op_operator[transform_operator[0]](x, *transform_operator[1]) 

428 return DTensor.from_local(x, to_layout.mesh, to_layout.alias_placements) 

429 

430 @staticmethod 

431 def ragged_to_normal(input_x: DTensor, to_layout: Layout) -> DTensor: 

432 """Gather one flat RaggedShard into its Replicate normal view.""" 

433 from_layout = input_x.layout 

434 info = from_layout.ragged_shard 

435 if not isinstance(info, RaggedShardInfo): 

436 raise ValueError("ragged_to_normal requires a RaggedShard source layout") 

437 global_shape = tuple(input_x.shape) 

438 output_splits = _compute_ragged_splits(global_shape, from_layout) 

439 local_tensor = input_x.to_local() 

440 if len(output_splits) == 1: 

441 gathered = local_tensor 

442 else: 

443 group = from_layout.mesh.get_group(info.mesh_dim) 

444 gathered = _utils.differentiable_variable_all_gather( 

445 local_tensor, 

446 output_splits, 

447 group, 

448 ) 

449 return DTensor.from_local_with_layout( 

450 gathered.reshape(global_shape), 

451 to_layout, 

452 shape=global_shape, 

453 ) 

454 

455 @staticmethod 

456 def normal_to_ragged(input_x: DTensor, to_layout: Layout) -> DTensor: 

457 """Slice a normal-view tensor into the target flat RaggedShard.""" 

458 global_shape = tuple(input_x.shape) 

459 local_slice = _compute_ragged_slice(global_shape, to_layout) 

460 flat_tensor = input_x.to_local().reshape((-1,)) 

461 local_tensor = flat_tensor[ 

462 local_slice.flat_start:local_slice.flat_end 

463 ].clone() 

464 return DTensor.from_local_with_layout( 

465 local_tensor, 

466 to_layout, 

467 shape=global_shape, 

468 ) 

469 

470 @staticmethod 

471 def ragged_to_ragged_via_replicate( 

472 input_x: DTensor, 

473 replicate_layout: Layout, 

474 to_layout: Layout, 

475 ) -> DTensor: 

476 """Redistribute different RaggedShard dims through a Replicate layout.""" 

477 from_layout = input_x.layout 

478 if ( 

479 from_layout.mesh_shape != to_layout.mesh_shape 

480 or from_layout.rank_list != to_layout.rank_list 

481 ): 

482 raise ValueError("ragged_to_ragged only supports changes on the same device mesh") 

483 replicated = TensorRedistribution.ragged_to_normal( 

484 input_x, 

485 replicate_layout, 

486 ) 

487 return TensorRedistribution.normal_to_ragged(replicated, to_layout) 

488 

489 @staticmethod 

490 def ragged_to_ragged(input_x: DTensor, to_layout: Layout) -> DTensor: 

491 """Redistribute a local-units-only RaggedShard change with variable all-to-all.""" 

492 from_layout = input_x.layout 

493 source_info = from_layout.ragged_shard 

494 if ( 

495 from_layout.mesh_shape != to_layout.mesh_shape 

496 or from_layout.rank_list != to_layout.rank_list 

497 ): 

498 raise ValueError("ragged_to_ragged only supports local_units changes on the same device mesh") 

499 

500 global_shape = tuple(input_x.shape) 

501 input_splits, output_splits = _compute_ragged_all_to_all_splits( 

502 global_shape, 

503 from_layout, 

504 to_layout, 

505 ) 

506 flat_input = input_x.to_local().reshape((-1,)) 

507 

508 if flat_input.shape[0] != sum(input_splits): 

509 raise ValueError( 

510 "RaggedShard source storage does not match all-to-all splits, " 

511 f"got local_numel={flat_input.shape[0]}, input_splits={input_splits!r}" 

512 ) 

513 if len(input_splits) == 1: 

514 flat_output = flat_input 

515 else: 

516 group = from_layout.mesh.get_group(source_info.mesh_dim) 

517 flat_output = _utils.differentiable_all_to_all_single( 

518 flat_input, 

519 input_splits, 

520 output_splits, 

521 group, 

522 ) 

523 if flat_output.shape[0] != sum(output_splits): 

524 raise ValueError( 

525 "RaggedShard target storage does not match all-to-all splits, " 

526 f"got local_numel={flat_output.shape[0]}, output_splits={output_splits!r}" 

527 ) 

528 return DTensor.from_local_with_layout( 

529 flat_output, 

530 to_layout, 

531 shape=global_shape, 

532 ) 

533 

534 def _infer_transform_operator_list(self, from_layout, to_layout, from_full_shape, key, rank_list): 

535 """infer transform operator list""" 

536 from_layout_tuple, to_layout_tuple = \ 

537 _construct_layout_tuple_for_transform_operator_list(from_layout, to_layout, from_full_shape) 

538 self._transform_cache[key] = \ 

539 _utils.get_tensor_transform().transform_tensor_sharding(from_layout_tuple, to_layout_tuple, 

540 rank_list, False, self.rank_id) 

541 return self._transform_cache[key] 

542 

543 @staticmethod 

544 def _allreduce_along_dev_dim(x, op, layout, dev_dim): 

545 """Do allreduce at specified axis along dev_dim.""" 

546 logger.debug( 

547 "differentiable_all_reduce: input_shape=%s, op=%s, dev_dim=%s", 

548 tuple(x.shape), op, dev_dim, 

549 ) 

550 group = layout.get_comm_group_by_axis(dev_dim) 

551 zero_dim = x.dim() == 0 

552 if zero_dim: 

553 x = x.unsqueeze(0) 

554 if op == 'avg': 

555 dev_num = layout.mesh_shape[layout.alias_name.index(dev_dim)] 

556 x = _utils.differentiable_all_reduce(x, 'sum', group) 

557 x = x / dev_num 

558 elif op == 'all': 

559 x_int32 = _utils.tensor_type_cast(x.bool(), 'int32') # True→1, False→0 

560 x = _utils.differentiable_all_reduce(x_int32, 'all', group) 

561 x = x.bool() 

562 else: 

563 x = _utils.differentiable_all_reduce(x, op, group) 

564 if zero_dim: 

565 x = x.squeeze(0) 

566 return x 

567 

568 @staticmethod 

569 def _reduce_scatter_along_dev_dim_with_axis(x, axis, op, layout, dev_dim): 

570 """Do reduce_scatter at specified axis along dev_dim.""" 

571 dev_num = layout.mesh_shape[layout.alias_name.index(dev_dim)] 

572 logger.debug( 

573 "differentiable_reduce_scatter: input_shape=%s, axis=%d, " 

574 "op=%s, dev_dim=%s, dev_num=%d", 

575 tuple(x.shape), axis, op, dev_dim, dev_num, 

576 ) 

577 group = layout.get_comm_group_by_axis(dev_dim) 

578 output_tensor = _utils.differentiable_reduce_scatter(x, dev_num, axis, op, group) 

579 return output_tensor 

580 

581 def reduce_partial(self, input_x, to_layout): 

582 """Reduce partial status.""" 

583 from_layout = input_x.layout 

584 x = input_x 

585 if from_layout is None or not from_layout.is_partial(): 

586 return x 

587 

588 x = x.to_local() 

589 if from_layout.mesh_shape != to_layout.mesh_shape: 

590 raise ValueError(f"For reduce partial, mesh_shape between from_layout and to_layout must be the same, " 

591 f"but got {from_layout.mesh_shape} and {to_layout.mesh_shape}") 

592 if to_layout.is_partial(): 

593 raise ValueError(f"For reduce partial, to_layout must be non-partial status, but got to_layout.partial: " 

594 f"{to_layout.partial}") 

595 

596 dev_map_order = {} 

597 for dev_axis in to_layout.alias_tensor_map: 

598 if isinstance(dev_axis, tuple): 

599 for i, sub_dev_axis in enumerate(dev_axis): 

600 dev_map_order[sub_dev_axis] = i 

601 else: 

602 dev_map_order[dev_axis] = 0 

603 

604 pending_reduce_op_list = [] # List[Tuple[comm_op, op, dev_dim, reduce_dim]] 

605 for dev_axis_index, op in enumerate(from_layout.partial): 

606 if op is None: 

607 continue 

608 dev_axis = from_layout.alias_name[dev_axis_index] 

609 apply_shard_dim = to_layout.get_dev_axis_apply_shard_axis(dev_axis) 

610 comm_op = "ReduceScatter" if apply_shard_dim is not None else "AllReduce" 

611 pending_reduce_op_list.append((comm_op, op, dev_axis, apply_shard_dim)) 

612 

613 # sort reduce op 

614 # 1. ReduceScatter is executed before AllReduce 

615 # 2. If multiple split, the dev axis split outer will be execute first. 

616 # e.g. ("cp", "tp"), will execute reduce_scatter along "cp" before "tp" 

617 # 3. Lower dev_id execute before higher dev_id 

618 def _reduce_pair_sort_key(reduce_pair): 

619 return (reduce_pair[0] != "ReduceScatter", 

620 dev_map_order.get(reduce_pair[2], 0), 

621 to_layout.mesh.axis_id(reduce_pair[2])) 

622 

623 sorted_pending_reduce_op_list = sorted(pending_reduce_op_list, key=_reduce_pair_sort_key) 

624 

625 output_alias_tensor_map = list(from_layout.alias_tensor_map) 

626 for reduce_op_pair in sorted_pending_reduce_op_list: 

627 comm_op = reduce_op_pair[0] 

628 op = reduce_op_pair[1] 

629 dev_axis = reduce_op_pair[2] 

630 if comm_op == "AllReduce": 

631 x = TensorRedistribution._allreduce_along_dev_dim(x, op, from_layout, dev_axis) 

632 elif comm_op == "ReduceScatter": 

633 reduce_axis = reduce_op_pair[3] 

634 x = self._reduce_scatter_along_dev_dim_with_axis(x, reduce_axis, op, from_layout, dev_axis) 

635 if output_alias_tensor_map[reduce_axis] == "None": 

636 output_alias_tensor_map[reduce_axis] = dev_axis 

637 elif isinstance(output_alias_tensor_map[reduce_axis], tuple): 

638 output_alias_tensor_map[reduce_axis] += (dev_axis,) 

639 else: 

640 output_alias_tensor_map[reduce_axis] = (output_alias_tensor_map[reduce_axis], dev_axis) 

641 

642 output_layout = from_layout(*output_alias_tensor_map) 

643 output_layout.reset_partial() 

644 return DTensor.from_local(x, output_layout.mesh, output_layout.alias_placements) 

645 

646 

647_tensor_redistribution = TensorRedistribution()