Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / platform / torch / platform.py: 63%

852 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-08-25 04:27 +0800

1# Copyright 2025-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"""Torch platform api""" 

16from datetime import timedelta 

17from typing import Any, Callable, Optional, Sequence, Union 

18import dataclasses 

19from collections import OrderedDict 

20 

21import numpy as np 

22from safetensors.torch import save_file, load_file 

23import torch 

24from torch import nn 

25from torch import Tensor 

26from torch._C._distributed_c10d import Store, ProcessGroup 

27from torch.distributed import Backend 

28from torch.distributed.distributed_c10d import _get_default_group 

29from torch.nn import Parameter, Module 

30from torch.nn.utils.rnn import PackedSequence 

31from torch._ops import OpOverload, OpOverloadPacket 

32from torch.utils.checkpoint import noop_context_fn 

33 

34import torch.distributed.nn.functional as dist_func 

35import torch.distributed as dist 

36from hyper_parallel.platform.torch.dtensor import DTensorBase 

37from hyper_parallel.platform.torch.pipeline_parallel.stage import PipelineStageBase 

38from hyper_parallel.platform.torch.group_utils import create_sub_groups 

39from hyper_parallel.platform.platform import Platform, PlatformType, EXISTING_COMM_GROUPS 

40from hyper_parallel.platform.torch.function_override import override_functions 

41from hyper_parallel.platform.torch.init_weights import init_on_device as _init_on_device 

42 

43override_functions() 

44 

45 

46# --------------------------------------------------------------------------- 

47# Module-level A2A reshape helpers 

48# --------------------------------------------------------------------------- 

49 

50def _a2a_reconstruct(out_perm: torch.Tensor, concat_dim: int) -> torch.Tensor: 

51 """Reconstruct A2A result from raw out_perm buffer. 

52 

53 ``out_perm`` has shape ``[ws, *rest_dims]``, chunk at ``concat_dim + 1``. 

54 Returns tensor with merged chunk dimension. 

55 """ 

56 new_ndim = out_perm.dim() 

57 chunk_in_perm = concat_dim + 1 

58 recon_perm = list(range(1, chunk_in_perm)) + [0] + list(range(chunk_in_perm, new_ndim)) 

59 x_recon = out_perm.permute(recon_perm).contiguous() 

60 shape = list(x_recon.shape) 

61 merged = shape[concat_dim] * shape[concat_dim + 1] 

62 return x_recon.reshape(shape[:concat_dim] + [merged] + shape[concat_dim + 2:]) 

63 

64 

65def _normalize_dim(dim: int, ndim: int) -> int: 

66 """Normalize a possibly negative dimension index.""" 

67 return dim + ndim if dim < 0 else dim 

68 

69 

70def _move_dim_to_front(tensor: torch.Tensor, dim: int) -> torch.Tensor: 

71 """Move ``dim`` to the front while keeping the other dimensions ordered.""" 

72 dim = _normalize_dim(dim, tensor.dim()) 

73 if dim == 0: 

74 return tensor.contiguous() 

75 perm = [dim] + [i for i in range(tensor.dim()) if i != dim] 

76 return tensor.permute(perm).contiguous() 

77 

78 

79def _move_dim_from_front(tensor: torch.Tensor, dim: int) -> torch.Tensor: 

80 """Inverse of :func:`_move_dim_to_front`.""" 

81 dim = _normalize_dim(dim, tensor.dim()) 

82 if dim == 0: 

83 return tensor.contiguous() 

84 perm = [dim] + [i for i in range(tensor.dim()) if i != dim] 

85 inverse = [0] * len(perm) 

86 for idx, value in enumerate(perm): 

87 inverse[value] = idx 

88 return tensor.permute(inverse).contiguous() 

89 

90 

91class _TorchAsyncA2AFunction(torch.autograd.Function): 

92 """Differentiable wrapper for pre-launched async all-to-all. 

93 

94 Forward: wait async handle, reconstruct A2A result. 

95 Backward: launch async head→seq A2A and store handle in ``handle_box`` 

96 for the projection pre-hook to wait, achieving GEMM–A2A overlap. 

97 """ 

98 

99 @staticmethod 

100 def forward(ctx, x, work, out_perm, group, world_size, concat_dim, split_dim, # pylint: disable=arguments-differ 

101 handle_box): 

102 """Wait for pre-launched async A2A and return reconstructed output.""" 

103 ctx.group = group 

104 ctx.world_size = world_size 

105 ctx.concat_dim = concat_dim 

106 ctx.split_dim = split_dim 

107 ctx.handle_box = handle_box 

108 ctx.x_shape = x.shape 

109 work.wait() 

110 return _a2a_reconstruct(out_perm, concat_dim) 

111 

112 @staticmethod 

113 def backward(ctx, grad_output): 

114 """Launch async head→seq A2A for backward overlap, or return zero grad.""" 

115 if ctx.handle_box is not None: 

116 # Launch async head→seq A2A (reverse of forward seq→head) 

117 g = grad_output.contiguous() 

118 shape = list(g.shape) 

119 seq_dim = ctx.concat_dim 

120 s_full = shape[seq_dim] 

121 ndim = len(shape) + 1 

