Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / core / shard / _op_dispatch.py: 84%

377 statements  

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

16import atexit 

17import glob 

18import importlib 

19import os 

20import sys 

21import warnings 

22from contextvars import ContextVar 

23from itertools import chain 

24from typing import Any, Dict, FrozenSet, List, Optional 

25 

26import yaml 

27 

28from hyper_parallel.core.shard.ops.parallel_ops_register import get_distributed_op 

29from hyper_parallel.core.dtensor.dtensor import DTensor 

30from hyper_parallel.core.dtensor.random import OffsetBasedRNGTracker, is_rng_supported_mesh 

31from hyper_parallel.platform import get_platform 

32from hyper_parallel.platform.platform import PlatformType 

33 

34from hyper_parallel.core.tensor_parallel._ce_op_registry import is_loss_parallel_op, is_decomposed_ce_op 

35from hyper_parallel.core.tensor_parallel.loss_parallel import is_loss_parallel_active 

36from hyper_parallel.core.tensor_parallel.loss_parallel_ops_common import _is_shard_on_last_dim 

37 

38platform = get_platform() 

39Tensor = platform.Tensor 

40 

41 

42def _apply_shard_offset_to_rng_args(args, offset_incr): 

43 """Apply per-shard offset increment to seed/offset tensors in MindSpore random op args. 

44 

45 MindSpore random ops (e.g. ``randn_like_``) receive ``(seed, offset)`` as 

46 explicit int64 scalar tensors from ``default_generator._step()`` in the 

47 Python wrapper *before* the C++ dispatch triggers ``__fallback__``. By the 

48 time ``_dispatch_random_op`` is called, the kernel will use whatever 

49 ``(seed, offset)`` values are in the args—it does **not** read the 

50 generator again. This function finds the offset tensor and adds the 

51 per-rank offset increment so each shard gets a unique random stream. 

52 

53 The (seed, offset) pair is identified as the last two consecutive int64 

54 0-dim tensors in *args* (scanning from the end to skip trailing dtype / 

55 device arguments). 

56 

57 Args: 

58 args: The list of local args for the random op. 

59 offset_incr (int): Per-shard offset increment. 

60 

61 Returns: 

62 list: Modified args with the offset tensor adjusted. 

63 """ 

64 int64_dtype = platform.tensor_dtype.int64 

65 last_int64_idx = -1 

66 for i in range(len(args) - 1, -1, -1): 

67 arg = args[i] 

68 if isinstance(arg, Tensor) and arg.dtype == int64_dtype and arg.ndim == 0: 

69 if last_int64_idx == i + 1: 

70 offset_idx = i + 1 

71 new_args = list(args) 

72 new_offset = int(new_args[offset_idx].item()) + offset_incr 

73 new_args[offset_idx] = platform.tensor([new_offset], dtype=int64_dtype).reshape(()) 

74 return new_args 

75 last_int64_idx = i 

76 return args 

77 

78_dtensor_dispatch_disabled: ContextVar[bool] = ContextVar('_dtensor_dispatch_disabled', default=False) 

79_no_skip_ops: ContextVar[FrozenSet[str]] = ContextVar('_no_skip_ops', default=frozenset()) 

80 

81 

82def get_no_skip_ops() -> FrozenSet[str]: 

83 """Return the set of op names that are exempt from SkipDTensorDispatch.""" 

84 return _no_skip_ops.get() 

85 

86 

87def get_dtensor_dispatch() -> bool: 

88 """ 

89 Get the current DTensor dispatch status. 

90 

91 Returns: 

92 bool: True if DTensor dispatch is enabled, False otherwise. 

93 """ 

94 return not _dtensor_dispatch_disabled.get() 

95 

96 

97class LayoutCacheKey: 

98 """Immutable layout cache key.""" 

99 __slots__ = ('_tuple', '_hash') 

100 

101 def __init__(self, layout_ids: List[str]): 

102 self._tuple = tuple(layout_ids) 

103 self._hash = hash(self._tuple) 

104 

105 @classmethod 

106 def from_cache_values(cls, cache_values: list) -> "LayoutCacheKey": 

107 """Build a LayoutCacheKey from a cache_values list. 

108 

109 Args: 

110 cache_values (list): Mixed list of Layout objects (with compact_str) and raw scalars. 

111 

112 Returns: 

113 LayoutCacheKey: Immutable key derived from the string representation of each value. 

114 """ 

115 key_values = [] 

116 for v in cache_values: 

117 if hasattr(v, 'compact_str'): 

118 key_values.append(str(v.compact_str)) 

119 else: 

120 key_values.append(str(v)) 

121 return cls(key_values) 

122 

123 def __eq__(self, other): 

124 if not isinstance(other, LayoutCacheKey): 

125 return False 

126 return self._tuple == other._tuple 

127 

128 def __hash__(self): 

129 return self._hash 

130 

131 def __repr__(self): 

132 return f"LayoutCacheKey({self._tuple})" 

133 

134 

135class LayoutCacheManager: 

