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

429 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"""_op_dispatch""" 

16import atexit 

17import copy 

18import glob 

19import importlib 

20import logging 

21import os 

22import sys 

23import warnings 

24from contextvars import ContextVar 

25from itertools import chain 

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

27 

28import yaml 

29 

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

31from hyper_parallel.core.dtensor.dtensor import DTensor 

32from hyper_parallel.core.dtensor.layout import RaggedShardInfo 

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

34from hyper_parallel.core.dtensor.debug._dispatch_logger import log_dispatch_enter, log_dispatch_exit 

35from hyper_parallel.platform import get_platform 

36from hyper_parallel.platform.platform import PlatformType 

37 

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

39from hyper_parallel.core.tensor_parallel.loss_parallel import is_loss_parallel_active 

40from hyper_parallel.core.tensor_parallel.loss_parallel_ops_common import _is_shard_on_last_dim 

41 

42platform = get_platform() 

43Tensor = platform.Tensor 

44 

45logger = logging.getLogger(__name__) 

46 

47 

48def _apply_shard_offset_to_rng_args(args, offset_incr): 

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

50 

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

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

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

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

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

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

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

58 

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

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

61 device arguments). 

62 

63 Args: 

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

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

66 

67 Returns: 

68 list: Modified args with the offset tensor adjusted. 

69 """ 

70 int64_dtype = platform.tensor_dtype.int64 

71 last_int64_idx = -1 

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

73 arg = args[i] 

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

75 if last_int64_idx == i + 1: 

76 offset_idx = i + 1 

77 new_args = list(args) 

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

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

80 return new_args 

81 last_int64_idx = i 

82 return args 

83 

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

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

86_debug_mode_observer: ContextVar = ContextVar('_debug_mode_observer', default=None) 

87 

88_RAGGED_ELEMENTWISE_OPS = { 

89 "abs": "unary", "absolute": "unary", "clone": "unary", "cos": "unary", 

90 "conj": "unary", "empty_like": "unary", "exp": "unary", "gelu": "unary", 

91 "isinf": "unary", "isnan": "unary", 

92 "log": "unary", "neg": "unary", "negative": "unary", "relu": "unary", 

93 "rsqrt": "unary", "sigmoid": "unary", "silu": "unary", "sin": "unary", 

94 "sqrt": "unary", "square": "unary", "zeros_like": "unary", 

95 "add": "binary", "add_": "binary", "addcdiv_": "binary", 

96 "addcmul_": "binary", "div": "binary", "lerp_": "binary", 

97 "mul": "binary", "mul_": "binary", "pow": "binary", "real_div": "binary", 

98 "sub": "binary", "__rsub__": "binary", "__rpow__": "binary", 

99 "true_divide": "binary", 

100} 

101 

102_RAGGED_INPLACE_ELEMENTWISE_OPS = frozenset({ 

103 "add_", 

104 "addcdiv_", 

105 "addcmul_", 

106 "lerp_", 

107 "mul_", 

108}) 

109 

110# Tensor subclass bookkeeping must stay available when a Ragged DTensor is 

111# wrapped as a Parameter. These operations only inspect or update the local 

112# autograd wrapper and do not reinterpret the logical distributed shape. 

113_RAGGED_METADATA_BYPASS_OPS = frozenset({ 

114 "requires_grad_", 

115 "__get__", 

116 "__set__", 

117 "register_hook", 

118 "_has_compatible_shallow_copy_type", 

119 "is_complex", 

120 "is_floating_point", 

121 "is_contiguous", 

122}) 

123 

124 

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

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

127 return _no_skip_ops.get() 

128 

129 

130def get_dtensor_dispatch() -> bool: 

131 """ 

132 Get the current DTensor dispatch status. 

133 

134 Returns: 

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

136 """ 

137 return not _dtensor_dispatch_disabled.get() 

138 

139 

140class LayoutCacheKey: 

141 """Immutable layout cache key.""" 

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

143 

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

145 self._tuple = tuple(layout_ids) 

146 self._hash = hash(self._tuple) 

147 

148 @classmethod 

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

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

151 

152 Args: 

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

154 

155 Returns: 

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

157 """ 