122 x_perm = g.reshape( 

123 shape[:seq_dim] + [ctx.world_size, s_full // ctx.world_size] + shape[seq_dim + 1:] 

124 ).permute( 

125 [seq_dim] + list(range(seq_dim)) + list(range(seq_dim + 1, ndim)) 

126 ).contiguous() 

127 out_perm = torch.empty_like(x_perm) 

128 work = dist.all_to_all_single(out_perm, x_perm, group=ctx.group, async_op=True) 

129 ctx.handle_box.append((work, out_perm)) 

130 return grad_output.new_zeros(ctx.x_shape), None, None, None, None, None, None, None 

131 

132 

133class _TorchAsyncAllGatherFunction(torch.autograd.Function): 

134 """Differentiable wrapper for pre-launched async all-gather.""" 

135 

136 @staticmethod 

137 def forward(ctx, x, work, out_perm, group, world_size, gather_dim, handle_box): # pylint: disable=arguments-differ 

138 """Wait for pre-launched all-gather and reconstruct the gathered tensor.""" 

139 ctx.group = group 

140 ctx.world_size = world_size 

141 ctx.gather_dim = gather_dim 

142 ctx.handle_box = handle_box 

143 ctx.x_shape = x.shape 

144 work.wait() 

145 return _move_dim_from_front(out_perm, gather_dim) 

146 

147 @staticmethod 

148 def backward(ctx, grad_output): 

149 """Launch reverse reduce-scatter for the all-gather.""" 

150 grad_perm = _move_dim_to_front(grad_output.contiguous(), ctx.gather_dim) 

151 output_shape = list(grad_perm.shape) 

152 if output_shape[0] % ctx.world_size != 0: 

153 raise ValueError( 

154 "all_gather backward expected gathered dimension to be divisible by world_size, " 

155 f"got {output_shape[0]} and {ctx.world_size}." 

156 ) 

157 output_shape[0] //= ctx.world_size 

158 output = torch.empty(output_shape, dtype=grad_perm.dtype, device=grad_perm.device) 

159 work = dist.reduce_scatter_tensor(output, grad_perm, group=ctx.group, async_op=True) 

160 if ctx.handle_box is not None: 

161 ctx.handle_box.append((work, output, ctx.gather_dim)) 

162 return grad_output.new_zeros(ctx.x_shape), None, None, None, None, None, None 

163 work.wait() 

164 return _move_dim_from_front(output, ctx.gather_dim), None, None, None, None, None, None 

165 

166 

167class _AsyncA2ALazyBwd(torch.autograd.Function): 

168 """All-to-all whose forward AND backward return ``AsyncCollectiveTensor``. 

169 

170 PyTorch's stock ``all_to_all_single_autograd`` calls ``wait_tensor`` in 

171 its backward eagerly, and the autograd engine binds backward stream 

172 context to the forward stream — so even if the BWD thread is wrapped 

173 in a side-stream context, that wait still lands on the FWD main 

174 stream and blocks Attention launches. 

175 

176 This Function bypasses the engine's binding by calling the 

177 non-autograd functional op in both directions and returning ACT. 

178 The wait is deferred to the next consumer's first non-view access 

179 (e.g. the indexing backward of ``_unpermute``), giving the FWD 

180 thread a small Python window to enqueue its Attention kernels onto 

181 the main stream **before** the wait lands there. 

182 """ 

183 

184 @staticmethod 

185 def forward(ctx, input_tensor, output_splits, input_splits, group): # pylint: disable=arguments-differ 

186 """Perform the forward all-to-all single collective, saving splits and group for backward.""" 

187 ctx.input_splits = input_splits 

188 ctx.output_splits = output_splits 

189 ctx.group = group 

190 # pylint: disable=C0415 

191 from torch.distributed._functional_collectives import all_to_all_single 

192 return all_to_all_single( 

193 input_tensor, output_splits, input_splits, group, 

194 ) 

195 

196 @staticmethod 

197 def backward(ctx, grad_output): 

198 """Compute the backward pass by performing the inverse all-to-all with swapped splits.""" 

199 # pylint: disable=C0415 

200 from torch.distributed._functional_collectives import all_to_all_single 

201 grad_input = all_to_all_single( 

202 grad_output, ctx.input_splits, ctx.output_splits, ctx.group, 

203 ) 

204 return grad_input, None, None, None 

205 

206 

207class _TorchSyncHookFunction(torch.autograd.Function): 

208 """Autograd identity that fires HookCoordinator rendezvous on fwd/bwd. 

209 

210 Uses a **4-hook** design (``A``, ``B``, ``C``, ``D``) with pure 

211 COMM / COMPUTE roles — no NONE role. Every rendezvous is a strict 

212 COMM + COMPUTE pair, guaranteeing NCCL-first dispatch ordering at 

213 **all** points including layer boundaries. 

214 

215 Hook placement per MoE layer:: 

216 

217 [A] → dispatch → [B] → module → [C] → combine → [D] → (Attention) → [A_next] 

218 

219 At layer boundaries (D / A hooks), the Attention that runs between 

220 layers is treated as COMPUTE, and the combine / combine.bwd is treated 

221 as COMM, so the coordinator enforces comm-first ordering even across 

222 layer transitions. 

223 """ 

224 

225 # 4-hook role tables: (prev_role_idx, next_role_idx). 

226 # Index encoding: 1 = COMM, 2 = COMPUTE. 

227 # 

228 # Torch only uses the four core hooks A/B/C/D + D_LAST sentinel. 

229 # The MS backend adds ``CHUNK_START`` / ``CHUNK_END`` because of 

230 # MS-specific issues (stream binding follows the calling thread; 

231 # autograd cannot have FWD-record + BWD-replay concurrently). 

232 # Torch has neither problem — CUDA streams are process-wide and 

233 # Torch autograd is thread-safe — so we keep the original 

234 # 4-hook design here. Do not add CHUNK_START / CHUNK_END to 

235 # the Torch tables; if a future test does need them, copy the 

236 # MS implementation and add the matching skip rules in 

237 # ``forward`` / ``backward``. 

238 _FWD_ROLES = { 

239 # (prev, next) prev op next op 

240 "A": (2, 1), # COMPUTE, COMM Attention | dispatch 

241 "B": (1, 2), # COMM, COMPUTE dispatch | module 

242 "C": (2, 1), # COMPUTE, COMM module | combine 

243 "D": (1, 2), # COMM, COMPUTE combine | Attention 

244 } 

245 _BWD_ROLES = { 

246 "D": (2, 1), # COMPUTE, COMM Attn.bwd | combine.bwd 

247 "C": (1, 2), # COMM, COMPUTE combine.bwd | module.bwd 

248 "B": (2, 1), # COMPUTE, COMM module.bwd | dispatch.bwd 

249 "A": (1, 2), # COMM, COMPUTE dispatch.bwd| Attn.bwd 

250 } 

251 

252 _ROLE_CACHE = None 

253 

254 @staticmethod 

255 def _role_enum(idx: int): 

256 if _TorchSyncHookFunction._ROLE_CACHE is None: 

257 from hyper_parallel.core.pipeline_parallel.hook_coordinator import HookRole # pylint: disable=C0415 

258 _TorchSyncHookFunction._ROLE_CACHE = (None, HookRole.COMM, HookRole.COMPUTE) 

259 return _TorchSyncHookFunction._ROLE_CACHE[idx] 

260 

261 @staticmethod 

262 def forward(ctx, x, hook_name, coordinator): # pylint: disable=arguments-differ 

263 """Identity forward that fires a HookCoordinator rendezvous. 

264 

265 Notifies the previous op's role and rendezvouses for the next op's 

266 role per the ``_FWD_ROLES`` table. ``"D_LAST"`` is a sentinel 

267 meaning "skip this rendezvous" (last layer's closing D — no 

268 Attention follows). 

269 

270 Args: 

271 ctx: Autograd context, stores ``hook_name`` and 

272 ``coordinator`` for the backward pass. 

273 x: Input tensor, returned unchanged. 

274 hook_name: One of ``"A"``, ``"B"``, ``"C"``, ``"D"``, 

275 ``"D_LAST"``. 

276 coordinator: The :class:`HookCoordinator` driving the rendezvous. 

277 

278 Returns: 

279 ``x`` unchanged. 

280 """ 

281 ctx.hook_name = hook_name 

282 ctx.coordinator = coordinator 

283 

284 if not coordinator.is_enabled(): 

285 return x 

286 

287 if hook_name == "D_LAST": 

288 # ``D_LAST`` marks the last layer's closing D hook — no 

289 # Attention follows in this chunk, so the rendezvous is 

290 # meaningless and is skipped. We still 

291 # ``notify_dispatched(COMM)`` so the COMPUTE side of the 

292 # preceding ``C`` rendezvous unblocks early, letting 

293 # BWD's Attn.bwd_last overlap with FWD's post-combine 

294 # work — Torch autograd is thread-safe so this concurrent 

295 # FWD-record + BWD-replay is fine. 

296 prev_idx, _ = _TorchSyncHookFunction._FWD_ROLES["D"] 

297 role_of = _TorchSyncHookFunction._role_enum 

298 coordinator.notify_dispatched(role_of(prev_idx)) 

299 return x 

300 

301 prev_idx, next_idx = _TorchSyncHookFunction._FWD_ROLES[hook_name] 

302 role_of = _TorchSyncHookFunction._role_enum 

303 coordinator.notify_dispatched(role_of(prev_idx)) 

304 coordinator.rendezvous(role_of(next_idx)) 

305 return x 

306 

307 @staticmethod 

308 def backward(ctx, grad_output): 

309 """Identity backward that fires a HookCoordinator rendezvous. 

310 

311 Mirror of :meth:`forward` using the ``_BWD_ROLES`` table. 

312 ``"D_LAST"`` skips the rendezvous because this is the first BWD 

313 hook to fire and ``combine.bwd`` has already dispatched freely 

314 before any rendezvous can happen. 

315 

316 Args: 

317 ctx: Autograd context with ``hook_name`` and 

318 ``coordinator`` saved during forward. 

319 grad_output: Gradient w.r.t. the forward output, returned 

320 unchanged. 

321 

322 Returns: 

323 ``(grad_output, None, None)`` — gradients only flow back to 

324 the tensor input, ``hook_name`` and ``coordinator`` are 

325 non-tensor inputs. 

326 """ 

327 hook_name = ctx.hook_name 

328 coordinator = ctx.coordinator 

329 

330 if not coordinator.is_enabled(): 

331 return grad_output, None, None 

332 

333 if hook_name == "D_LAST": 

334 # First BWD hook to fire; combine.bwd has already 

335 # dispatched freely before any rendezvous can happen. 

336 # Skipping here is safe on Torch because CUDA streams 

337 # are process-wide and the NCCL FIFO order is consistent 

338 # across ranks regardless of which thread launched 

339 # combine.bwd. 

340 return grad_output, None, None 

341 

342 prev_idx, next_idx = _TorchSyncHookFunction._BWD_ROLES[hook_name] 

343 role_of = _TorchSyncHookFunction._role_enum 

344 coordinator.notify_dispatched(role_of(prev_idx)) 

345 coordinator.rendezvous(role_of(next_idx)) 

346 return grad_output, None, None 

347 

348 

349class _TorchP2PExchangeFunction(torch.autograd.Function): 

350 """Symmetric bidirectional P2P: send local tensor to peer, receive peer's tensor.""" 

351 

352 @staticmethod 

353 def forward(ctx, tensor: torch.Tensor, peer_rank: int, group) -> torch.Tensor: # pylint: disable=arguments-differ 

354 """Perform symmetric bidirectional P2P exchange with peer_rank.""" 

355 ctx.peer_rank = peer_rank 

356 ctx.group = group 

357 send_buf = tensor.contiguous() 

358 recv_buf = torch.empty_like(send_buf) 

359 reqs = dist.batch_isend_irecv([ 

360 dist.P2POp(dist.isend, send_buf, peer_rank, group), 

361 dist.P2POp(dist.irecv, recv_buf, peer_rank, group), 

362 ]) 

363 for req in reqs: 

364 req.wait() 

365 return recv_buf 

366 

367 @staticmethod 

368 def backward(ctx, grad_output: torch.Tensor): 

369 """Perform symmetric P2P exchange for the backward gradient pass.""" 

370 send_buf = grad_output.contiguous() 

371 recv_buf = torch.empty_like(send_buf) 

372 reqs = dist.batch_isend_irecv([ 

373 dist.P2POp(dist.isend, send_buf, ctx.peer_rank, ctx.group), 

374 dist.P2POp(dist.irecv, recv_buf, ctx.peer_rank, ctx.group), 

375 ]) 

376 for req in reqs: 

377 req.wait() 

378 return recv_buf, None, None 

379 

380 

381class _TorchDifferentiableVariableAllGather(torch.autograd.Function): 

382 """Variable dim-zero all-gather with an uneven reduce-scatter backward.""" 

383 

384 @staticmethod 

385 def forward(ctx, input_tensor, output_splits, group): # pylint: disable=arguments-differ 

386 """Gather each rank's true row count without replicating inputs for A2A.""" 

387 if input_tensor.ndim == 0: 

388 raise ValueError("variable all-gather input must have at least one dimension") 

389 splits = tuple(output_splits) 

390 if not splits: 

391 raise ValueError("output_splits must contain at least one group rank") 

392 if any(not isinstance(rows, int) or isinstance(rows, bool) or rows < 0 for rows in splits): 

393 raise ValueError(f"output_splits must contain non-negative integers, got {splits!r}") 

394 

395 group_rank = dist.get_rank(group=group) 

396 if group_rank < 0 or group_rank >= len(splits): 

397 raise ValueError(f"group rank must be in [0, {len(splits)}), got {group_rank}") 

398 if input_tensor.shape[0] != splits[group_rank]: 

399 raise ValueError( 

400 "variable all-gather local rows must match output_splits at the group rank, " 

401 f"got local_rows={input_tensor.shape[0]}, group_rank={group_rank}, " 

402 f"output_splits={splits!r}" 

403 ) 

404 

405 input_tensor = input_tensor.contiguous() 

406 feature_shape = tuple(input_tensor.shape[1:]) 

407 if input_tensor.device.type == "npu": 

408 gathered = [input_tensor.new_empty((rows, *feature_shape)) for rows in splits] 

409 dist.all_gather(gathered, input_tensor, group=group) 

410 else: 

411 max_rows = max(splits) 

412 if max_rows == 0: 

413 gathered = [input_tensor.new_empty((0, *feature_shape)) for _ in splits] 

414 else: 

415 padded = input_tensor.new_zeros((max_rows, *feature_shape)) 

416 if input_tensor.shape[0] > 0: 

417 padded[:input_tensor.shape[0]].copy_(input_tensor) 

418 padded_outputs = [torch.empty_like(padded) for _ in splits] 

419 dist.all_gather(padded_outputs, padded, group=group) 

420 gathered = [ 

421 output[:rows].contiguous() 

422 for output, rows in zip(padded_outputs, splits) 

423 ] 

424 

425 ctx.output_splits = splits 

426 ctx.group = group 

427 ctx.group_rank = group_rank 

428 return torch.cat(gathered, dim=0) 

429 

430 @staticmethod 

431 def backward(ctx, grad_output): 

432 """Sum replicated output gradients and return this rank's uneven shard.""" 

433 output_rows = ctx.output_splits[ctx.group_rank] 

434 output = grad_output.new_empty((output_rows, *grad_output.shape[1:])) 

435 if sum(ctx.output_splits) == 0: 

436 return output, None, None 

437 

438 grad_output = grad_output.contiguous() 

439 if grad_output.device.type == "npu": 

440 from torch_npu.distributed import reduce_scatter_tensor_uneven # pylint: disable=C0415 

441 reduce_scatter_tensor_uneven( 

442 output, 

443 grad_output, 

444 input_split_sizes=list(ctx.output_splits), 

445 op=dist.ReduceOp.SUM, 

446 group=ctx.group, 

447 ) 

448 else: 

449 reduced = grad_output.clone() 

450 dist.all_reduce(reduced, op=dist.ReduceOp.SUM, group=ctx.group) 

451 start = sum(ctx.output_splits[:ctx.group_rank]) 

452 output.copy_(reduced.narrow(0, start, output_rows)) 

453 return output, None, None 

454 

455 

456# Mapping from string op names to torch.distributed.ReduceOp 

457_OP_MAP = { 

458 'sum': dist.ReduceOp.SUM, 

459 'prod': dist.ReduceOp.PRODUCT, 

460 'max': dist.ReduceOp.MAX, 

461 'min': dist.ReduceOp.MIN, 

462 # convert tensor elements to int32 and use MIN 

463 'all': dist.ReduceOp.MIN, 

464 # 'avg' is typically handled by SUM followed by division in current implementation logic 

465 'avg': dist.ReduceOp.SUM, 

466} 

467 

468# Try to add AVG for 'mean' if supported by current torch version 

469if hasattr(dist.ReduceOp, "AVG"): 

470 _OP_MAP['mean'] = dist.ReduceOp.AVG 

471else: 

472 # Fallback for older torch versions if necessary, though this might require manual division upstream 

473 # Assuming standard behavior where 'mean' implies native AVG support or upstream handling 

474 _OP_MAP['mean'] = dist.ReduceOp.SUM 

475 

476 

477def _ensure_contiguous(x): 

478 """Return a contiguous copy of *x* if not already contiguous.""" 

479 if torch.compiler.is_compiling(): 

480 return x.contiguous() 

481 if not x.is_contiguous() or x.storage_offset() != 0: 

482 return x.contiguous() 

483 return x 

484 

485 

486class _TorchBatchP2PWork: 

487 """Single ``.wait()`` handle wrapping the per-op works returned by 

488 ``torch.distributed.batch_isend_irecv``. 

489 

490 Torch returns one ``Work`` per op in the batch (the ops are coalesced 

491 onto one comm stream), whereas the platform contract — and the scheduler 

492 that consumes it — expects a single handle covering the whole batch so 

493 the wait can be deferred to one consumption point (mirroring MindSpore's 

494 single packaging ``CommHandle``). Waiting this handle waits every 

495 underlying op. 

496 """ 

497 

498 __slots__ = ("_works",) 

499 

500 def __init__(self, works): 

501 self._works = works 

502 

503 def wait(self): 

504 for work in self._works: 

505 if work is not None: 

506 work.wait() 

507 

508 

509# pylint: disable=C0103 

510class TorchPlatform(Platform): 

511 """Torch platform api""" 

512 Tensor = Tensor 

513 tensor = torch.tensor 

514 Parameter = Parameter 

515 Module = Module 

516 DTensorBase = DTensorBase 

517 PipelineStageBase = PipelineStageBase 

518 platform_type = PlatformType.PYTORCH 

519 tensor_dtype = torch 

520 dtype = torch.dtype 

521 Function = torch.autograd.Function 

522 

523 _custom_ops_cls = None 

524 

525 @property 

526 def custom_ops(self): 

527 """Return the Torch platform custom ops instance. 

528 

529 .. warning:: 

530 This is an experimental API that subject to change or deletion. 

531 

532 Returns: 

533 TorchCustomOps: Custom ops class that raises NotImplementedError 

534 for all operators (MindSpore-only at this time). 

535 """ 

536 if self._custom_ops_cls is None: 

537 from hyper_parallel.platform.torch.custom_ops import TorchCustomOps # pylint: disable=import-outside-toplevel 

538 self._custom_ops_cls = TorchCustomOps 

539 return self._custom_ops_cls 

540 

541 @staticmethod 

542 def get_swap_optimizer(): 

543 """Return the Torch optimizer-state swap wrapper class.""" 

544 from hyper_parallel.platform.torch.swap_optimizer.swap_optimizer import ( # pylint: disable=import-outside-toplevel 

545 get_swap_optimizer, 

546 ) 

547 return get_swap_optimizer() 

548 

549 @staticmethod 

550 def is_linear_module(module) -> bool: 

551 """Check whether *module* is a ``torch.nn.Linear`` instance.""" 

552 return isinstance(module, nn.Linear) 

553 

554 @staticmethod 

555 def is_embedding_module(module) -> bool: 

556 """Check whether *module* is a ``torch.nn.Embedding`` instance.""" 

557 return isinstance(module, nn.Embedding) 

558 

559 @staticmethod 

560 def device_count(device_handle): 

561 """ 

562 Get the number of available devices. 

563 

564 Args: 

565 device_handle: The device handle (e.g., torch.cuda, torch.npu). 

566 

567 Returns: 

568 int: The number of available devices. 

569 """ 

570 return device_handle.device_count() 

571 

572 def device_type(self): 

573 """ 

574 Get the current device type. 

575 

576 Returns: 

577 str: The device type string ("npu" for NPU, "cuda" for GPU). 

578 """ 

579 device_handle = self.get_device_handle() 

580 if device_handle == torch.npu: 

581 return "npu" 

582 return "cuda" 

583 

584 def device(self, device_idx=None): 

585 """ 

586 Get a torch.device object for the specified device index. 

587 

588 Args: 

589 device_idx (Optional[int]): The device index. If None, returns device without index. 

590 

591 Returns: 

592 torch.device: A torch device object. 

593 """ 

594 device_type = self.device_type() 

595 if device_idx is None: 

596 return torch.device(device_type) 

597 return torch.device(f"{device_type}:{device_idx:d}") 

598 

599 @staticmethod 

600 def get_rng_state(device=None, device_handle=None): 

601 """ 

602 Get the random number generator state. 

603 

604 Args: 

605 device (Optional): The device to get RNG state from. 

606 device_handle (Optional): The device handle (torch.cuda, torch.npu, etc.). 

607 

608 Returns: 

609 Tensor: The RNG state as a byte tensor. 

610 """ 

611 if device_handle is None: 

612 return torch.get_rng_state() 

613 if device is None: 

614 return device_handle.get_rng_state() 

615 return device_handle.get_rng_state(device) 

616 

617 @staticmethod 

618 def set_rng_state(state, device=None, device_handle=None): 

619 """ 

620 Set the random number generator state. 

621 

622 Args: 

623 state (Tensor): The RNG state to set. 

624 device (Optional): The device to set RNG state for. 

625 device_handle (Optional): The device handle (torch.cuda, torch.npu, etc.). 

626 """ 

627 if device_handle is None: 

628 return torch.set_rng_state(state) 

629 if device is None: 

630 return device_handle.set_rng_state(state) 

631 return device_handle.set_rng_state(state, device) 

632 

633 @staticmethod 

634 def manual_seed(seed): 

635 """ 

636 Set the random seed for reproducibility. 

637 

638 Args: 

639 seed (int): The random seed value. 

640 

641 Returns: 

642 torch.Generator: The random number generator. 

643 """ 

644 return torch.manual_seed(seed) 

645 

646 @staticmethod 

647 def ones(size, dtype=None): 

648 """ 

649 Create a tensor filled with ones. 

650 

651 Args: 

652 size (tuple): The shape of the output tensor. 

653 dtype (Optional[torch.dtype]): The desired data type. 

654 

655 Returns: 

656 Tensor: A tensor filled with ones. 

657 """ 

658 return torch.ones(size, dtype=dtype) 

659 

660 @staticmethod 

661 def zeros(size, dtype=None, device=None): 

662 """ 

663 Create a tensor filled with zeros. 

664 

665 Args: 

666 size (tuple): The shape of the output tensor. 

667 dtype (Optional[torch.dtype]): The desired data type. 

668 device (Optional[torch.device]): The device to create the tensor on. 

669 

670 Returns: 

671 Tensor: A tensor filled with zeros. 

672 """ 

673 return torch.zeros(size, dtype=dtype, device=device) 

674 

675 @staticmethod 

676 def full(size, fill_value, dtype=None): 

677 """ 

678 Create a tensor filled with a scalar value. 

679 

680 Args: 

681 size (tuple): The shape of the output tensor. 

682 fill_value (scalar): The value to fill the tensor with. 

683 dtype (Optional[torch.dtype]): The desired data type. 

684 

685 Returns: 

686 Tensor: A tensor filled with the specified value. 

687 """ 

688 return torch.full(size, fill_value, dtype=dtype) 

689 

690 @staticmethod 

691 def empty(size, dtype=None, device=None): 

692 """ 

693 Create an uninitialized tensor. 

694 

695 Args: 

696 size (tuple): The shape of the output tensor. 

697 dtype (Optional[torch.dtype]): The desired data type. 

698 device (Optional[torch.device or str]): Target device. When 

699 ``None`` the tensor is allocated on the default device 

700 (CPU under PyTorch defaults), matching the original 

701 back-compat behavior. 

702 

703 Returns: 

704 Tensor: An uninitialized tensor. 

705 """ 

706 return torch.empty(size, dtype=dtype, device=device) 

707 

708 @staticmethod 

709 def rand(size, dtype=None, device=None): 

710 """Create a tensor filled with uniform random values in ``[0, 1)``.""" 

711 return torch.rand(size, dtype=dtype, device=device) 

712 

713 @staticmethod 

714 def randn(size, dtype=None, device=None): 

715 """Create a tensor filled with standard-normal random values.""" 

716 return torch.randn(size, dtype=dtype, device=device) 

717 

718 @staticmethod 

719 def get_rank(): 

720 """ 

721 Get the rank of the current process in the distributed group. 

722 

723 Returns: 

724 int: The rank of the current process. 

725 """ 

726 return dist.get_rank() 

727 

728 @staticmethod 

729 def get_global_rank(group, group_rank): 

730 """ 

731 Get the global rank from a group rank. 

732 

733 Args: 

734 group (ProcessGroup): The process group. 

735 group_rank (int): The rank within the group. 

736 

737 Returns: 

738 int: The global rank. 

739 """ 

740 return dist.get_global_rank(group, group_rank) 

741 

742 @staticmethod 

743 def get_group_rank(group): 

744 """Return this process's rank within *group*.""" 

745 return dist.get_group_rank(group, dist.get_rank()) 

746 

747 @staticmethod 

748 def get_world_size(): 

749 """ 

750 Get the total number of processes in the distributed group. 

751 

752 Returns: 

753 int: The world size. 

754 """ 

755 return dist.get_world_size() 

756 

757 @staticmethod 

758 def get_param_local_shape(param): 

759 """ 

760 Get the local shape of a parameter, handling both regular and distributed tensors. 

761 

762 Args: 

763 param (Union[Tensor, DTensorBase]): The parameter tensor. 

764 

765 Returns: 

766 torch.Size: The local shape of the parameter. 

767 """ 

768 if isinstance(param, DTensorBase): 

769 return param.local_shape 

770 return param.shape 

771 

772 @staticmethod 

773 def get_param_local_data(param): 

774 """ 

775 Get the local data of a parameter, handling both regular and distributed tensors. 

776 

777 Args: 

778 param (Union[Tensor, DTensorBase]): The parameter tensor. 

779 

780 Returns: 

781 Tensor: The local tensor data. 

782 """ 

783 if isinstance(param, DTensorBase): 

784 return param.to_local() 

785 return param 

786 

787 @staticmethod 

788 def update_param_data(param, data): 

789 """ 

790 Update the data of a parameter. 

791 

792 Args: 

793 param (Parameter): The parameter to update. 

794 data (Tensor): The new data tensor. 

795 """ 

796 param.data = data 

797 

798 @staticmethod 

799 def load_into_param(param, data): 

800 """Load tensor *data* into *param* (plain tensor or DTensor).""" 

801 if isinstance(param, DTensorBase): 

802 local = param._local_tensor # pylint: disable=W0212 

803 if local.is_meta: 

804 # Meta tensor materialisation: replace the placeholder. 

805 orig_requires_grad = param.requires_grad 

806 param._local_tensor = data # pylint: disable=W0212 

807 if data.requires_grad != orig_requires_grad: 

808 param.requires_grad_(orig_requires_grad) 

809 else: 

810 local.copy_(data) 

811 else: 

812 param.copy_(data) 

813 

814 @staticmethod 

815 def get_op_name(func): 

816 """ 

817 Extract the operation name from various function types. 

818 

819 Args: 

820 func: The function or operation to extract the name from. 

821 

822 Returns: 

823 str: The operation name. 

824 """ 

825 if hasattr(func, "__name__"): 

826 return func.__name__ 

827 if isinstance(func, OpOverload): 

828 full_name = func.name 

829 core_name = full_name.split("::")[-1].split(".")[0] 

830 return core_name 

831 if isinstance(func, OpOverloadPacket): 

832 return func.name.split("::")[-1] 

833 func_str = str(func) 

834 if "built-in function" in func_str: 

835 return func_str.split()[-1].strip(">") 

836 if "function" in func_str: 

837 return func_str.split()[1] 

838 return "unknown_op" 

839 

840 @staticmethod 

841 def differentiable_all_gather_concat(data, group, concat_size, concat_dim, rank_list=None): 

842 data = _ensure_contiguous(data) 

843 output = list(dist_func.all_gather(data, group=group)) 

844 if rank_list is not None: 

845 group_ranks = dist.get_process_group_ranks(group) 

846 if tuple(rank_list) != tuple(group_ranks): 

847 rank_to_idx = {int(rank): idx for idx, rank in enumerate(group_ranks)} 

848 output = [output[rank_to_idx[int(rank)]] for rank in rank_list] 

849 return torch.cat(output, dim=concat_dim) 

850 

851 @staticmethod 

852 def chunk(data, split_dim, split_size, index): 

853 return torch.chunk(data, split_size, dim=split_dim)[index] 

854 

855 @staticmethod 

856 def differentiable_all_to_all(input_data, output_shape, group): 

857 input_data = _ensure_contiguous(input_data) 

858 output_tensor = torch.empty(output_shape, device=input_data.device, dtype=input_data.dtype) 

859 output_tensor = dist_func.all_to_all_single( 

860 output_tensor, 

861 input_data, 

862 group=group 

863 ) 

864 return output_tensor 

865 

866 @staticmethod 

867 def tensor_type_cast(input_data, cast_type): 

868 """Cast tensor to specified data type.""" 

869 type_mapping = { 

870 'float32': torch.float32, 

871 'float16': torch.float16, 

872 'int64': torch.int64, 

873 'int32': torch.int32 

874 } 

875 if cast_type not in type_mapping: 

876 raise ValueError(f"Unknown cast type: {cast_type}. Supported types: {list(type_mapping.keys())}") 

877 return input_data.to(type_mapping[cast_type]) 

878 

879 @staticmethod 

880 def differentiable_all_reduce(data, op, group): 

881 data = _ensure_contiguous(data) 

882 # Resolve the op from string to ReduceOp enum if necessary 

883 reduce_op = _OP_MAP.get(op, dist.ReduceOp.SUM) if isinstance(op, str) else op 

884 return dist_func.all_reduce(data, op=reduce_op, group=group) 

885 

886 @staticmethod 

887 def get_cell_construct(cell): 

888 return cell.forward 

889 

890 @staticmethod 

891 def get_cells_and_names(cell): 

892 return cell.named_modules() 

893 

894 @staticmethod 

895 def get_modules(module): 

896 return module.modules() 

897 

898 @staticmethod 

899 def search_parameter_by_name(cell, param_name: str): 

900 """ 

901 Find the parent Module of the parameter, the parameter's name in the parent Module, and the parameter. 

902 Return value: (parent Module instance, parameter's name in parent Module, parameter object). 

903 Returns None if not found. 

904 """ 

905 # Remove the "self." prefix from param_name 

906 param_name = param_name.replace("self.", "") 

907 # Case 1: The parameter is a direct parameter of the current Module 

908 if param_name in cell._parameters: # pylint: disable=protected-access 

909 return (cell, param_name, cell._parameters[param_name]) # pylint: disable=protected-access 

910 

911 # Case 2: The parameter is in a sub-Module 

912 if "." in param_name: 

913 cell_path, param_key = param_name.rsplit(".", 1) 

914 try: 

915 # Locate the sub-Module where the parameter resides (supports multi-level paths) 

916 target_cell = cell.get_submodule(cell_path) 

917 # Check if the sub-Module directly contains this parameter 

918 if param_key in target_cell._parameters: # pylint: disable=protected-access 

919 return target_cell, param_key, target_cell._parameters[param_key] # pylint: disable=protected-access 

920 except AttributeError: 

921 pass 

922 

923 # Traverse all sub-Modules (recursively) to search for the parameter 

924 for _, child_cell in cell.named_children(): 

925 if isinstance(child_cell, Module): 

926 result = TorchPlatform.search_parameter_by_name(child_cell, param_name) 

927 if result is not None: 

928 return result 

929 

930 return None 

931 

932 @staticmethod 

933 def update_parameter_by_name(cell, result: tuple, new_param) -> bool: 

934 """ 

935 Modify the original parameter in a Module or sub-Module using the search result 

936 """ 

937 parent_cell, param_key, _ = result 

938 # Key operation: directly modify the _parameters dictionary. 

939 if param_key in parent_cell._parameters: # pylint: disable=protected-access 

940 parent_cell._parameters[param_key] = new_param # pylint: disable=protected-access 

941 else: 

942 parent_cell.register_parameter(param_key, new_param) 

943 return True 

944 

945 @staticmethod 

946 def set_layout_into_parameter(param, layout): 

947 """Set layout into parameter""" 

948 from hyper_parallel.core.dtensor.dtensor import DTensor # pylint: disable=import-outside-toplevel 

949 from hyper_parallel.core.dtensor.layout import _get_slice_tensor_by_layout # pylint: disable=import-outside-toplevel 

950 if isinstance(param, DTensor): 

951 raise ValueError(f"Parameter {param} has been configured layout, cannot be set repeatedly.") 

952 requires_grad = param.requires_grad 

953 param_dtensor = DTensor.from_local( 

954 _get_slice_tensor_by_layout(param, layout), 

955 layout.mesh, layout.alias_placements) 

956 new_param = Parameter(param_dtensor, requires_grad=requires_grad) 

957 return new_param 

958 

959 @staticmethod 

960 def differentiable_reduce_scatter(data, dev_num, axis, op, group): 

961 data = _ensure_contiguous(data) 

962 input_tuple = torch.chunk(data, dev_num, dim=axis) 

963 output_tensor = torch.empty(input_tuple[0].shape, device=data.device, dtype=data.dtype) 

964 

965 # Resolve the op from string to ReduceOp enum 

966 reduce_op = _OP_MAP.get(op, dist.ReduceOp.SUM) if isinstance(op, str) else op 

967 

968 output_tensor = dist_func.reduce_scatter(output_tensor, input_tuple, op=reduce_op, group=group) 

969 

970 # Keep manual handling for 'avg' string as it maps to SUM in _OP_MAP 

971 if op == 'avg': 

972 output_tensor = output_tensor / dev_num 

973 return output_tensor 

974 

975 @staticmethod 

976 def get_device_handle(device_type: str = "npu"): 

977 """Return the torch device module (e.g. ``torch.npu`` or ``torch.cuda``) for the given device type.""" 

978 try: 

979 handle = getattr(torch, device_type) 

980 except AttributeError as e: 

981 raise RuntimeError(f"TorchPlatform expect got device handle: 'torch.{device_type}' failed.") from e 

982 return handle 

983 

984 @staticmethod 

985 def get_param_type_size(param): 

986 # pylint: disable=W0212 

987 return torch._utils._element_size(param.dtype) 

988 

989 @staticmethod 

990 def is_tensor(obj: Any) -> bool: 

991 """Return True if ``obj`` is a ``torch.Tensor``.""" 

992 return isinstance(obj, Tensor) 

993 

994 @staticmethod 

995 def get_tensor_storage_size(tensor: Any) -> int: 

996 """Return serialized byte size (numel * element size) for a PyTorch tensor.""" 

997 if not TorchPlatform.is_tensor(tensor): 

998 raise TypeError( 

999 f"TorchPlatform.get_tensor_storage_size expects torch.Tensor, got {type(tensor)!r}" 

1000 ) 

1001 return int(tensor.numel()) * int(tensor.element_size()) 

1002 

1003 @staticmethod 

1004 def parameters_dict(cell: Module): 

1005 return cell.named_parameters() 

1006 

1007 @staticmethod 

1008 def buffers_dict(cell: Module) -> Any: 

1009 """Return all named buffers registered by the module tree.""" 

1010 return cell.named_buffers() 

1011 

1012 @staticmethod 

1013 def get_model_state_dict(model: Any, *, options: Any = None) -> dict[str, Any]: 

1014 """Get the state dictionary of a model. 

1015 

1016 Delegates to torch-specific implementation that handles DTensor 

1017 gathering, CPU offloading and frozen-parameter filtering. 

1018 """ 

1019 # pylint: disable=C0415 

1020 from hyper_parallel.platform.torch.fully_shard.state_dict_utils import ( 

1021 get_model_state_dict as _get_model_state_dict, 

1022 ) 

1023 return _get_model_state_dict(model, options=options) 

1024 

1025 @staticmethod 

1026 def set_model_state_dict(model: Any, model_state_dict: dict[str, Any], *, options: Any = None) -> None: 

1027 """Set the state dictionary of a model. 

1028 

1029 Delegates to torch-specific implementation that scatters full tensors 

1030 into DTensor shards and performs an in-place load. 

1031 """ 

1032 # pylint: disable=C0415 

1033 from hyper_parallel.platform.torch.fully_shard.state_dict_utils import ( 

1034 set_model_state_dict as _set_model_state_dict, 

1035 ) 

1036 return _set_model_state_dict(model, model_state_dict, options=options) 

1037 

1038 @staticmethod 

1039 def save_checkpoint(cell: Module, file_path: str, ckpt_format: str = "safetensors") -> None: 

1040 if ckpt_format == "safetensors": 

1041 save_file(tensors=cell, filename=file_path) 

1042 else: 

1043 torch.save(obj=cell, f=file_path) 

1044 

1045 @staticmethod 

1046 def load_checkpoint(file_path: str, ckpt_format: str = "safetensors") -> dict: 

1047 if ckpt_format == "safetensors": 

1048 return load_file(filename=file_path) 

1049 return torch.load(f=file_path) 

1050 

1051 @staticmethod 

1052 def new_zero_parameter(param_shape, param_type, requires_grad, device): 

1053 return nn.Parameter(torch.zeros(param_shape, dtype=param_type, device=device), requires_grad=requires_grad) 

1054 

1055 @staticmethod 

1056 def new_tensor(tensor_shape, tensor_type, device): 

1057 return torch.empty(size=tensor_shape, dtype=tensor_type, device=device) 

1058 

1059 @staticmethod 

1060 def full_like(tensor, fill_value, dtype=None): 

1061 return torch.full_like(tensor, fill_value, dtype=dtype) 

1062 

1063 @staticmethod 

1064 def set_tensor_requires_grad(input_tensor): 

1065 """ 

1066 set requires grad flag for input tensor, only effective for leaf node 

1067 """ 

1068 if input_tensor.is_leaf: 

1069 input_tensor.requires_grad = True 

1070 

1071 def _create_group(self, rank_list): 

1072 normalized_rank_list = tuple(sorted(rank_list)) 

1073 world_rank_list = tuple(range(self.get_world_size())) 

1074 if normalized_rank_list == world_rank_list: 

1075 group = _get_default_group() 

1076 EXISTING_COMM_GROUPS[str(normalized_rank_list)] = group 

1077 return group 

1078 group_dict = create_sub_groups(rank_list) 

1079 return group_dict[normalized_rank_list] 

1080 

1081 @staticmethod 

1082 def all_gather_into_tensor(data, group_info, async_op=False): 

1083 output_shape = list(data.shape) 

1084 output_shape[0] = output_shape[0] * group_info.rank_size 

1085 output = torch.empty(output_shape, dtype=data.dtype, device=data.device) 

1086 handle = dist.all_gather_into_tensor(output, data, group=group_info.group, async_op=async_op) 

1087 return output, handle 

1088 

1089 @staticmethod 

1090 def all_gather_single(input_tensor, output_shape, group, async_op=False): 

1091 output = torch.empty(output_shape, dtype=input_tensor.dtype, device=input_tensor.device) 

1092 handle = dist.all_gather_into_tensor(output, input_tensor, group=group, async_op=async_op) 

1093 return output, handle 

1094 

1095 @staticmethod 

1096 def all_reduce(data, group_info, async_op=False): 

1097 if not data.is_contiguous(): 

1098 data = data.contiguous() 

1099 handle = dist.all_reduce(data, group=group_info.group, async_op=async_op) 

1100 return data, handle 

1101 

1102 @staticmethod 

1103 def broadcast(data, src=None, group=None, async_op=False, group_src=None): 

1104 if group_src is not None: 

1105 src = dist.get_global_rank(group, group_src) 

1106 handle = dist.broadcast(data, src, group, async_op) 

1107 if async_op and handle is not None: 

1108 handle.wait() 

1109 

1110 @staticmethod 

1111 def scatter(output, scatter_list, src=None, group=None, async_op=False, group_src=None): 

1112 if group_src is not None: 

1113 src = dist.get_global_rank(group, group_src) 

1114 handle = dist.scatter(output, scatter_list, src=src, group=group, async_op=async_op) 

1115 if async_op and handle is not None: 

1116 handle.wait() 

1117 return output 

1118 

1119 @staticmethod 

1120 def isend(tensor, dst=None, group=None, tag=0): 

1121 return dist.isend(tensor, dst, group, tag) 

1122 

1123 @staticmethod 

1124 def irecv(tensor, src=None, group=None, tag=0): 

1125 return dist.irecv(tensor, src, group, tag) 

1126 

1127 @staticmethod 

1128 def p2p_op(op_type, tensor, peer, group=None): 

1129 # torch's P2POp takes the op callable (dist.isend / dist.irecv), not 

1130 # the "isend"/"irecv" string the stage specs builders emit. 

1131 if op_type == "isend": 

1132 op = dist.isend 

1133 elif op_type == "irecv": 

1134 op = dist.irecv 

1135 else: 

1136 raise ValueError( 

1137 f"p2p_op op_type must be 'isend' or 'irecv', but got {op_type!r}." 

1138 ) 

1139 return dist.P2POp(op, tensor, peer, group) 

1140 

1141 @staticmethod 

1142 def batch_isend_irecv(p2p_ops): 

1143 """Launch a peer-batched P2P group as one coalesced op. 

1144 

1145 ``torch.distributed.batch_isend_irecv`` coalesces the ops onto one 

1146 comm stream and returns one ``Work`` per op; we wrap them in a single 

1147 ``.wait()`` handle so a send and a recv to the same peer overlap on 

1148 the duplex link and the caller can defer the whole batch's wait to one 

1149 consumption point. 

1150 """ 

1151 if not p2p_ops: 

1152 return None 

1153 works = dist.batch_isend_irecv(p2p_ops) 

1154 return _TorchBatchP2PWork(works) if works else None 

1155 

1156 @staticmethod 

1157 def prepare_batch_p2p_group(group: Any = None) -> None: 

1158 """Synchronize a group before its first subset batched P2P call. 

1159 

1160 PyTorch requires every rank in a process group to participate when 

1161 ``batch_isend_irecv`` is the first collective on that group. A barrier 

1162 at the common pipeline run boundary initializes the communicator 

1163 before ranks reach peer operations at different times. 

1164 

1165 Args: 

1166 group: The process group used by the batched P2P operations. 

1167 ``None`` uses the default group. 

1168 """ 

1169 dist.barrier(group=group) 

1170 

1171 @staticmethod 

1172 def p2p_exchange(tensor, peer_rank: int, group=None): 

1173 if peer_rank == dist.get_rank(group): 

1174 return tensor 

1175 return _TorchP2PExchangeFunction.apply(tensor, peer_rank, group) 

1176 

1177 @staticmethod 

1178 def send_object_list(obj_list, dst=None, group=None): 

1179 dist.send_object_list(obj_list, dst, group) 

1180 

1181 @staticmethod 

1182 def recv_object_list(obj_list, src=None, group=None): 

1183 dist.recv_object_list(obj_list, src, group) 

1184 

1185 @staticmethod 

1186 def reduce_scatter_tensor(data, group_info, async_op=False): 

1187 output_shape = list(data.shape) 

1188 output_shape[0] = output_shape[0] // group_info.rank_size 

1189 output = torch.empty(output_shape, dtype=data.dtype, device=data.device) 

1190 handle = dist.reduce_scatter_tensor(output, data, group=group_info.group, async_op=async_op) 

1191 return output, handle 

1192 

1193 @staticmethod 

1194 def reduce_scatter_single(input_tensor, output_shape, group, async_op=False): 

1195 output = torch.empty(output_shape, dtype=input_tensor.dtype, device=input_tensor.device) 

1196 handle = dist.reduce_scatter_tensor(output, input_tensor, group=group, async_op=async_op) 

1197 return output, handle 

1198 

1199 @staticmethod 

1200 def all_to_all_single(input_tensor, output_shape, group, async_op=False): 

1201 output = torch.empty(output_shape, device=input_tensor.device, dtype=input_tensor.dtype) 

1202 work = dist.all_to_all_single(output, input_tensor, group=group, async_op=async_op) 

1203 return output, work 

1204 

1205 @staticmethod 

1206 def differentiable_all_to_all_single(input_tensor, input_splits, output_splits, group): 

1207 """Variable-split all-to-all with autograd support for EP token dispatch/combine.""" 

1208 out_total = sum(output_splits) 

1209 output = torch.empty( 

1210 out_total, *input_tensor.shape[1:], 

1211 dtype=input_tensor.dtype, device=input_tensor.device, 

1212 ) 

1213 output = dist_func.all_to_all_single( 

1214 output, input_tensor, 

1215 output_split_sizes=output_splits, 

1216 input_split_sizes=input_splits, 

1217 group=group, 

1218 ) 

1219 return output 

1220 

1221 @staticmethod 

1222 def differentiable_all_to_all_single_async(input_tensor, input_splits, output_splits, group): 

1223 """Truly-async variant of :meth:`differentiable_all_to_all_single`. 

1224 

1225 Both forward AND backward return :class:`AsyncCollectiveTensor`, 

1226 so the ``wait_tensor`` op is queued lazily — only when a downstream 

1227 kernel actually reads the result. 

1228 

1229 Why both directions need lazy wait: 

1230 

1231 * FWD: ACT lazy wait lets host return immediately and the paired 

1232 BWD thread's compute kernel slip into the queue before the wait. 

1233 * BWD: PyTorch's stock backward issues ``wait_tensor`` eagerly, 

1234 and the autograd engine binds backward stream to the forward 

1235 stream — so even running BWD inside a ``with torch.npu.stream 

1236 (side_stream)`` context does not move that wait off the main 

1237 stream. Returning ACT from backward defers the wait to the 

1238 next backward op's first consumption, opening a small window 

1239 during which FWD's Attention kernels can be queued onto the 

1240 main stream **before** the wait lands. 

1241 

1242 Args: 

1243 input_tensor: Input tensor, split along dim 0 by ``input_splits``. 

1244 input_splits: ``list[int]`` — rows sent to each rank. 

1245 output_splits: ``list[int]`` — rows received from each rank. 

1246 group: Process group. 

1247 

1248 Returns: 

1249 ``AsyncCollectiveTensor`` of shape 

1250 ``[sum(output_splits), *input_tensor.shape[1:]]``. 

1251 """ 

1252 return _AsyncA2ALazyBwd.apply(input_tensor, output_splits, input_splits, group) 

1253 

1254 @staticmethod 

1255 def differentiable_variable_all_gather( 

1256 input_tensor: Tensor, output_splits: Sequence[int], group: Any) -> Tensor: 

1257 """Gather variable dim-zero shards on HCCL or Gloo with autograd support.""" 

1258 return _TorchDifferentiableVariableAllGather.apply( 

1259 input_tensor, tuple(output_splits), group 

1260 ) 

1261 

1262 @staticmethod 

1263 def wait_async_tensor(tensor): 

1264 """Wait for an async collective tensor to become materialised. 

1265 

1266 Idempotent — calling on an already-waited tensor is a no-op. 

1267 

1268 Args: 

1269 tensor: ``AsyncCollectiveTensor`` whose device-side values may 

1270 not yet be ready. 

1271 

1272 Returns: 

1273 The same *tensor*, now fully materialised. 

1274 """ 

1275 from torch.distributed._functional_collectives import wait_tensor # pylint: disable=C0415 

1276 wait_tensor(tensor) 

1277 return tensor 

1278 

1279 @staticmethod 

1280 def differentiable_async_allgather_wait(x, work, out_perm, group, world_size, gather_dim, 

1281 handle_box=None): 

1282 """Wait async all-gather handle and reconstruct result (differentiable).""" 

1283 return _TorchAsyncAllGatherFunction.apply( 

1284 x, work, out_perm, group, world_size, gather_dim, handle_box 

1285 ) 

1286 

1287 @staticmethod 

1288 def arange(start, end=None, step=1, dtype=None, device=None): 

1289 """Create a 1-D tensor with evenly spaced values.""" 

1290 if end is None: 

1291 return torch.arange(start, dtype=dtype, device=device) 

1292 return torch.arange(start, end, step, dtype=dtype, device=device) 

1293 

1294 @staticmethod 

1295 def differentiable_async_a2a_wait(x, work, out_perm, group, world_size, concat_dim, split_dim, 

1296 handle_box=None): 

1297 """Wait async A2A handle and reconstruct result (differentiable). 

1298 

1299 Args: 

1300 x: Input tensor. 

1301 work: Async work handle from all_to_all. 

1302 out_perm: Output buffer from all_to_all. 

1303 group: Process group. 

1304 world_size: World size. 

1305 concat_dim: Dimension for concatenation. 

1306 split_dim: Dimension for split. 

1307 handle_box: Optional mutable list; backward appends (work, out_perm) here. 

1308 """ 

1309 return _TorchAsyncA2AFunction.apply( 

1310 x, work, out_perm, group, world_size, concat_dim, split_dim, handle_box 

1311 ) 

1312 

1313 @staticmethod 

1314 def differentiable_sync_hook(x, hook_name: str, coordinator): 

1315 """Identity op that fires coordinator rendezvous on forward and backward. 

1316 

1317 Always goes through ``_TorchSyncHookFunction.apply`` so that the 

1318 autograd graph **records a SyncHook node regardless of whether the 

1319 coordinator is currently enabled**. Skipping ``apply`` when 

1320 disabled would leave warmup-forwarded graphs without the hook 

1321 nodes, and a later ``overlap.run`` — whose BWD thread back-props 

1322 such a graph — would then traverse zero hooks while the paired FWD 

1323 thread (whose current forward DOES record hooks) waits at a 

1324 barrier for a partner that never arrives. 

1325 

1326 Args: 

1327 x: Input tensor. 

1328 hook_name: One of: 

1329 * ``"A"`` / ``"B"`` / ``"C"`` / ``"D"`` — 

1330 full rendezvous on both directions. 

1331 * ``"D_LAST"`` — closing D of the last MoE 

1332 layer in a chunk. Forward: ``notify_dispatched`` 

1333 only (no Attention follows so rendezvous is 

1334 skipped). Backward: pure skip (first BWD 

1335 hook to fire; combine.bwd has already 

1336 dispatched freely). 

1337 coordinator: A :class:`HookCoordinator` instance. 

1338 """ 

1339 return _TorchSyncHookFunction.apply(x, hook_name, coordinator) 

1340 

1341 @staticmethod 

1342 def get_tensor_transform(): 

1343 raise NotImplementedError("Unsupported get_tensor_transform for torch platform") 

1344 

1345 @staticmethod 

1346 def construct_strided_slice(x, begin, end, stride): 

1347 raise NotImplementedError("Unsupported construct_strided_slice for torch platform") 

1348 

1349 @staticmethod 

1350 def micro_batch(micro_batch_num, args_batch_dim=None, kwargs_batch_dim=None): 

1351 # pylint: disable=C0415 

1352 from hyper_parallel.platform.torch.pipeline_parallel._utils import _MicroBatch 

1353 return _MicroBatch(micro_batch_num, args_batch_dim, kwargs_batch_dim) 

1354 

1355 @staticmethod 

1356 def get_symmetric_memory_handler(): 

1357 # pylint: disable=C0415 

1358 from hyper_parallel.platform.torch.symmetric_memory import TorchSymmetricMemoryHandler 

1359 symmetric_memory = TorchSymmetricMemoryHandler() 

1360 return symmetric_memory 

1361 

1362 @staticmethod 

1363 def get_multicore_handler(): 

1364 """Return a TorchMulticoreHandler instance for multi-core device management.""" 

1365 # pylint: disable=C0415 

1366 from hyper_parallel.platform.torch.multicore import TorchMulticoreHandler 

1367 return TorchMulticoreHandler() 

1368 

1369 def new_stream(self): 

1370 device = self.get_device_handle() 

1371 return device.Stream() 

1372 

1373 def get_stream_context(self): 

1374 device = self.get_device_handle() 

1375 return device.stream 

1376 

1377 @staticmethod 

1378 def all_gather_object(object_list, obj, group=None) -> None: 

1379 """ 

1380 Gathers objects from the given group into object list. 

1381 

1382 Args: 

1383 object_list (list[Any]): Define the output list, which size equal to the size of group. 

1384 obj (Any): The object on current rank and in given process group. 

1385 group (ProcessGroup, optional): The process group to gather obj. Default is ``None``, and ``None`` means 

1386 global group. 

1387 

1388 Returns: 

1389 None. Objs are gathered into ``object_list``. 

1390 """ 

1391 dist.all_gather_object(object_list, obj, group) 

1392 

1393 @staticmethod 

1394 def barrier(group=None, async_op: bool = False, device_ids=None) -> Any: 

1395 """ 

1396 Synchronize all processes in the given process group. 

1397 

1398 Args: 

1399 group (ProcessGroup, optional): The process group to work on. Default is ``None``, 

1400 meaning the default process group. 

1401 async_op (bool, optional): Whether this op should be asynchronous. Default: ``False``. 

1402 device_ids (list[int], optional): Device ids for backends that require a device for 

1403 barrier (e.g. NCCL). Default: ``None``. 

1404 

1405 Returns: 

1406 Async work handle if ``async_op`` is True; otherwise ``None``. 

1407 """ 

1408 return dist.barrier(group, async_op, device_ids) 

1409 

1410 @staticmethod 

1411 def init_process_group( 

1412 backend: Optional[str] = None, 

1413 *, 

1414 init_method: Optional[str] = None, 

1415 timeout: Optional[timedelta] = None, 

1416 world_size: int = -1, 

1417 rank: int = -1, 

1418 store: Optional[Store] = None, 

1419 pg_options: Optional[Any] = None, 

1420 device_id: Optional[Union[torch.device, int]] = None, 

1421 ) -> None: 

1422 """ 

1423 Initialize global process group. 

1424 

1425 Args: 

1426 backend (str or Backend, optional): The backend to use for distributed communication. 

1427 init_method (str, optional): URL specifying how to initialize the process group. Default is "env://", 

1428 can not be specified at the same time with ``store``. 

1429 timeout (timedelta, optional): Timeout for process group. Default 10 minutes for NCCL and for other 

1430 backends 30 minutes. 

1431 world_size (int, optional): Number of processes. If ``store`` is specified, world_size is required. 

1432 rank (int, optional): Rank of the current process, which value must between 0 and ``world_size``-1. If 

1433 ``store`` is specified, rank is required. 

1434 store (Store, optional): Key/value store accessible to all workers, used to exchange connection/address 

1435 information. Can not be specified at the same time with ``init_method``. 

1436 pg_options (ProcessGroupOptions, optional): Extra options to pass during constructing process groups. 

1437 device_id (torch.device | int, optional): Specific device this process will work on. 

1438 """ 

1439 try: 

1440 _get_default_group() 

1441 # except multi version error 

1442 except (ValueError, RuntimeError): 

1443 if backend is None: 

1444 backend = "hccl" 

1445 dist.init_process_group(backend=backend, init_method=init_method, timeout=timeout, world_size=world_size, 

1446 rank=rank, store=store, pg_options=pg_options, device_id=device_id) 

1447 

1448 @staticmethod 

1449 def destroy_process_group(group: Optional[ProcessGroup] = None) -> None: 

1450 """ 

1451 Destroy given process group. 

1452 

1453 Args: 

1454 group (ProcessGroup, optional): Given process group will be destroyed, if not given, all process groups 

1455 will be destroyed. 

1456 """ 

1457 group = group or _get_default_group() 

1458 if group in EXISTING_COMM_GROUPS.values(): 

1459 keys_to_destroy = [k for k, v in EXISTING_COMM_GROUPS.items() if v == group] 

1460 for k in keys_to_destroy: 

1461 del EXISTING_COMM_GROUPS[k] 

1462 dist.destroy_process_group(group) 

1463 

1464 @staticmethod 

1465 def get_process_group_ranks(group: Optional[ProcessGroup] = None) -> list[int]: 

1466 """ 

1467 Get all ranks relative to given process group. 

1468 

1469 Args: 

1470 group (Optional[ProcessGroup]): Process group worked on. Default is ``None``, and ``None`` means global 

1471 group. 

1472 

1473 Returns: 

1474 Rank list. 

1475 """ 

1476 group = group or _get_default_group() 

1477 return dist.get_process_group_ranks(group) 

1478 

1479 @staticmethod 

1480 def get_backend(group: Optional[ProcessGroup] = None) -> Backend: 

1481 """ 

1482 Get the backend of the given process group. 

1483 

1484 Args: 

1485 group (ProcessGroup, optional): Process group worked on. Default is ``None``, and ``None`` means global 

1486 group. 

1487 

1488 Returns: 

1489 The backend object of the given process group. 

1490 """ 

1491 group = group or _get_default_group() 

1492 return dist.get_backend(group) 

1493 

1494 @staticmethod 

1495 def split_group(parent_pg: Optional[ProcessGroup] = None, 

1496 split_ranks: Optional[list] = None, 

1497 timeout: Optional[timedelta] = None, 

1498 pg_options: Optional[Any] = None, 

1499 group_desc: Optional[str] = None, 

1500 ) -> Optional[ProcessGroup]: 

1501 """ 

1502 Create split groups for every group rank in split_ranks, and return the split process group which relative to 

1503 current rank id. 

1504 

1505 Args: 

1506 parent_pg (Optional[ProcessGroup]): A process group which the goal group split from. 

1507 split_ranks (Optional[list]): A list like ``list[list[int]]``. 

1508 timeout (Optional[timedelta]): Timeout for process group. Default 10 minutes for NCCL and for other 

1509 backend 30 minutes. 

1510 pg_options (Optional[Any]): Extra options to pass during constructing process groups. 

1511 group_desc (Optional[str]): Description of process group. 

1512 

1513 Return: 

1514 Optional[ProcessGroup]: One of split process group which relative to current rank id 

1515 """ 

1516 if split_ranks is None or len(split_ranks) == 0: 

1517 raise ValueError("split_ranks cannot be None or empty") 

1518 

1519 split_group = None 

1520 for split_rank in split_ranks: 

1521 dist_group = TorchPlatform.get_created_group(split_rank) 

1522 if dist_group is None: 

1523 dist_group = dist.new_group(ranks=split_rank) 

1524 EXISTING_COMM_GROUPS[str(tuple(sorted(split_rank)))] = dist_group 

1525 if TorchPlatform.get_rank() in split_rank: 

1526 split_group = dist_group 

1527 

1528 return split_group 

1529 

1530 @staticmethod 

1531 def get_group_local_rank(group: ProcessGroup = None) -> int: 

1532 """get group local rank id.""" 

1533 group = group or _get_default_group() 

1534 return group.rank() 

1535 

1536 @staticmethod 

1537 def no_grad(): 

1538 return torch.no_grad() 

1539 

1540 @staticmethod 

1541 def preserve_version_counter(tensor): 

1542 return torch.autograd._unsafe_preserve_version_counter(tensor) # pylint: disable=W0212 

1543 

1544 @staticmethod 

1545 def relu(tensor): 

1546 return torch.relu(tensor) 

1547 

1548 @staticmethod 

1549 def cat(tensors, dim=0): 

1550 return torch.cat(tensors, dim=dim) 

1551 

1552 @staticmethod 

1553 def empty_like(tensor, *, dtype=None, device=None, pin_memory=False): 

1554 return torch.empty_like(tensor, dtype=dtype, device=device, pin_memory=pin_memory) 

1555 

1556 def get_current_stream(self): 

1557 device = self.get_device_handle() 

1558 return device.current_stream() 

1559 

1560 def new_event(self): 

1561 device = self.get_device_handle() 

1562 return device.Event() 

1563 

1564 def tree_map(self, fn, tree): 

1565 return torch.utils._pytree.tree_map(fn, tree) # pylint: disable=protected-access 

1566 

1567 @property 

1568 def checkpoint(self): 

1569 # pylint: disable=C0415 

1570 from hyper_parallel.platform.torch.activation_checkpoint.checkpoint import checkpoint 

1571 return checkpoint 

1572 

1573 @staticmethod 

1574 def recompute_handle_collector_ctx(): 

1575 # pylint: disable=C0415 

1576 from hyper_parallel.platform.torch.activation_checkpoint.checkpoint import recompute_handle_collector_ctx 

1577 return recompute_handle_collector_ctx() 

1578 

1579 @staticmethod 

1580 def recompute_handle(handle, session_id): 

1581 # pylint: disable=C0415 

1582 from hyper_parallel.platform.torch.activation_checkpoint.checkpoint import recompute_handle 

1583 return recompute_handle(handle, session_id) 

1584 

1585 @staticmethod 

1586 def recompute_session_ctx(session_id, retain_on_unpack=False): 

1587 # pylint: disable=C0415 

1588 from hyper_parallel.platform.torch.activation_checkpoint.checkpoint import recompute_session_ctx 

1589 return recompute_session_ctx(session_id=session_id, retain_on_unpack=retain_on_unpack) 

1590 

1591 @staticmethod 

1592 def clear_recompute_session(session_id): 

1593 # pylint: disable=C0415 

1594 from hyper_parallel.platform.torch.activation_checkpoint.checkpoint import clear_recompute_session 

1595 return clear_recompute_session(session_id) 

1596 

1597 @staticmethod 

1598 def is_compiling() -> bool: 

1599 """Return whether execution is currently captured by ``torch.compile``.""" 

1600 return torch.compiler.is_compiling() 

1601 

1602 @staticmethod 

1603 def checkpoint_wrapper(module, **checkpoint_kwargs): 

1604 # pylint: disable=C0415 

1605 from hyper_parallel.platform.torch.activation_checkpoint.checkpoint_wrapper import ckpt_wrapper 

1606 return ckpt_wrapper(module, **checkpoint_kwargs) 

1607 

1608 @staticmethod 

1609 def checkpoint_exclude_wrapper(module: Any, *, save_output: bool = True) -> Any: 

1610 """Wrap a module or callable whose activations should not be recomputed. 

1611 

1612 Args: 

1613 module: PyTorch Module or callable to exclude from checkpoint replay. 

1614 save_output: Whether to retain the excluded region output for replay. 

1615 

1616 Returns: 

1617 The platform-specific checkpoint exclusion wrapper. 

1618 """ 

1619 # pylint: disable=C0415 

1620 from hyper_parallel.platform.torch.activation_checkpoint.checkpoint_exclude_wrapper import ( 

1621 checkpoint_exclude_wrapper, 

1622 ) 

1623 return checkpoint_exclude_wrapper(module, save_output=save_output) 

1624 

1625 @staticmethod 

1626 def swap_wrapper(module, policy_fn=None, group_swap=False): 

1627 # pylint: disable=C0415 

1628 from hyper_parallel.platform.torch.activation_checkpoint.activation_swap import swap_wrapper 

1629 return swap_wrapper(module, policy_fn=policy_fn, group_swap=group_swap) 

1630 

1631 @staticmethod 

1632 def swap_tensor_wrapper(target, tag=None, group_swap=False): 

1633 # pylint: disable=C0415 

1634 from hyper_parallel.platform.torch.activation_checkpoint.activation_swap import swap_tensor_wrapper 

1635 return swap_tensor_wrapper(target, tag=tag, group_swap=group_swap) 

1636 

1637 @staticmethod 

1638 def get_class_activation_wrapper(): 

1639 # pylint: disable=C0415 

1640 from hyper_parallel.platform.torch.activation_checkpoint.activation_swap import ActivationWrapper 

1641 return ActivationWrapper 

1642 

1643 @property 

1644 def noop_context_fn(self): 

1645 return noop_context_fn 

1646 

1647 @staticmethod 

1648 def ignore_sac_ops(ignore_ops: list[object | None]) -> None: 

1649 # pylint: disable=C0415 

1650 from hyper_parallel.platform.torch.activation_checkpoint.sac import ignore_sac_ops 

1651 ignore_sac_ops(ignore_ops) 

1652 

1653 @staticmethod 

1654 def create_selective_checkpoint_contexts(policy_fn_or_list, allow_cache_entry_mutation=False, group_swap=False): 

1655 # pylint: disable=C0415 

1656 from hyper_parallel.platform.torch.activation_checkpoint.sac import create_selective_checkpoint_contexts 

1657 return create_selective_checkpoint_contexts(policy_fn_or_list, allow_cache_entry_mutation, group_swap) 

1658 

1659 @staticmethod 

1660 def create_native_selective_checkpoint_contexts(policy_fn: Callable) -> Any: 

1661 """Create Torch-native selective-checkpoint contexts for compile.""" 

1662 # pylint: disable=C0415 

1663 from hyper_parallel.platform.torch.activation_checkpoint.native_compile import ( 

1664 create_native_selective_checkpoint_contexts, 

1665 ) 

1666 return create_native_selective_checkpoint_contexts(policy_fn) 

1667 

1668 @staticmethod 

1669 def async_save_on_cpu(policy_fn=None, group_swap: bool = False): 

1670 # pylint: disable=C0415 

1671 from hyper_parallel.platform.torch.activation_checkpoint.activation_swap import AsyncSaveOnCpu 

1672 return AsyncSaveOnCpu(policy_fn, group_swap=group_swap) 

1673 

1674 @staticmethod 

1675 def get_element_size(tensor): 

1676 """Get Tensor Element Size""" 

1677 return tensor.element_size() 

1678 

1679 @staticmethod 

1680 def alloc_tensor_buffer(numel: int, dtype, device, pin_memory: bool = False): 

1681 """Allocate an uninitialized 1-D tensor buffer.""" 

1682 if pin_memory: 

1683 return torch.empty(numel, dtype=dtype, device='cpu', pin_memory=True) 

1684 return torch.empty(numel, dtype=dtype, device=device) 

1685 

1686 @staticmethod 

1687 def tensor_to_numpy(tensor) -> np.ndarray: 

1688 """Convert PyTorch tensor to numpy array.""" 

1689 return tensor.cpu().numpy() 

1690 

1691 @staticmethod 

1692 def from_numpy(np_array): 

1693 """Create a host (CPU) PyTorch tensor from a numpy array.""" 

1694 return torch.from_numpy(np_array) 

1695 

1696 @staticmethod 

1697 def clip_grad_norm_( 

1698 parameters, max_norm, norm_type=2.0, 

1699 error_if_nonfinite=False, foreach=None, 

1700 ): 

1701 # pylint: disable=C0415 

1702 from hyper_parallel.platform.torch.clip_grad import ( 

1703 clip_grad_norm_ as _clip_grad_norm, 

1704 ) 

1705 return _clip_grad_norm( 

1706 parameters, max_norm, norm_type, 

1707 error_if_nonfinite=error_if_nonfinite, foreach=foreach, 

1708 ) 

1709 

1710 @staticmethod 

1711 def profiler_record(name): 

1712 """Profiler context manager for recording operations using torch.profiler.""" 

1713 return torch.profiler.record_function(name) 

1714 

1715 def cast_fp_tensor(self, dtype, x): 

1716 """ 

1717 Cast floating-point tensor to target dtype if applicable. 

1718 """ 

1719 if ( 

1720 not isinstance(x, torch.Tensor) 

1721 or not torch.is_floating_point(x) 

1722 or x.dtype == dtype 

1723 ): 

1724 return x 

1725 return x.to(dtype) 

1726 

1727 def apply_to_tensors(self, fn, container): 

1728 """Recursively apply to all tensor in different kinds of container types.""" 

1729 

1730 def apply(x): 

1731 

1732 if isinstance(x, torch.Tensor): 

1733 return fn(x) 

1734 if hasattr(x, "__dataclass_fields__"): 

1735 dc = dataclasses.replace(x) 

1736 changes = { 

1737 f.name: apply(getattr(dc, f.name)) for f in dataclasses.fields(dc) 

1738 } 

1739 return dataclasses.replace(dc, **changes) 

1740 if isinstance(x, OrderedDict): 

1741 od = x.__class__() 

1742 for key, value in x.items(): 

1743 od[key] = apply(value) 

1744 return od 

1745 if isinstance(x, PackedSequence): 

1746 apply(x.data) 

1747 return x 

1748 if isinstance(x, dict): 

1749 return {key: apply(value) for key, value in x.items()} 

1750 if isinstance(x, tuple) and hasattr(x, "_asdict") and hasattr(x, "_fields"): 

1751 res = (apply(el) for el in x) 

1752 return type(x)(*res) 

1753 if isinstance(x, (list, tuple, set)): 

1754 return type(x)(apply(el) for el in x) 

1755 return x 

1756 

1757 return apply(container) 

1758 

1759 

1760 @property 

1761 def meta_device(self): 

1762 return torch.device("meta") 

1763 

1764 def init_on_device(self, device, include_buffers=False): 

1765 return _init_on_device(device, include_buffers=include_buffers) 

1766 

1767 def str_to_dtype(self, dtype_str: str) -> torch.dtype: 

1768 """Map ``torch.<type>`` strings from checkpoint metadata to ``torch.dtype``.""" 

1769 parts = dtype_str.split(".", 1) 

1770 if len(parts) != 2: 

1771 raise ValueError( 

1772 f"Expected dtype string like 'torch.float32', got {dtype_str!r}." 

1773 ) 

1774 prefix, name = parts 

1775 if prefix != "torch": 

1776 raise ValueError( 

1777 f"Expected PyTorch dtype string with prefix 'torch', got {dtype_str!r}." 

1778 ) 

1779 dtype = getattr(torch, name) 

1780 if isinstance(dtype, torch.dtype): 

1781 return dtype 

1782 raise ValueError(f"{dtype_str!r} does not resolve to a torch.dtype.") 

1783 

1784 def list_to_size(self, size_list: list[int]) -> torch.Size: 

1785 return torch.Size(size_list)