136 """ 

137 Cache layout in infer layout. 

138 

139 A singleton class that manages layout caches for distributed operations. 

140 It caches the inferred layouts and operation implementations to avoid 

141 redundant computation during repeated calls with the same input layouts. 

142 """ 

143 _instance = None 

144 

145 def __init__(self): 

146 self.layout_cache: Dict[str, Dict[LayoutCacheKey, Any]] = {} 

147 atexit.register(self.clear_cache) 

148 

149 @classmethod 

150 def get_instance(cls) -> "LayoutCacheManager": 

151 """ 

152 Get the singleton instance of LayoutCacheManager. 

153 

154 Returns: 

155 LayoutCacheManager: The singleton instance. 

156 """ 

157 if cls._instance is None: 

158 cls._instance = LayoutCacheManager() 

159 return cls._instance 

160 

161 def get_layout_cache(self) -> Dict[str, Dict[LayoutCacheKey, Any]]: 

162 """ 

163 Get the layout cache dictionary. 

164 

165 Returns: 

166 Dict[str, Dict[LayoutCacheKey, Any]]: The nested dictionary mapping 

167 operation names to their layout caches. 

168 """ 

169 return self.layout_cache 

170 

171 @staticmethod 

172 def distributed_op(op_name: str) -> Any: 

173 """ 

174 Get the distributed operation implementation by name. 

175 

176 Args: 

177 op_name (str): The name of the distributed operation. 

178 

179 Returns: 

180 Any: The distributed operation class or implementation. 

181 """ 

182 op = get_distributed_op(op_name) 

183 return op 

184 

185 def clear_cache(self) -> None: 

186 """ 

187 Clear all cached layouts. 

188 

189 This method is automatically registered with atexit to ensure 

190 cache is cleared when the program exits. 

191 """ 

192 self.layout_cache.clear() 

193 

194 

195class OpDispatcher: 

196 """ 

197 OpDispatcher 

198 """ 

199 

200 # Whitelisted ops that mutate args[0]'s storage in place. The dispatch bypass 

201 # must return the original DTensor self for these, not the unwrapped local 

202 # result, or it demotes a DTensor accumulator to a plain Tensor and breaks the 

203 # next op that adds a DTensor to it (e.g. grad-accumulation `loss += micro_loss`). 

204 # Class-level so it stays available on instances built via __new__ (e.g. tests). 

205 _INPLACE_BYPASS_OPS = frozenset( 

206 {"InplaceAddExt", "InplaceSubExt", "InplaceMul", "InplaceDiv"}) 

207 

208 # MindSpore random kernels that always mutate an existing tensor in place. 

209 # Out-of-place random kernels belong in _random_ms_ops only, not here. 

210 _RANDOM_INPLACE_MS_OPS = frozenset({ 

211 "InplaceBernoulliScalar", 

212 "InplaceBernoulliTensor", 

213 "InplaceNormal", 

214 "InplaceRandom", 

215 "InplaceUniform", 

216 }) 

217 

218 def __init__(self): 

219 self._env_yaml_dir: Optional[str] = os.environ.get("HYPER_PARALLEL_OPS_YAML_DIR") 

220 self._env_python_path: Optional[str] = os.environ.get("HYPER_PARALLEL_OPS_PYTHON_PATH") 

221 # The following attributes are initialized in _setup_yaml_dir() 

222 self.work_dir = "" # Initialized in _setup_yaml_dir() 

223 self.yaml_dir = "" # Initialized in _setup_yaml_dir() 

224 

225 self._setup_paths_from_env() 

226 

227 self.layout_infer_ops = self.safe_load_yaml_from_dir() 

228 self.whitelist = ["typeof", "DistCommIsend", 

229 "DistCommIrecv", "DistCommBroadcast", "DistCommAllReduce", "DistCommAllGather", "DistCommBatchIsendIrecv", 

230 "DistCommReduceScatter", "requires_grad_", "item", "__get__", "__set__", "register_hook", 

231 "is_complex", "chunk", "__bool__", "__len__", "__format__", "dim", 

232 "_has_compatible_shallow_copy_type", "is_floating_point", "is_contiguous"] 

233 

234 # Ops requiring args unpacking for layout inference (packed as prim, name, real_args). 

235 self.unpack_ops = ["ScatterUpdate", "Mod", "GatherNd", "StopGradient"] 

236 

237 self._random_ops = { 

238 "normal_", "uniform_", "bernoulli", "bernoulli_", 

239 "native_dropout", "rand", "rand_like", "randn", 

240 "randn_like", "randint_like", "kaiming_uniform_", 

241 "multinomial", 

242 } 

243 # Only mint random op support 

244 # MindSpore use the actual kernel name. 

245 self._random_ms_ops = { 

246 "BernoulliExt", "MultinomialExt", 

247 "InplaceBernoulliScalar", "InplaceBernoulliTensor", 

248 "InplaceNormal", "InplaceRandom", "InplaceUniform", 

249 "NormalFloatFloat", "NormalFloatTensor", "NormalTensorFloat", "NormalTensorTensor", 

250 "RandpermExt", "Randn", "RandLikeExt", "RandnLike", "RandInt", "RandIntLike", "RandExt", 

251 "FuncDropoutExt", "UniformExt", 

252 } 