158 # Read the cached ``_compact_str`` attribute directly instead of going through 

159 # the ``compact_str`` property getter (one fewer Python frame per Layout), and 

160 # build the key in one comprehension. The resulting tuple is byte-for-byte the 

161 # legacy string key, so eq/hash semantics are unchanged. 

162 # NOTE: the key stays a string tuple by design (cross-checked against manually 

163 # built legacy keys in the UTs); CPython caches each compact_str's hash on the 

164 # string object, so this is not the bottleneck a full integer key would target. 

165 return cls([cs if (cs := getattr(v, '_compact_str', None)) is not None else str(v) 

166 for v in cache_values]) 

167 

168 def __eq__(self, other): 

169 if not isinstance(other, LayoutCacheKey): 

170 return False 

171 return self._tuple == other._tuple 

172 

173 def __hash__(self): 

174 return self._hash 

175 

176 def __repr__(self): 

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

178 

179 

180class LayoutCacheManager: 

181 """ 

182 Cache layout in infer layout. 

183 

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

185 It caches the inferred layouts and operation implementations to avoid 

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

187 """ 

188 _instance = None 

189 

190 def __init__(self): 

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

192 atexit.register(self.clear_cache) 

193 

194 @classmethod 

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

196 """ 

197 Get the singleton instance of LayoutCacheManager. 

198 

199 Returns: 

200 LayoutCacheManager: The singleton instance. 

201 """ 

202 if cls._instance is None: 

203 cls._instance = LayoutCacheManager() 

204 return cls._instance 

205 

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

207 """ 

208 Get the layout cache dictionary. 

209 

210 Returns: 

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

212 operation names to their layout caches. 

213 """ 

214 return self.layout_cache 

215 

216 @staticmethod 

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

218 """ 

219 Get the distributed operation implementation by name. 

220 

221 Args: 

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

223 

224 Returns: 

225 Any: The distributed operation class or implementation. 

226 """ 

227 op = get_distributed_op(op_name) 

228 return op 

229 

230 def clear_cache(self) -> None: 

231 """ 

232 Clear all cached layouts. 

233 

234 This method is automatically registered with atexit to ensure 

235 cache is cleared when the program exits. 

236 """ 

237 self.layout_cache.clear() 

238 

239 

240class OpDispatcher: 

241 """ 

242 OpDispatcher 

243 """ 

244 

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

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

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

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

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