253 self._rng_tracker: Optional[OffsetBasedRNGTracker] = None 

254 

255 self._register_distributed_ops() 

256 

257 def _setup_paths_from_env(self): 

258 """ 

259 Setup YAML directory and Python path from environment variables. 

260 

261 This method initializes the YAML directory and extends sys.path based on 

262 environment variables HYPER_PARALLEL_OPS_YAML_DIR and HYPER_PARALLEL_OPS_PYTHON_PATH. 

263 """ 

264 self._setup_yaml_dir(self._env_yaml_dir) 

265 self._extend_sys_path(self._env_python_path) 

266 

267 def _setup_yaml_dir(self, env_yaml_dir: Optional[str]): 

268 """ 

269 Feature: Configure yaml_dir/work_dir for OpDispatcher 

270 Description: Resolve the YAML directory used to load distributed op definitions. 

271 If env_yaml_dir is an absolute path, use it directly; otherwise treat it 

272 as a path relative to the project work_dir. If env_yaml_dir is not set, 

273 fall back to the default 'shard/ops/yaml' under work_dir. 

274 Expectation: self.yaml_dir and self.work_dir are set to valid values used later by 

275 safe_load_yaml_from_dir(); no functional behavior is changed. 

276 """ 

277 if env_yaml_dir: 

278 if os.path.isabs(env_yaml_dir): 

279 self.yaml_dir = env_yaml_dir 

280 self.work_dir = "" 

281 else: 

282 self.work_dir = os.path.normpath( 

283 os.path.join(os.path.dirname(os.path.realpath(__file__)), "../") 

284 ) 

285 self.yaml_dir = env_yaml_dir 

286 else: 

287 self.yaml_dir = "shard/ops/yaml" 

288 self.work_dir = os.path.normpath( 

289 os.path.join(os.path.dirname(os.path.realpath(__file__)), "../") 

290 ) 

291 

292 @staticmethod 

293 def _extend_sys_path(env_python_path: Optional[str]): 

294 if not env_python_path: 

295 return 

296 python_paths = env_python_path.split(":") 

297 for path in python_paths: 

298 if path and os.path.isdir(path) and path not in sys.path: 

299 sys.path.append(path) 

300 

301 def _register_distributed_ops(self): 

302 for op_name, config in self.layout_infer_ops.items(): 

303 self._register_single_distributed_op(op_name, config) 

304 

305 def _register_single_distributed_op(self, op_name: str, config: dict): 

306 """ 

307 Feature: Register a single distributed op implementation 

308 Description: Import the distributed op class specified by config and instantiate it 

309 with op_name to trigger registration in the distributed op registry. 

310 Prefer 'distributed_op_module' when provided; otherwise import from 

311 built-in module prefix 'hyper_parallel.core.shard.ops.' plus 

312 'distributed_op_file'. If import fails and an external python path is 

313 provided via env, fall back to importing 'distributed_op_file' directly. 

314 Expectation: The distributed op class is imported and instantiated successfully, 

315 or the original import error is raised; no functional behavior is changed. 

316 """ 

317 class_name = config["distributed_op_class"] 

318 

319 if "distributed_op_module" in config: 

320 module_name = config["distributed_op_module"] 

321 module = importlib.import_module(module_name) 

322 op_class = getattr(module, class_name) 

323 _ = op_class(op_name) 

324 return 

325 

326 module_file = config["distributed_op_file"] 

327 try: 

328 module_name = "hyper_parallel.core.shard.ops." + module_file 

329 module = importlib.import_module(module_name) 

330 op_class = getattr(module, class_name) 

331 _ = op_class(op_name) 

332 except (ModuleNotFoundError, ImportError): 

333 if self._env_python_path: 

334 module = importlib.import_module(module_file) 

335 op_class = getattr(module, class_name) 

336 _ = op_class(op_name) 

337 else: 

338 raise 

339 

340 @staticmethod 

341 def _merge_default(config: dict): 

342 """Apply __default__ values to all ops in this YAML file.""" 

343 if "__default__" not in config: 

344 return config 

345 

346 default_cfg = config["__default__"] 

347 merged = {} 

348 

349 for op_name, op_cfg in config.items(): 

350 if op_name == "__default__": 

351 continue 

352 

353 new_cfg = default_cfg.copy() 

354 new_cfg.update(op_cfg) 

355 merged[op_name] = new_cfg 

356 

357 return merged 

358 

359 def safe_load_yaml_from_dir(self) -> dict: 

360 """ 

361 Load yaml dictionary from directory. 

362 

363 Returns: 

364 dict: Merged dictionary of all operator configurations loaded from YAML files. 

365 """ 

366 yaml_dict = {} 

367 yaml_path = os.path.join(self.work_dir, self.yaml_dir) if self.work_dir else self.yaml_dir 

368 if not os.path.isdir(yaml_path): 

369 raise ValueError(f"Invalid yaml directory path: {yaml_path}") 

370 

371 for yaml_file_path in glob.glob(os.path.join(yaml_path, '*.yaml')): 

372 with open(yaml_file_path, 'r', encoding="utf-8") as f: 

373 yaml_data = yaml.safe_load(f) 

374 

375 yaml_data = OpDispatcher._merge_default(yaml_data) 

376 for name, data in yaml_data.items(): 

377 if name in yaml_dict: 

378 raise ValueError(f"Duplicate yaml object with name '{name}'.") 

379 yaml_dict[name] = data 

380 

381 return yaml_dict 

382 

383 def _dispatch_random_op(self, op_name: str, op_call: callable, args, kwargs): 

384 """Handle dispatch for random ops that operate on DTensors.""" 

385 first_arg = next( 

386 (x for x in chain(args, kwargs.values()) if isinstance(x, DTensor)), 

387 None, 

388 ) 

389 # Fall back to the default op if no DTensor is found. 

390 if first_arg is None: 

391 return op_call(*args, **kwargs) 

392 

393 local_args = [arg.to_local() if isinstance(arg, DTensor) else arg for arg in args] 

394 local_kwargs = {k: v.to_local() if isinstance(v, DTensor) else v for k, v in kwargs.items()} 

395 first_local_arg = first_arg.to_local() 

396 

397 if self._rng_tracker is None and is_rng_supported_mesh(first_arg.device_mesh): 

398 self._rng_tracker = OffsetBasedRNGTracker() 

399 

400 maybe_user_generator = local_kwargs.pop("generator", None) 

401 if ( 

402 self._rng_tracker is not None 

403 and not first_local_arg.is_meta 

404 and self._rng_tracker.distribute_region_enabled 

405 ): 

406 # pylint: disable=W0212 

407 with self._rng_tracker._distribute_region( 

408 device_mesh=first_arg.device_mesh, 

409 placements=first_arg.placements, 

410 global_shape=first_arg.shape, 

411 generator=maybe_user_generator, 

412 ): 

413 # MindSpore random ops (e.g. mint.randn_like) extract (seed, offset) 

414 # from default_generator._step() in the Python wrapper *before* the 

415 # C++ dispatch triggers __fallback__. The callback reuses these 

416 # pre-fetched tensor args, so set_rng_state inside _distribute_region 

417 # has no effect on the kernel. Fix: apply the per-shard offset 

418 # increment directly to the offset tensor in the args. 

419 if platform.platform_type == PlatformType.MINDSPORE: 

420 offset_incr = self._rng_tracker.compute_offset_incr( 

421 first_arg.device_mesh, first_arg.placements, first_arg.shape, 

422 ) 

423 local_args = _apply_shard_offset_to_rng_args(local_args, offset_incr) 

424 local_results = op_call(*local_args, **local_kwargs) 

425 else: 

426 if maybe_user_generator is not None: 

427 local_kwargs["generator"] = maybe_user_generator 

428 local_results = op_call(*local_args, **local_kwargs) 

429 

430 return self._wrap_random_result(op_name, local_results, first_arg, args, kwargs) 

431 

432 @staticmethod 

433 def _func_dropout_ext_inplace(args, kwargs) -> bool: 

434 """Return True when FuncDropoutExt is invoked with inplace=True.""" 

435 # Kernel signature: (input, p, training, inplace, seed, offset). 

436 if len(args) >= 4: 

437 return bool(args[3]) 

438 return bool(kwargs.get("inplace", False)) 

439 

440 @staticmethod 

441 def _random_op_returns_self(op_name: str, args, kwargs) -> bool: 

442 """Return True when a random op mutates an existing DTensor in place.""" 

443 if op_name in OpDispatcher._RANDOM_INPLACE_MS_OPS: 

444 return True 

445 if op_name == "FuncDropoutExt": 

446 return OpDispatcher._func_dropout_ext_inplace(args, kwargs) 

447 # Torch random inplace ops follow the ATen '_' suffix convention. 

448 return op_name.endswith('_') 

449 

450 @staticmethod 

451 def _wrap_random_result(op_name, local_results, first_arg, args, kwargs): 

452 """Wrap a random op's local result(s) back into DTensor(s). 

453 

454 In-place ops return the input DTensor itself. Torch random inplace ops use 

455 the ATen '_' suffix; MindSpore inplace random kernels are listed in 

456 ``_RANDOM_INPLACE_MS_OPS``. ``FuncDropoutExt`` is handled separately 

457 because the same kernel serves both modes via its ``inplace`` argument. 

458 """ 

459 if OpDispatcher._random_op_returns_self(op_name, args, kwargs): 

460 return first_arg 

461 mesh = first_arg.device_mesh 

462 placements = first_arg.layout.alias_placements 

463 # Some ops return tuple/list, e.g. native_dropout returns (output, mask). 

464 if isinstance(local_results, (tuple, list)): 