250 _INPLACE_BYPASS_OPS = frozenset( 

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

252 

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

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

255 _RANDOM_INPLACE_MS_OPS = frozenset({ 

256 "InplaceBernoulliScalar", 

257 "InplaceBernoulliTensor", 

258 "InplaceNormal", 

259 "InplaceRandom", 

260 "InplaceUniform", 

261 }) 

262 

263 def __init__(self): 

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

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

266 # The following attributes are initialized in _setup_yaml_dir() 

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

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

269 

270 self._setup_paths_from_env() 

271 

272 self.layout_infer_ops = self.safe_load_yaml_from_dir() 

273 # frozenset for O(1) membership (checked on every dispatch's bypass test). 

274 self.whitelist = frozenset({"typeof", "DistCommIsend", 

275 "DistCommIrecv", "DistCommBroadcast", "DistCommAllReduce", "DistCommAllGather", 

276 "DistCommBatchIsendIrecv", 

277 "DistCommReduceScatter", "requires_grad_", "item", "__get__", "__set__", 

278 "register_hook", 

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

280 "_has_compatible_shallow_copy_type", "is_floating_point", "is_contiguous"}) 

281 

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

283 # frozenset so the aclop-normalization gate in _dispatch_layout_infer is O(1). 

284 self.unpack_ops = frozenset({"ScatterUpdate", "Mod", "GatherNd", "StopGradient"}) 

285 

286 self._random_ops = { 

287 "normal_", "uniform_", "bernoulli", "bernoulli_", 

288 "native_dropout", "rand", "rand_like", "randn", 

289 "randn_like", "randint_like", "kaiming_uniform_", 

290 "multinomial", 

291 } 

292 # Only mint random op support 

293 # MindSpore use the actual kernel name. 

294 self._random_ms_ops = { 

295 "BernoulliExt", "MultinomialExt", 

296 "InplaceBernoulliScalar", "InplaceBernoulliTensor", 

297 "InplaceNormal", "InplaceRandom", "InplaceUniform", 

298 "NormalFloatFloat", "NormalFloatTensor", "NormalTensorFloat", "NormalTensorTensor", 

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

300 "FuncDropoutExt", "UniformExt", 

301 } 

302 self._rng_tracker: Optional[OffsetBasedRNGTracker] = None 

303 # Op names proven to be loss/CE-irrelevant (both is_loss_parallel_op and 

304 # is_decomposed_ce_op are False). For these the loss_parallel / decomposed-CE 

305 # guards in dispatch() are always no-ops regardless of context, so we skip 

306 # them (and their is_loss_parallel_active() contextvar reads) on later calls. 

307 self._non_loss_ops: set = set() 

308 

309 self._register_distributed_ops() 

310 

311 def _setup_paths_from_env(self): 

312 """ 

313 Setup YAML directory and Python path from environment variables. 

314 

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

316 environment variables HYPER_PARALLEL_OPS_YAML_DIR and HYPER_PARALLEL_OPS_PYTHON_PATH. 

317 """ 

318 self._setup_yaml_dir(self._env_yaml_dir) 

319 self._extend_sys_path(self._env_python_path) 

320 

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

322 """ 

323 Feature: Configure yaml_dir/work_dir for OpDispatcher 

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

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

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

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

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

329 safe_load_yaml_from_dir(); no functional behavior is changed. 

330 """ 

331 if env_yaml_dir: 

332 if os.path.isabs(env_yaml_dir): 

333 self.yaml_dir = env_yaml_dir 

334 self.work_dir = "" 

335 else: 

336 self.work_dir = os.path.normpath( 

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

338 ) 

339 self.yaml_dir = env_yaml_dir 

340 else: 

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

342 self.work_dir = os.path.normpath( 

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

344 ) 

345 

346 @staticmethod 

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

348 if not env_python_path: 

349 return 

350 python_paths = env_python_path.split(":") 

351 for path in python_paths: 

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

353 sys.path.append(path) 

354 

355 def _register_distributed_ops(self): 

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

357 self._register_single_distributed_op(op_name, config) 

358 

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

360 """ 

361 Feature: Register a single distributed op implementation 

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

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

364 Prefer 'distributed_op_module' when provided; otherwise import from 

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

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

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

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

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

370 """ 

371 class_name = config["distributed_op_class"] 

372 

373 if "distributed_op_module" in config: 

374 module_name = config["distributed_op_module"] 

375 module = importlib.import_module(module_name) 

376 op_class = getattr(module, class_name) 

377 _ = op_class(op_name) 

378 return 

379 

380 module_file = config["distributed_op_file"] 

381 try: 

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

383 module = importlib.import_module(module_name) 

384 op_class = getattr(module, class_name) 

385 _ = op_class(op_name) 

386 except (ModuleNotFoundError, ImportError): 

387 if self._env_python_path: 

388 module = importlib.import_module(module_file) 

389 op_class = getattr(module, class_name) 

390 _ = op_class(op_name) 

391 else: 

392 raise 

393 

394 @staticmethod 

395 def _merge_default(config: dict): 

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

397 if "__default__" not in config: 

398 return config 

399 

400 default_cfg = config["__default__"] 

401 merged = {} 

402 

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

404 if op_name == "__default__": 

405 continue 

406 

407 new_cfg = default_cfg.copy() 

408 new_cfg.update(op_cfg) 

409 merged[op_name] = new_cfg 

410 

411 return merged 

412 

413 def safe_load_yaml_from_dir(self) -> dict: 

414 """ 

415 Load yaml dictionary from directory. 

416 

417 Returns: 

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

419 """ 

420 yaml_dict = {} 

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

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

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

424 

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

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

427 yaml_data = yaml.safe_load(f) 

428 

429 yaml_data = OpDispatcher._merge_default(yaml_data) 

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

431 if name in yaml_dict: 

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

433 yaml_dict[name] = data 

434 

435 return yaml_dict 

436 

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

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

439 first_arg = next( 

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

441 None, 

442 ) 

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

444 if first_arg is None: 

445 return op_call(*args, **kwargs) 

446 

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

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

449 first_local_arg = first_arg.to_local() 

450 

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

452 self._rng_tracker = OffsetBasedRNGTracker() 

453 

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

455 if ( 

456 self._rng_tracker is not None 

457 and not first_local_arg.is_meta 

458 and self._rng_tracker.distribute_region_enabled 

459 ): 

460 # pylint: disable=W0212 

461 with self._rng_tracker._distribute_region( 

462 device_mesh=first_arg.device_mesh, 

463 placements=first_arg.placements, 

464 global_shape=first_arg.shape, 

465 generator=maybe_user_generator, 

466 ): 

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

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

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

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

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

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

473 if platform.platform_type == PlatformType.MINDSPORE: 

474 offset_incr = self._rng_tracker.compute_offset_incr( 

475 first_arg.device_mesh, first_arg.placements, first_arg.shape, 

476 ) 

477 local_args = _apply_shard_offset_to_rng_args(local_args, offset_incr) 

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

479 else: 

480 if maybe_user_generator is not None: 

481 local_kwargs["generator"] = maybe_user_generator 

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

483 

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

485 

486 @staticmethod 

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

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

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

490 if len(args) >= 4: 

491 return bool(args[3]) 

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

493 

494 @staticmethod 

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

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

497 if op_name in OpDispatcher._RANDOM_INPLACE_MS_OPS: 

498 return True 

499 if op_name == "FuncDropoutExt": 

500 return OpDispatcher._func_dropout_ext_inplace(args, kwargs) 

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

502 return op_name.endswith('_') 

503 

504 @staticmethod 

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

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

507 

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

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

510 ``_RANDOM_INPLACE_MS_OPS``. ``FuncDropoutExt`` is handled separately 

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

512 """ 

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

514 return first_arg 

515 mesh = first_arg.device_mesh 

516 placements = first_arg.layout.alias_placements 

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

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

519 return tuple( 

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

521 for r in local_results 

522 ) 

523 if isinstance(local_results, Tensor): 

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

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

526 return local_results 

527 

528 @staticmethod 

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

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

531 

532 Args: 

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

534 

535 Returns: 

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

537 """ 

538 if isinstance(value, DTensor): 

539 return value.to_local() 

540 if isinstance(value, tuple): 

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

542 if isinstance(value, list): 

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

544 return value 

545 

546 @staticmethod 

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

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

549 

550 Args: 

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

552 

553 Returns: 

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

555 """ 

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

557 

558 @staticmethod 

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

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

561 

562 Args: 

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

564 

565 Returns: 

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

567 """ 

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

569 

570 @staticmethod 

571 def _collect_dtensors(value: object) -> List[DTensor]: 

572 """Return all DTensors nested in one dispatch argument.""" 

573 if isinstance(value, DTensor): 

574 return [value] 

575 if isinstance(value, (tuple, list)): 

576 return list(chain.from_iterable( 

577 OpDispatcher._collect_dtensors(item) for item in value 

578 )) 

579 if isinstance(value, dict): 

580 return list(chain.from_iterable( 

581 OpDispatcher._collect_dtensors(item) for item in value.values() 

582 )) 

583 return [] 

584 

585 def _validate_ragged_dispatch( 

586 self, op_name: str, args: tuple, kwargs: dict 

587 ) -> Optional[DTensor]: 

588 """Return the first Ragged input for a whitelisted elementwise op.""" 

589 reference = next( 

590 ( 

591 dtensor for dtensor in self._collect_dtensors((args, kwargs)) 

592 if isinstance(getattr(dtensor.layout, "ragged_shard", None), RaggedShardInfo) 

593 ), 

594 None, 

595 ) 

596 if reference is None: 

597 return None 

598 if op_name not in _RAGGED_ELEMENTWISE_OPS: 

599 raise RuntimeError( 

600 f"Operator {op_name!r} does not support RaggedShard in phase one" 

601 ) 

602 return reference 

603 

604 def _dispatch_ragged_elementwise( 

605 self, op_call: callable, args: tuple, kwargs: dict, 

606 reference: DTensor, 

607 ) -> DTensor: 

608 """Execute a whitelisted op locally and inherit its Ragged Layout.""" 

609 local_args = tuple(self._unwrap_args(args)) 

610 local_kwargs = self._unwrap_kwargs(kwargs) 

611 py_output = op_call(*local_args, **local_kwargs) 

612 op_name = platform.get_op_name(op_call) 

613 if op_name in _RAGGED_INPLACE_ELEMENTWISE_OPS: 

614 if not args or not isinstance(args[0], DTensor): 

615 raise ValueError( 

616 f"Ragged in-place operator {op_name!r} requires a DTensor first argument" 

617 ) 

618 return args[0] 

619 return DTensor.from_local_with_layout( 

620 py_output, 

621 copy.deepcopy(reference.layout), 

622 shape=tuple(reference.shape), 

623 ) 

624 

625 @staticmethod 

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

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

628 

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

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

631 

632 Args: 

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

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

635 

636 Returns: 

637 Tuple of (unwrapped_args, unwrapped_kwargs) with DTensor values 

638 replaced by their full tensor representations. 

639 

640 Warning: 

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

642 Operators without layout implementations should be registered properly. 

643 """ 

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

645 if isinstance(value, DTensor): 

646 return value.full_tensor() 

647 if isinstance(value, tuple): 

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

649 if isinstance(value, list): 

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

651 return value 

652 

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

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

655 

656 warnings.warn( 

657 "Operator has no distributed layout implementation. " 

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

659 "Consider registering a proper distributed operator.", 

660 UserWarning, 

661 stacklevel=4 

662 ) 

663 

664 return gathered_args, gathered_kwargs 

665 

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

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

668 

669 Args: 

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

671 

672 Returns: 

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

674 """ 

675 # Cheap O(1) frozenset checks first, short-circuit before the ContextVar 

676 # read (get_dtensor_dispatch) which is the priciest part of this guard. 

677 if op_name in self.whitelist or op_name in self._INPLACE_BYPASS_OPS: 

678 return True 

679 return get_dtensor_dispatch() is False and op_name not in get_no_skip_ops() 

680 

681 @staticmethod 

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

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

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

685 return 

686 first = args[0] 

687 if len(args) >= 2: 

688 second = args[1] 

689 elif "other" in kwargs: 

690 second = kwargs["other"] 

691 else: 

692 return 

693 if not isinstance(first, DTensor): 

694 return 

695 mesh_ndim = len(first.layout.partial) 

696 first_partial = tuple(first.layout.partial) 

697 if isinstance(second, DTensor): 

698 second_partial = tuple(second.layout.partial) 

699 if len(second_partial) != mesh_ndim: 

700 raise ValueError( 

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

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

703 ) 

704 else: 

705 second_partial = (None,) * mesh_ndim 

706 if first_partial != second_partial: 

707 raise ValueError( 

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

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

710 ) 

711 

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

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

714 

715 Args: 

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

717 

718 Returns: 

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

720 """ 

721 return is_loss_parallel_active() and is_loss_parallel_op(op_name) 

722 

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

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

725 

726 Args: 

727 op_name: Canonical operator name. 

728 args: Positional arguments for op_call. 

729 kwargs: Keyword arguments for op_call. 

730 

731 Raises: 

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

733 with vocab-sharded DTensor input. 

734 """ 

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

736 return 

737 

738 has_vocab_sharded_dtensor = False 

739 for arg in args: 

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

741 has_vocab_sharded_dtensor = True 

742 break 

743 if not has_vocab_sharded_dtensor: 

744 for val in kwargs.values(): 

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

746 has_vocab_sharded_dtensor = True 

747 break 

748 

749 if has_vocab_sharded_dtensor: 

750 raise ValueError( 

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

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

753 f"For example, replace:\n" 

754 f" with loss_parallel():\n" 

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

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

757 f"with:\n" 

758 f" with loss_parallel():\n" 

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

760 ) 

761 

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

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

764 

765 Args: 

766 op_call: The raw operator callable. 

767 args: Positional arguments for op_call. 

768 kwargs: Keyword arguments for op_call. 

769 

770 Returns: 

771 Result of the distributed cross_entropy computation. 

772 """ 

773 if platform.platform_type == PlatformType.PYTORCH: 

774 # pylint: disable=C0415 

775 from hyper_parallel.platform.torch.loss_parallel_ops import distributed_cross_entropy_from_op_call 

776 elif platform.platform_type == PlatformType.MINDSPORE: 

777 # pylint: disable=C0415 

778 from hyper_parallel.platform.mindspore.loss_parallel_ops import distributed_cross_entropy_from_op_call 

779 else: 

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

781 return distributed_cross_entropy_from_op_call(op_call, args, kwargs) 

782 

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

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

785 

786 Args: 

787 op_name: Canonical operator name. 

788 args: Positional arguments for op_call. 

789 

790 Raises: 

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

792 """ 

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

794 return 

795 

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

797 return 

798 

799 logits = args[0] 

800 if _is_shard_on_last_dim(logits): 

801 raise ValueError( 

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

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

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

805 f" with loss_parallel():\n" 

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

807 f" loss.backward()\n" 

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

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

810 ) 

811 

812 @staticmethod 

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

814 """ 

815 Normalize aclop-packed arguments for MindSpore backend operators. 

816 

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

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

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

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

821 for layout inference and preprocessing. 

822 

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

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

825 

826 Args: 

827 op_name (str): Canonical operator name. 

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

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

830 

831 Returns: 

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

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

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

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

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

837 """ 

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

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

840 return None, args 

841 

842 @staticmethod 

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

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

845 return ( 

846 op_name in unpack_ops 

847 and len(args) == 3 

848 and isinstance(args[1], str) 

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

850 ) 

851 

852 @staticmethod 

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

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

855 

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

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

858 as positional arguments in the usual way. 

859 

860 Args: 

861 op_impl: The op implementation callable. 

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

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

864 kwargs: Keyword arguments dict. 

865 

866 Returns: 

867 Result of the *op_impl* invocation. 

868 """ 

869 if packed_call is not None: 

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

871 return op_impl(*args, **kwargs) 

872 

873 def _handle_unregistered_op( 

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

875 ): 

876 """Handle ops that have no registered layout-inference entry. 

877 

878 This is a fallback path for ops that are not registered in 

879 ``layout_infer_ops``. When arguments contain DTensors it either raises 

880 (with a hint to register a distributed op) or, for loss-parallel ops, 

881 gathers tensors to full and dispatches through the raw callable. 

882 

883 Args: 

884 op_name: Canonical operator name. 

885 op_call: The raw operator callable. 

886 args: Positional arguments for op_call. 

887 kwargs: Keyword arguments for op_call. 

888 

889 Returns: 

890 Raw dispatch result (plain Tensor, not wrapped as DTensor). 

891 

892 Raises: 

893 RuntimeError: If op_name is not registered for layout inference. 

894 """ 

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

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

897 if has_dtensor: 

898 self._check_ce_op_without_loss_parallel_context(op_name, args) 

899 

900 if not is_loss_parallel_op(op_name): 

901 raise RuntimeError( 

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

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

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

905 ) 

906 

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

908 

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

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

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

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

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

914 logits = gathered_args[0] 

915 targets = gathered_args[1] 

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

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

918 vocab_size = logits.shape[-1] 

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

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

921 

922 return op_call(*gathered_args, **gathered_kwargs) 

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

924 

925 def _dispatch_layout_infer( 

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

927 ): 

928 """Standard dispatch through layout-inference: preprocess → infer → execute → wrap. 

929 

930 Args: 

931 op_name: Canonical operator name (already resolved by the caller). 

932 op_call: The raw operator callable. 

933 args: Positional arguments for op_call. 

934 kwargs: Keyword arguments for op_call. 

935 

936 Returns: 

937 DTensor: Dispatched result wrapped as DTensor. 

938 

939 Raises: 

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

941 """ 

942 if op_name not in self.layout_infer_ops: 

943 return self._handle_unregistered_op(op_name, op_call, args, kwargs) 

944 

945 cache_manager = LayoutCacheManager.get_instance() 

946 distribute_op = cache_manager.distributed_op(op_name) 

947 

948 # Normalize aclop-packed args before any per-op processing. Only the handful 

949 # of (deprecation-bound) unpack_ops ever use the packed format, so gate the 

950 # whole normalization behind an O(1) membership test instead of paying two 

951 # function frames (_normalize_aclop_args + _is_aclop_packed) on every op. 

952 if op_name in getattr(self, 'unpack_ops', ()): 

953 packed_call, args = self._normalize_aclop_args(op_name, self.unpack_ops, args) 

954 else: 

955 packed_call = None 

956 

957 result = distribute_op.preprocess(args, kwargs) 

958 if result is None: 

959 raise RuntimeError( 

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

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

962 ) 

963 local_args, local_kwargs, cache_values = result 

964 cache_key = LayoutCacheKey.from_cache_values(cache_values) 

965 

966 infer_result, op_impl = OpDispatcher._lookup_or_infer_layout( 

967 op_call, op_name, cache_key, cache_values, distribute_op, cache_manager 

968 ) 

969 

970 op_impl = op_call if op_impl is None else op_impl 

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

972 output = distribute_op.wrap_output(py_output, infer_result[0]) 

973 return OpDispatcher._restore_inplace_dtensor_result(op_name, args, output) 

974 

975 @staticmethod 

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

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

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

979 return args[0] 

980 return output 

981 

982 @staticmethod 

983 def _lookup_or_infer_layout(func, func_name, cache_key, cache_values, distribute_op, cache_manager): 

984 """Look up cached layout or compute via distributed op. 

985 

986 Returns: 

987 (infer_result, op_impl) 

988 """ 

989 layout_cache = cache_manager.get_layout_cache() 

990 if func_name not in layout_cache: 

991 layout_cache[func_name] = {} 

992 op_layout_cache = layout_cache[func_name] 

993 if cache_key in op_layout_cache: 

994 return op_layout_cache[cache_key] 

995 infer_result = distribute_op.infer_layout(cache_values) 

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

997 op_layout_cache[cache_key] = (infer_result, op_impl) 

998 return infer_result, op_impl 

999 

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

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

1002 

1003 Args: 

1004 op_call: The raw operator callable. 

1005 args: Positional arguments for op_call. 

1006 kwargs: Keyword arguments for op_call. 

1007 

1008 Returns: 

1009 Result of the dispatched op call. 

1010 """ 

1011 op_name = platform.get_op_name(op_call) 

1012 if logger.isEnabledFor(logging.DEBUG): 

1013 log_dispatch_enter(op_name, args, kwargs) 

1014 

1015 observer = _debug_mode_observer.get() 

1016 if observer is not None: 

1017 observer.on_op_dispatch_enter(op_name, op_call, args, kwargs) 

1018 

1019 result = None 

1020 try: 

1021 should_bypass = self._should_bypass_dispatch(op_name) 

1022 ragged_dtensor = None 

1023 if not should_bypass or op_name not in _RAGGED_METADATA_BYPASS_OPS: 

1024 ragged_dtensor = self._validate_ragged_dispatch(op_name, args, kwargs) 

1025 if ragged_dtensor is not None: 

1026 result = self._dispatch_ragged_elementwise( 

1027 op_call, args, kwargs, ragged_dtensor 

1028 ) 

1029 return result 

1030 

1031 if should_bypass: 

1032 self._validate_inplace_partial_inputs(op_name, args, kwargs) 

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

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

1035 result = args[0] 

1036 return result 

1037 

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

1039 result = self._dispatch_random_op(op_name, op_call, args, kwargs) 

1040 return result 

1041 

1042 self._check_decomposed_ce_op_in_loss_parallel(op_name, args, kwargs) 

1043 

1044 if self._should_dispatch_loss_parallel(op_name): 

1045 result = self._dispatch_loss_parallel(op_call, args, kwargs) 

1046 return result 

1047 

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

1049 self.layout_infer_ops[op_name] = {} 

1050 

1051 result = self._dispatch_layout_infer(op_name, op_call, args, kwargs) 

1052 return result 

1053 finally: 

1054 if logger.isEnabledFor(logging.DEBUG): 

1055 log_dispatch_exit(op_name, result) 

1056 

1057 if observer is not None: 

1058 observer.on_op_dispatch_exit(op_name, result) 

1059 

1060_OP_DISPATCHER = OpDispatcher()