465 return tuple( 

466 DTensor.from_local(r, mesh, placements) if isinstance(r, Tensor) else r 

467 for r in local_results 

468 ) 

469 if isinstance(local_results, Tensor): 

470 return DTensor.from_local(local_results, mesh, placements) 

471 # Fallback: return as-is for non-Tensor results (currently unreachable with existing _random_ops). 

472 return local_results 

473 

474 @staticmethod 

475 def _unwrap_value(value: object) -> object: 

476 """Replace DTensor with its local tensor; pass scalars and plain tensors through. 

477 

478 Args: 

479 value (object): A single argument value from an op call. 

480 

481 Returns: 

482 object: The local tensor if value is a DTensor, otherwise value unchanged. 

483 """ 

484 if isinstance(value, DTensor): 

485 return value.to_local() 

486 if isinstance(value, tuple): 

487 return tuple(OpDispatcher._unwrap_value(e) for e in value) 

488 if isinstance(value, list): 

489 return [OpDispatcher._unwrap_value(e) for e in value] 

490 return value 

491 

492 @staticmethod 

493 def _unwrap_args(args: tuple) -> list: 

494 """Strip DTensor wrappers from args, preserving tuple/list container structure. 

495 

496 Args: 

497 args: Op call positional arguments, may contain DTensor instances. 

498 

499 Returns: 

500 List of args with DTensor replaced by their local tensors. 

501 """ 

502 return [OpDispatcher._unwrap_value(arg) for arg in args] 

503 

504 @staticmethod 

505 def _unwrap_kwargs(kwargs: dict) -> dict: 

506 """Strip DTensor wrappers from kwargs values, preserving tuple/list container structure. 

507 

508 Args: 

509 kwargs: Op call keyword arguments, values may contain DTensor instances. 

510 

511 Returns: 

512 Dict of kwargs with DTensor values replaced by their local tensors. 

513 """ 

514 return {k: OpDispatcher._unwrap_value(v) for k, v in kwargs.items()} 

515 

516 @staticmethod 

517 def _gather_dtensors_to_full(args: tuple, kwargs: dict) -> tuple: 

518 """Gather all DTensor arguments to full tensors for fallback execution. 

519 

520 Used when an operator has no parallel layout implementation. All DTensor 

521 arguments are gathered to full tensors before calling the standard operator. 

522 

523 Args: 

524 args: Op call positional arguments, may contain DTensor instances. 

525 kwargs: Op call keyword arguments, may contain DTensor instances. 

526 

527 Returns: 

528 Tuple of (unwrapped_args, unwrapped_kwargs) with DTensor values 

529 replaced by their full tensor representations. 

530 

531 Warning: 

532 This fallback performs all-gather which may consume significant memory. 

533 Operators without layout implementations should be registered properly. 

534 """ 

535 def gather(value: object) -> object: 

536 if isinstance(value, DTensor): 

537 return value.full_tensor() 

538 if isinstance(value, tuple): 

539 return tuple(gather(e) for e in value) 

540 if isinstance(value, list): 

541 return [gather(e) for e in value] 

542 return value 

543 

544 gathered_args = [gather(arg) for arg in args] 

545 gathered_kwargs = {k: gather(v) for k, v in kwargs.items()} 

546 

547 warnings.warn( 

548 "Operator has no distributed layout implementation. " 

549 "Falling back to all-gather which may consume significant memory. " 

550 "Consider registering a proper distributed operator.", 

551 UserWarning, 

552 stacklevel=4 

553 ) 

554 

555 return gathered_args, gathered_kwargs 

556 

557 def _should_bypass_dispatch(self, op_name: str) -> bool: 

558 """Return True if the op should bypass DTensor dispatch and run locally. 

559 

560 Args: 

561 op_name: Canonical operator name from platform.get_op_name(). 

562 

563 Returns: 

564 True when the op is whitelisted or DTensor dispatch is globally disabled. 

565 """ 

566 skip_dispatch = get_dtensor_dispatch() is False and op_name not in get_no_skip_ops() 

567 return op_name in self.whitelist or op_name in self._INPLACE_BYPASS_OPS or skip_dispatch 

568 

569 @staticmethod 

570 def _validate_inplace_partial_inputs(op_name: str, args: tuple, kwargs: dict) -> None: 

571 """Reject local in-place add/sub when Partial contributions need gating.""" 

572 if op_name not in {"InplaceAddExt", "InplaceSubExt"} or not args: 

573 return 

574 first = args[0] 

575 if len(args) >= 2: 

576 second = args[1] 

577 elif "other" in kwargs: 

578 second = kwargs["other"] 

579 else: 

580 return 

581 if not isinstance(first, DTensor): 

582 return 

583 mesh_ndim = len(first.layout.partial) 

584 first_partial = tuple(first.layout.partial) 

585 if isinstance(second, DTensor): 

586 second_partial = tuple(second.layout.partial) 

587 if len(second_partial) != mesh_ndim: 

588 raise ValueError( 

589 f"For {op_name}, in-place input mesh dimensions must match, " 

590 f"but got {mesh_ndim} and {len(second_partial)}." 

591 ) 

592 else: 

593 second_partial = (None,) * mesh_ndim 

594 if first_partial != second_partial: 

595 raise ValueError( 

596 f"For {op_name}, input Partial placements must be identical for " 

597 f"local in-place execution, but got {first_partial} and {second_partial}." 

598 ) 

599 

600 def _should_dispatch_loss_parallel(self, op_name: str) -> bool: 

601 """Check if should dispatch through loss_parallel path. 

602 

603 Args: 

604 op_name: Canonical operator name from platform.get_op_name(). 

605 

606 Returns: 

607 True when in loss_parallel context and op is a CE entry point. 

608 """ 

609 return is_loss_parallel_active() and is_loss_parallel_op(op_name) 

610 

611 def _check_decomposed_ce_op_in_loss_parallel(self, op_name: str, args: tuple, kwargs: dict): 

612 """Check if decomposed CE ops are called in loss_parallel context. 

613 

614 Args: 

615 op_name: Canonical operator name. 

616 args: Positional arguments for op_call. 

617 kwargs: Keyword arguments for op_call. 

618 

619 Raises: 

620 ValueError: If decomposed CE op is called in loss_parallel context 

621 with vocab-sharded DTensor input. 

622 """ 

623 if not is_loss_parallel_active() or not is_decomposed_ce_op(op_name): 

624 return 

625 

626 has_vocab_sharded_dtensor = False 

627 for arg in args: 

628 if isinstance(arg, DTensor) and _is_shard_on_last_dim(arg): 

629 has_vocab_sharded_dtensor = True 

630 break 

631 if not has_vocab_sharded_dtensor: 

632 for val in kwargs.values(): 

633 if isinstance(val, DTensor) and _is_shard_on_last_dim(val): 

634 has_vocab_sharded_dtensor = True 

635 break 

636 

637 if has_vocab_sharded_dtensor: 

638 raise ValueError( 

639 f"Operator '{op_name}' is a decomposed component of cross_entropy and should not be called " 

640 f"directly within loss_parallel() context. Use F.cross_entropy(logits, targets) instead. " 

641 f"For example, replace:\n" 

642 f" with loss_parallel():\n" 

643 f" log_probs = F.log_softmax(logits, dim=-1)\n" 

644 f" loss = F.nll_loss(log_probs, targets)\n" 

645 f"with:\n" 

646 f" with loss_parallel():\n" 

647 f" loss = F.cross_entropy(logits, targets)" 

648 ) 

649 

650 def _dispatch_loss_parallel(self, op_call: callable, args: tuple, kwargs: dict): 

651 """Dispatch cross_entropy through the loss_parallel distributed kernel. 

652 

653 Args: 

654 op_call: The raw operator callable. 

655 args: Positional arguments for op_call. 

656 kwargs: Keyword arguments for op_call. 

657 

658 Returns: 

659 Result of the distributed cross_entropy computation. 

660 """ 

661 if platform.platform_type == PlatformType.PYTORCH: 

662 # pylint: disable=C0415 

663 from hyper_parallel.platform.torch.loss_parallel_ops import distributed_cross_entropy_from_op_call 

664 elif platform.platform_type == PlatformType.MINDSPORE: 

665 # pylint: disable=C0415 

666 from hyper_parallel.platform.mindspore.loss_parallel_ops import distributed_cross_entropy_from_op_call 

667 else: 

668 raise RuntimeError(f"Unsupported platform for loss_parallel: {platform.platform_type}") 

669 return distributed_cross_entropy_from_op_call(op_call, args, kwargs) 

670 

671 def _check_ce_op_without_loss_parallel_context(self, op_name: str, args: tuple): 

672 """Check if CE op is called with Shard(-1) DTensor outside loss_parallel context. 

673 

674 Args: 

675 op_name: Canonical operator name. 

676 args: Positional arguments for op_call. 

677 

678 Raises: 

679 ValueError: If CE op is called with Shard(-1) logits outside loss_parallel context. 

680 """ 

681 if is_loss_parallel_active() or not is_loss_parallel_op(op_name): 

682 return 

683 

684 if len(args) == 0 or not isinstance(args[0], DTensor): 

685 return 

686 

687 logits = args[0] 

688 if _is_shard_on_last_dim(logits): 

689 raise ValueError( 

690 f"Operator '{op_name}' requires loss_parallel context when input logits are " 

691 f"sharded on the vocabulary dimension (Shard(-1)). Please wrap your forward " 

692 f"and backward pass with loss_parallel():\n" 

693 f" with loss_parallel():\n" 

694 f" loss = F.cross_entropy(logits, targets)\n" 

695 f" loss.backward()\n" 

696 f"If you intentionally want to gather all shards to compute cross_entropy " 

697 f"(not recommended for large vocabulary), use logits.full_tensor() explicitly." 

698 ) 

699 

700 @staticmethod 

701 def _normalize_aclop_args(op_name: str, unpack_ops: list, args: tuple) -> tuple: 

702 """ 

703 Normalize aclop-packed arguments for MindSpore backend operators. 

704 

705 NOTE: This handles MindSpore aclop operators whose kernel signature packs 

706 arguments as ``(prim, op_name_str, (real_arg0, real_arg1, ...))``. The 

707 ``prim`` and ``op_name_str`` are preserved as ``packed_call`` for the 

708 final kernel invocation, while the real tensor arguments are extracted 

709 for layout inference and preprocessing. 

710 

711 **aclop is planned for deprecation.** Once aclop is fully removed, this 

712 normalization and the associated ``unpack_ops`` list can be deleted. 

713 

714 Args: 

715 op_name (str): Canonical operator name. 

716 unpack_ops (list): List of op names that may use aclop packed format. 

717 args (tuple): Raw positional arguments from the op call. 

718 

719 Returns: 

720 tuple: ``(packed_call, normalized_args)`` 

721 - **packed_call**: ``(prim, op_name_str)`` tuple for kernel 

722 invocation, or ``None`` if no unpacking was performed. 

723 - **normalized_args**: The real tensor arguments (unpacked if 

724 the packed format was detected, otherwise the original args). 

725 """ 

726 if OpDispatcher._is_aclop_packed(op_name, unpack_ops, args): 

727 return (args[0], args[1]), tuple(args[2]) 

728 return None, args 

729 

730 @staticmethod 

731 def _is_aclop_packed(op_name: str, unpack_ops: list, args: tuple) -> bool: 

732 """Check if arguments use aclop packed format.""" 

733 return ( 

734 op_name in unpack_ops 

735 and len(args) == 3 

736 and isinstance(args[1], str) 

737 and isinstance(args[2], (tuple, list)) 

738 ) 

739 

740 @staticmethod 

741 def _call_op_impl(op_impl: callable, packed_call, args, kwargs: dict): 

742 """Invoke *op_impl* with optional aclop packed-call wrapping. 

743 

744 When *packed_call* is not ``None`` the MindSpore aclop kernel expects 

745 ``(prim, op_name, (arg0, arg1, ...))``. Otherwise *args* are spread 

746 as positional arguments in the usual way. 

747 

748 Args: 

749 op_impl: The op implementation callable. 

750 packed_call: ``(prim, op_name)`` tuple or ``None``. 

751 args: Local tensor arguments (list or tuple). 

752 kwargs: Keyword arguments dict. 

753 

754 Returns: 

755 Result of the *op_impl* invocation. 

756 """ 

757 if packed_call is not None: 

758 return op_impl(packed_call[0], packed_call[1], tuple(args), **kwargs) 

759 return op_impl(*args, **kwargs) 

760 

761 def _dispatch_layout_infer( 

762 self, op_name: str, op_call: callable, args: tuple, kwargs: dict 

763 ): 

764 """Dispatch an op through the layout-inference path. 

765 

766 Args: 

767 op_name: Canonical operator name. 

768 op_call: The raw operator callable. 

769 args: Positional arguments for op_call. 

770 kwargs: Keyword arguments for op_call. 

771 

772 Returns: 

773 Result of the layout-infer dispatch. 

774 

775 Raises: 

776 RuntimeError: If op_name is not registered, or preprocess returns None 

777 (operator not migrated to three-phase dispatch). 

778 """ 

779 if op_name not in self.layout_infer_ops: 

780 has_dtensor = any(isinstance(arg, DTensor) for arg in args) 

781 has_dtensor = has_dtensor or any(isinstance(v, DTensor) for v in kwargs.values()) 

782 if has_dtensor: 

783 self._check_ce_op_without_loss_parallel_context(op_name, args) 

784 

785 if not is_loss_parallel_op(op_name): 

786 raise RuntimeError( 

787 f"Operator {op_name} does not contain parallel layout infer func. " 

788 f"DTensor dispatch requires explicit layout inference registration. " 

789 f"Please register a distributed operator for '{op_name}' or use local tensors." 

790 ) 

791 

792 gathered_args, gathered_kwargs = self._gather_dtensors_to_full(args, kwargs) 

793 

794 # Special handling for cross_entropy with 3D logits (only when NOT in loss_parallel context) 

795 # PyTorch expects: logits [N, C], targets [N] 

796 # But LLM forward returns: logits [batch, seq, vocab], targets [batch, seq] 

797 # Note: nll_loss input is log_probs, typically already 2D, so we only reshape for cross_entropy 

798 if op_name == "cross_entropy" and len(gathered_args) >= 2: 

799 logits = gathered_args[0] 

800 targets = gathered_args[1] 

801 if isinstance(logits, Tensor) and isinstance(targets, Tensor): 

802 if logits.ndim > 2 and targets.ndim > 1 and targets.ndim == logits.ndim - 1: 

803 vocab_size = logits.shape[-1] 

804 gathered_args[0] = logits.reshape(-1, vocab_size) 

805 gathered_args[1] = targets.reshape(-1) 

806 

807 return op_call(*gathered_args, **gathered_kwargs) 

808 raise RuntimeError(f"Operator {op_name} does not contain parallel layout infer func.") 

809 

810 cache_manager = LayoutCacheManager.get_instance() 

811 distribute_op = cache_manager.distributed_op(op_name) 

812 

813 # Normalize aclop-packed args before any per-op processing. 

814 # This allows all distributed op classes (ElementWiseDistributedOp, 

815 # GatherNdDistributedOp, etc.) to receive clean unpacked args, 

816 # avoiding duplicated unpack logic in each class's preprocess. 

817 packed_call, args = self._normalize_aclop_args(op_name, getattr(self, 'unpack_ops', []), args) 

818 

819 result = distribute_op.preprocess(args, kwargs) 

820 if result is None: 

821 raise RuntimeError( 

822 f"Operator '{op_name}' has not been migrated to the three-phase dispatch flow. " 

823 f"Please implement preprocess() to return (local_args, local_kwargs, cache_values)." 

824 ) 

825 output = self._dispatch_new(op_call, distribute_op, packed_call, result) 

826 return self._restore_inplace_dtensor_result(op_name, args, output) 

827 

828 @staticmethod 

829 def _restore_inplace_dtensor_result(op_name: str, args: tuple, output: Any) -> Any: 

830 """Return the original DTensor wrapper after a local in-place operation.""" 

831 if op_name in {"add_", "sub_"} and args and isinstance(args[0], DTensor): 

832 return args[0] 

833 return output 

834 

835 @staticmethod 

836 def _dispatch_new(func, distribute_op, packed_call, result) -> Tensor: 

837 """New dispatch flow using preprocess result. 

838 

839 Args: 

840 func: Original function. 

841 distribute_op: Distributed operation instance. 

842 packed_call: (prim, op_name_str) tuple for aclop kernel invocation, 

843 or None for regular ops. 

844 result: Preprocessed result (local_args, local_kwargs, cache_values). 

845 

846 Returns: 

847 Tensor: Dispatched result as DTensor. 

848 """ 

849 local_args, local_kwargs, cache_values = result 

850 cache_key = LayoutCacheKey.from_cache_values(cache_values) 

851 func_name = platform.get_op_name(func) 

852 

853 infer_result, op_impl = OpDispatcher._lookup_or_infer_layout_new( 

854 func, func_name, cache_key, cache_values, distribute_op 

855 ) 

856 

857 op_impl = func if op_impl is None else op_impl 

858 py_output = OpDispatcher._call_op_impl(op_impl, packed_call, local_args, local_kwargs) 

859 return distribute_op.wrap_output(py_output, infer_result[0]) 

860 

861 @staticmethod 

862 def _lookup_or_infer_layout_new(func, func_name, cache_key, cache_values, distribute_op): 

863 """Look up cached layout or compute via distributed op (new three-phase API). 

864 

865 Returns: 

866 (infer_result, op_impl) 

867 """ 

868 cache_manager = LayoutCacheManager.get_instance() 

869 layout_cache = cache_manager.get_layout_cache() 

870 if func_name not in layout_cache: 

871 layout_cache[func_name] = {} 

872 op_layout_cache = layout_cache[func_name] 

873 if cache_key in op_layout_cache: 

874 return op_layout_cache[cache_key] 

875 infer_result = distribute_op.infer_layout(cache_values) 

876 op_impl = distribute_op.get_expand_impl(func, infer_result, cache_values) 

877 op_layout_cache[cache_key] = (infer_result, op_impl) 

878 return infer_result, op_impl 

879 

880 def dispatch(self, op_call: callable, args: tuple, kwargs: dict) -> object: 

881 """Route an op call through the appropriate DTensor dispatch path. 

882 

883 Args: 

884 op_call: The raw operator callable. 

885 args: Positional arguments for op_call. 

886 kwargs: Keyword arguments for op_call. 

887 

888 Returns: 

889 Result of the dispatched op call. 

890 """ 

891 op_name = platform.get_op_name(op_call) 

892 

893 if self._should_bypass_dispatch(op_name): 

894 self._validate_inplace_partial_inputs(op_name, args, kwargs) 

895 result = op_call(*self._unwrap_args(args), **self._unwrap_kwargs(kwargs)) 

896 if op_name in self._INPLACE_BYPASS_OPS and args and isinstance(args[0], DTensor): 

897 return args[0] 

898 return result 

899 

900 if op_name in self._random_ops or op_name in self._random_ms_ops: 

901 return self._dispatch_random_op(op_name, op_call, args, kwargs) 

902 

903 self._check_decomposed_ce_op_in_loss_parallel(op_name, args, kwargs) 

904 

905 if self._should_dispatch_loss_parallel(op_name): 

906 return self._dispatch_loss_parallel(op_call, args, kwargs) 

907 

908 if op_name not in self.layout_infer_ops and get_distributed_op(op_name) is not None: 

909 self.layout_infer_ops[op_name] = {} 

910 

911 return self._dispatch_layout_infer(op_name, op_call, args, kwargs) 

912 

913_OP_DISPATCHER = OpDispatcher()