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

617 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-07-06 05:41 +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._suffix_dispatch: Dict[str, str] = { 

256 "WithShape": "_with_layout_infer_with_shape", 

257 "Reshape": "_with_layout_infer_reshape", 

258 "WithTupleExpand": "_with_layout_infer_with_tuple_expand", 

259 "Slice": "_with_layout_infer_slice", 

260 } 

261 

262 self._register_distributed_ops() 

263 

264 def _setup_paths_from_env(self): 

265 """ 

266 Setup YAML directory and Python path from environment variables. 

267 

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

269 environment variables HYPER_PARALLEL_OPS_YAML_DIR and HYPER_PARALLEL_OPS_PYTHON_PATH. 

270 """ 

271 self._setup_yaml_dir(self._env_yaml_dir) 

272 self._extend_sys_path(self._env_python_path) 

273 

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

275 """ 

276 Feature: Configure yaml_dir/work_dir for OpDispatcher 

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

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

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

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

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

282 safe_load_yaml_from_dir(); no functional behavior is changed. 

283 """ 

284 if env_yaml_dir: 

285 if os.path.isabs(env_yaml_dir): 

286 self.yaml_dir = env_yaml_dir 

287 self.work_dir = "" 

288 else: 

289 self.work_dir = os.path.normpath( 

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

291 ) 

292 self.yaml_dir = env_yaml_dir 

293 else: 

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

295 self.work_dir = os.path.normpath( 

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

297 ) 

298 

299 @staticmethod 

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

301 if not env_python_path: 

302 return 

303 python_paths = env_python_path.split(":") 

304 for path in python_paths: 

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

306 sys.path.append(path) 

307 

308 def _register_distributed_ops(self): 

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

310 self._register_single_distributed_op(op_name, config) 

311 

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

313 """ 

314 Feature: Register a single distributed op implementation 

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

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

317 Prefer 'distributed_op_module' when provided; otherwise import from 

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

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

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

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

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

323 """ 

324 class_name = config["distributed_op_class"] 

325 

326 if "distributed_op_module" in config: 

327 module_name = config["distributed_op_module"] 

328 module = importlib.import_module(module_name) 

329 op_class = getattr(module, class_name) 

330 _ = op_class(op_name) 

331 return 

332 

333 module_file = config["distributed_op_file"] 

334 try: 

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

336 module = importlib.import_module(module_name) 

337 op_class = getattr(module, class_name) 

338 _ = op_class(op_name) 

339 except (ModuleNotFoundError, ImportError): 

340 if self._env_python_path: 

341 module = importlib.import_module(module_file) 

342 op_class = getattr(module, class_name) 

343 _ = op_class(op_name) 

344 else: 

345 raise 

346 

347 @staticmethod 

348 def _process_args_and_kwargs( 

349 args, kwargs 

350 ) -> tuple[list, list, list, dict, list]: 

351 """_process_args_and_kwargs""" 

352 input_layouts = [] 

353 extra_args = [] 

354 input_args = [] 

355 input_kwargs = kwargs.copy() 

356 cache_key_values = [] 

357 

358 for arg in args: 

359 if arg is None: 

360 input_layouts.append(None) 

361 input_args.append(arg) 

362 continue 

363 

364 if not hasattr(arg, "_layout"): 

365 id_str = "scalar" 

366 if not isinstance(arg, Tensor): 

367 id_str = str(arg) 

368 cache_key_values.append(id_str) 

369 extra_args.append(arg) 

370 input_layouts.append(None) 

371 input_args.append(arg) 

372 else: 

373 layout = arg.layout 

374 layout_id = layout.compact_str 

375 cache_key_values.append(str(layout_id)) 

376 input_layouts.append(layout) 

377 if isinstance(arg, DTensor): 

378 input_args.append(arg.to_local()) 

379 else: 

380 input_args.append(arg) 

381 

382 for k, val in kwargs.items(): 

383 if val is None: 

384 input_layouts.append(None) 

385 continue 

386 if not hasattr(val, "_layout"): 

387 id_str = "scalar" 

388 if not isinstance(val, Tensor): 

389 id_str = str(val) 

390 cache_key_values.append(id_str) 

391 extra_args.append(val) 

392 input_layouts.append(None) 

393 else: 

394 layout = val.layout 

395 layout_id = layout.compact_str 

396 cache_key_values.append(str(layout_id)) 

397 input_layouts.append(layout) 

398 if isinstance(val, DTensor): 

399 input_kwargs[k] = val.to_local() 

400 

401 return input_layouts, extra_args, input_args, input_kwargs, cache_key_values 

402 

403 @staticmethod 

404 def _with_layout_infer(func: callable, *args, _packed_call=None, **kwargs) -> Tensor: 

405 """_with_layout_infer 

406 

407 NOTE: aclop packed args are already normalized upstream by 

408 ``_dispatch_layout_infer`` via ``_normalize_aclop_args``. The 

409 ``_packed_call`` kwarg carries the ``(prim, name)`` tuple when the 

410 op uses aclop format, or ``None`` otherwise. 

411 """ 

412 func_name = platform.get_op_name(func) 

413 

414 input_layouts, extra_args, input_args, input_kwargs, cache_values = \ 

415 OpDispatcher._process_args_and_kwargs(args, kwargs) 

416 cache_key = LayoutCacheKey(cache_values) 

417 

418 output_layout, op_impl, _ = OpDispatcher._lookup_or_infer_layout( 

419 func, func_name, cache_key, input_layouts, extra_args 

420 ) 

421 

422 op_impl = func if op_impl is None else op_impl 

423 py_output = OpDispatcher._call_op_impl(op_impl, _packed_call, input_args, input_kwargs) 

424 return OpDispatcher._pack_infer_output(py_output, output_layout) 

425 

426 @staticmethod 

427 def _extract_single_arg_layout(expanded_args, kwargs_value): 

428 """Helper to extract layout and cache info for a single argument.""" 

429 cache_key_values = [] 

430 input_layouts = [] 

431 extra_args = [] 

432 

433 for arg in chain(expanded_args, kwargs_value): 

434 if arg is None: 

435 input_layouts.append(None) 

436 continue 

437 

438 if not hasattr(arg, "_layout"): 

439 id_str = "scalar" if isinstance(arg, Tensor) else str(arg) 

440 cache_key_values.append(id_str) 

441 extra_args.append(arg) 

442 input_layouts.append(None) 

443 else: 

444 layout = arg.layout 

445 cache_key_values.append(str(layout.compact_str)) 

446 input_layouts.append(layout) 

447 return cache_key_values, input_layouts, extra_args 

448 

449 @staticmethod 

450 def _pack_infer_output(py_output, output_layout): 

451 """Helper to pack py_output into DTensors using output_layout.""" 

452 if isinstance(py_output, (tuple, list)): 

453 if not isinstance(output_layout, (tuple, list)): 

454 raise RuntimeError("Output is a tuple but layout is not") 

455 if len(py_output) != len(output_layout): 

456 raise RuntimeError(f"Output tuple size ({len(py_output)}) " 

457 f"does not match layout tuple size ({len(output_layout)})") 

458 

459 return tuple( 

460 DTensor.from_local(item, layout.mesh, layout.alias_placements) 

461 for item, layout in zip(py_output, output_layout) 

462 ) 

463 

464 return DTensor.from_local(py_output, output_layout.mesh, output_layout.alias_placements) 

465 

466 @staticmethod 

467 def _expand_tuple_args(args: tuple) -> tuple: 

468 """Expand tuple/list arguments for tuple element-wise ops. 

469 

470 Returns: 

471 (expanded_args, input_args): Flat list of DTensors for layout extraction 

472 and list of local-tensor tuples/values for kernel invocation. 

473 """ 

474 expanded_args = [] 

475 input_args = [] 

476 for arg in args: 

477 if isinstance(arg, (tuple, list)): 

478 expanded_args.extend(arg) 

479 # pylint: disable=R1728 

480 input_args.append(tuple( 

481 item.to_local() if hasattr(item, "_layout") else item for item in arg 

482 )) 

483 else: 

484 expanded_args.append(arg) 

485 input_args.append(arg.to_local() if isinstance(arg, DTensor) else arg) 

486 return expanded_args, input_args 

487 

488 @staticmethod 

489 def _lookup_or_infer_layout(func, func_name, cache_key, input_layouts, extra_args): 

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

491 

492 Returns: 

493 (output_layout, op_impl, distribute_op) 

494 """ 

495 cache_manager = LayoutCacheManager.get_instance() 

496 layout_cache = cache_manager.get_layout_cache() 

497 if func_name not in layout_cache: 

498 layout_cache[func_name] = {} 

499 

500 op_layout_cache = layout_cache[func_name] 

501 distribute_op = cache_manager.distributed_op(func_name) 

502 

503 if cache_key in op_layout_cache: 

504 output_layout, op_impl = op_layout_cache[cache_key] 

505 else: 

506 output_layout = distribute_op.infer_layout(input_layouts, extra_args) 

507 op_impl = distribute_op.get_expand_impl(func, output_layout, input_layouts, extra_args) 

508 op_layout_cache[cache_key] = (output_layout, op_impl) 

509 

510 return output_layout, op_impl, distribute_op 

511 

512 @staticmethod 

513 def _prepare_tuple_expand_args(args, kwargs): 

514 """Prepare arguments for tuple-expand layout inference. 

515 

516 Returns: 

517 (input_args, input_kwargs, input_layouts, extra_args, cache_key) 

518 """ 

519 expanded_args, input_args = OpDispatcher._expand_tuple_args(args) 

520 input_kwargs = { 

521 key: value.to_local() if isinstance(value, DTensor) else value 

522 for key, value in kwargs.items() 

523 } 

524 cache_values, input_layouts, extra_args = OpDispatcher._extract_single_arg_layout( 

525 expanded_args, kwargs.values() 

526 ) 

527 return input_args, input_kwargs, input_layouts, extra_args, LayoutCacheKey(cache_values) 

528 

529 def _with_layout_infer_with_tuple_expand(self, func: callable, *args, _packed_call=None, **kwargs) -> Tensor: 

530 """_with_layout_infer_with_tuple_expand 

531 

532 NOTE: aclop packed args are already normalized upstream by 

533 ``_dispatch_layout_infer`` via ``_normalize_aclop_args``. The 

534 ``_packed_call`` kwarg carries the ``(prim, name)`` tuple when the 

535 op uses aclop format, or ``None`` otherwise. 

536 """ 

537 input_args, input_kwargs, input_layouts, extra_args, cache_key = ( 

538 self._prepare_tuple_expand_args(args, kwargs) 

539 ) 

540 func_name = platform.get_op_name(func) 

541 

542 output_layout, op_impl, distribute_op = OpDispatcher._lookup_or_infer_layout( 

543 func, func_name, cache_key, input_layouts, extra_args 

544 ) 

545 

546 op_impl = func if op_impl is None else op_impl 

547 py_output = OpDispatcher._call_op_impl(op_impl, _packed_call, input_args, input_kwargs) 

548 return distribute_op.wrap_output(py_output, output_layout) 

549 

550 @staticmethod 

551 def _with_layout_infer_reshape(func: callable, *args) -> Tensor: 

552 """_with_layout_infer_reshape""" 

553 input_tensor = args[0] 

554 

555 # PyTorch reshape signature: reshape(*shape) 

556 # Can be called as: 

557 # reshape(-1, 1024) -> args = (tensor, -1, 1024) 

558 # reshape((-1, 1024)) -> args = (tensor, (-1, 1024)) 

559 # reshape([4, 16, 1024]) -> args = (tensor, [4, 16, 1024]) 

560 if len(args) > 2: 

561 # Multiple int arguments: reshape(-1, 1024) 

562 shape = args[1:] 

563 else: 

564 # Single argument: reshape((4, 16, 1024)) or reshape(4) 

565 shape = args[1] 

566 

567 layout = input_tensor.layout 

568 input_layouts = [layout] 

569 

570 extra_args = [shape, input_tensor.shape] 

571 

572 cache_key_values = [str(layout.compact_str), str(shape), str(input_tensor.shape)] 

573 cache_key = LayoutCacheKey(cache_key_values) 

574 

575 cache_manager = LayoutCacheManager.get_instance() 

576 layout_cache = cache_manager.get_layout_cache() 

577 func_name = platform.get_op_name(func) 

578 if func_name not in layout_cache: 

579 layout_cache[func_name] = {} 

580 

581 op_layout_cache = layout_cache[func_name] 

582 

583 distribute_op = cache_manager.distributed_op(func_name) 

584 if cache_key in op_layout_cache: 

585 infer_output, op_impl = op_layout_cache[cache_key] 

586 else: 

587 all_args = (input_layouts, extra_args) 

588 infer_output = distribute_op.infer_layout(*all_args) 

589 op_impl = distribute_op.get_expand_impl(func, infer_output, input_layouts, extra_args) 

590 op_layout_cache[cache_key] = (infer_output, op_impl) 

591 

592 infer_output_tuple = infer_output 

593 local_shape = infer_output_tuple[1] 

594 

595 if op_impl is None: 

596 op_impl = func 

597 

598 py_output = op_impl(input_tensor.to_local(), local_shape) 

599 

600 return DTensor.from_local(py_output, infer_output_tuple[0].mesh, infer_output_tuple[0].alias_placements) 

601 

602 @staticmethod 

603 def _process_args_and_kwargs_with_shape(args, kwargs): 

604 """Process args and kwargs with input shapes for WithShape suffix operators. 

605 

606 Args: 

607 args: Positional arguments from dispatch. 

608 kwargs: Keyword arguments from dispatch. 

609 

610 Returns: 

611 tuple: (input_layouts, input_shapes, extra_args, input_args, input_kwargs, cache_key_values) 

612 """ 

613 input_layouts = [] 

614 extra_args = [] 

615 input_shapes = [] 

616 input_args = [] 

617 input_kwargs = kwargs.copy() 

618 cache_key_values = [] 

619 

620 for arg in args: 

621 if arg is None: 

622 input_layouts.append(None) 

623 input_shapes.append(None) 

624 input_args.append(arg) 

625 continue 

626 

627 if not hasattr(arg, "_layout"): 

628 id_str = "scalar" 

629 if not isinstance(arg, Tensor): 

630 id_str = str(arg) 

631 cache_key_values.append(id_str) 

632 extra_args.append(arg) 

633 input_layouts.append(None) 

634 input_args.append(arg) 

635 else: 

636 layout = arg.layout 

637 layout_id = layout.compact_str 

638 cache_key_values.append(str(layout_id)) 

639 input_layouts.append(layout) 

640 if isinstance(arg, DTensor): 

641 input_args.append(arg.to_local()) 

642 else: 

643 input_args.append(arg) 

644 

645 if not hasattr(arg, "shape"): 

646 input_shapes.append(None) 

647 else: 

648 input_shape = arg.shape 

649 input_shapes.append(input_shape) 

650 cache_key_values.append(str(input_shape)) 

651 

652 for k, val in kwargs.items(): 

653 if val is None: 

654 input_layouts.append(None) 

655 continue 

656 if not hasattr(val, "_layout"): 

657 id_str = "scalar" 

658 if not isinstance(val, Tensor): 

659 id_str = str(val) 

660 cache_key_values.append(id_str) 

661 extra_args.append(val) 

662 input_layouts.append(None) 

663 else: 

664 layout = val.layout 

665 layout_id = layout.compact_str 

666 cache_key_values.append(str(layout_id)) 

667 input_layouts.append(layout) 

668 if isinstance(val, DTensor): 

669 input_kwargs[k] = val.to_local() 

670 

671 if not hasattr(val, "shape"): 

672 input_shapes.append(None) 

673 else: 

674 input_shape = val.shape 

675 input_shapes.append(input_shape) 

676 cache_key_values.append(str(input_shape)) 

677 

678 return input_layouts, input_shapes, extra_args, input_args, input_kwargs, cache_key_values 

679 

680 @staticmethod 

681 def _prepare_with_shape_args(args, kwargs): 

682 """Prepare arguments and cache key for WithShape operators. 

683 

684 Returns: 

685 (layouts, extra_args, local_args, local_kwargs, cache_key) 

686 """ 

687 processed = OpDispatcher._process_args_and_kwargs_with_shape(args, kwargs) 

688 layouts, shapes, extra_args, local_args, local_kwargs, cache_values = processed 

689 extra_args.append(shapes) 

690 return layouts, extra_args, local_args, local_kwargs, LayoutCacheKey(cache_values) 

691 

692 @staticmethod 

693 def _with_layout_infer_with_shape(func: callable, *args, _packed_call=None, **kwargs) -> Tensor: 

694 """_with_layout_infer_with_shape 

695 

696 NOTE: aclop packed args are already normalized upstream by 

697 ``_dispatch_layout_infer`` via ``_normalize_aclop_args``. The 

698 ``_packed_call`` kwarg carries the ``(prim, name)`` tuple when the 

699 op uses aclop format, or ``None`` otherwise. 

700 """ 

701 prepared = OpDispatcher._prepare_with_shape_args(args, kwargs) 

702 layouts, extra_args, local_args, local_kwargs, cache_key = prepared 

703 func_name = platform.get_op_name(func) 

704 

705 infer_result = OpDispatcher._lookup_or_infer_layout( 

706 func, func_name, cache_key, layouts, extra_args 

707 ) 

708 output_layout, op_impl = infer_result[:2] 

709 

710 op_impl = func if op_impl is None else op_impl 

711 py_output = OpDispatcher._call_op_impl(op_impl, _packed_call, local_args, local_kwargs) 

712 return OpDispatcher._pack_infer_output(py_output, output_layout) 

713 

714 def _with_layout_infer_slice(self, func: callable, *args) -> Tensor: 

715 """_with_layout_infer_slice""" 

716 input_tensor = args[0] 

717 begin = args[1] 

718 end = args[2] 

719 

720 # input layout 

721 input_layouts = [] 

722 

723 layout = input_tensor.layout 

724 global_shape = input_tensor.shape 

725 input_layouts.append(layout) 

726 layout_id = layout.compact_str 

727 

728 extra_args = [] 

729 extra_args.append(begin) 

730 extra_args.append(end) 

731 extra_args.append(global_shape) 

732 cache_key_values = [str(layout_id), str(begin), str(end), str(global_shape)] 

733 cache_key = LayoutCacheKey(cache_key_values) 

734 

735 cache_manager = LayoutCacheManager.get_instance() 

736 layout_cache = cache_manager.get_layout_cache() 

737 func_name = platform.get_op_name(func) 

738 if func_name not in layout_cache: 

739 layout_cache[func_name] = {} 

740 

741 op_layout_cache = layout_cache[func_name] 

742 

743 distribute_op = cache_manager.distributed_op(func_name) 

744 if cache_key in op_layout_cache: 

745 infer_output, op_impl = op_layout_cache[cache_key] 

746 else: 

747 all_args = (input_layouts, extra_args) 

748 infer_output = distribute_op.infer_layout(*all_args) 

749 op_impl = distribute_op.get_expand_impl(func, infer_output, input_layouts, extra_args) 

750 op_layout_cache[cache_key] = (infer_output, op_impl) 

751 

752 infer_output_tuple = infer_output 

753 new_begin = infer_output_tuple[1] 

754 new_end = infer_output_tuple[2] 

755 

756 if op_impl is None: 

757 op_impl = func 

758 

759 py_output = op_impl(input_tensor.to_local(), new_begin, new_end) 

760 

761 return DTensor.from_local(py_output, infer_output_tuple[0].mesh, infer_output_tuple[0].alias_placements) 

762 

763 @staticmethod 

764 def _merge_default(config: dict): 

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

766 if "__default__" not in config: 

767 return config 

768 

769 default_cfg = config["__default__"] 

770 merged = {} 

771 

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

773 if op_name == "__default__": 

774 continue 

775 

776 new_cfg = default_cfg.copy() 

777 new_cfg.update(op_cfg) 

778 merged[op_name] = new_cfg 

779 

780 return merged 

781 

782 def safe_load_yaml_from_dir(self) -> dict: 

783 """ 

784 Load yaml dictionary from directory. 

785 

786 Returns: 

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

788 """ 

789 yaml_dict = {} 

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

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

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

793 

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

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

796 yaml_data = yaml.safe_load(f) 

797 

798 yaml_data = OpDispatcher._merge_default(yaml_data) 

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

800 if name in yaml_dict: 

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

802 yaml_dict[name] = data 

803 

804 return yaml_dict 

805 

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

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

808 first_arg = next( 

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

810 None, 

811 ) 

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

813 if first_arg is None: 

814 return op_call(*args, **kwargs) 

815 

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

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

818 first_local_arg = first_arg.to_local() 

819 

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

821 self._rng_tracker = OffsetBasedRNGTracker() 

822 

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

824 if ( 

825 self._rng_tracker is not None 

826 and not first_local_arg.is_meta 

827 and self._rng_tracker.distribute_region_enabled 

828 ): 

829 # pylint: disable=W0212 

830 with self._rng_tracker._distribute_region( 

831 device_mesh=first_arg.device_mesh, 

832 placements=first_arg.placements, 

833 global_shape=first_arg.shape, 

834 generator=maybe_user_generator, 

835 ): 

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

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

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

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

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

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

842 if platform.platform_type == PlatformType.MINDSPORE: 

843 offset_incr = self._rng_tracker.compute_offset_incr( 

844 first_arg.device_mesh, first_arg.placements, first_arg.shape, 

845 ) 

846 local_args = _apply_shard_offset_to_rng_args(local_args, offset_incr) 

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

848 else: 

849 if maybe_user_generator is not None: 

850 local_kwargs["generator"] = maybe_user_generator 

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

852 

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

854 

855 @staticmethod 

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

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

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

859 if len(args) >= 4: 

860 return bool(args[3]) 

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

862 

863 @staticmethod 

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

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

866 if op_name in OpDispatcher._RANDOM_INPLACE_MS_OPS: 

867 return True 

868 if op_name == "FuncDropoutExt": 

869 return OpDispatcher._func_dropout_ext_inplace(args, kwargs) 

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

871 return op_name.endswith('_') 

872 

873 @staticmethod 

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

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

876 

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

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

879 ``_RANDOM_INPLACE_MS_OPS``. ``FuncDropoutExt`` is handled separately 

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

881 """ 

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

883 return first_arg 

884 mesh = first_arg.device_mesh 

885 placements = first_arg.layout.alias_placements 

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

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

888 return tuple( 

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

890 for r in local_results 

891 ) 

892 if isinstance(local_results, Tensor): 

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

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

895 return local_results 

896 

897 @staticmethod 

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

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

900 

901 Args: 

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

903 

904 Returns: 

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

906 """ 

907 if isinstance(value, DTensor): 

908 return value.to_local() 

909 if isinstance(value, tuple): 

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

911 if isinstance(value, list): 

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

913 return value 

914 

915 @staticmethod 

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

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

918 

919 Args: 

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

921 

922 Returns: 

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

924 """ 

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

926 

927 @staticmethod 

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

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

930 

931 Args: 

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

933 

934 Returns: 

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

936 """ 

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

938 

939 @staticmethod 

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

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

942 

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

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

945 

946 Args: 

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

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

949 

950 Returns: 

951 Tuple of (unwrapped_args, unwrapped_kwargs) with DTensor values 

952 replaced by their full tensor representations. 

953 

954 Warning: 

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

956 Operators without layout implementations should be registered properly. 

957 """ 

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

959 if isinstance(value, DTensor): 

960 return value.full_tensor() 

961 if isinstance(value, tuple): 

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

963 if isinstance(value, list): 

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

965 return value 

966 

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

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

969 

970 warnings.warn( 

971 "Operator has no distributed layout implementation. " 

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

973 "Consider registering a proper distributed operator.", 

974 UserWarning, 

975 stacklevel=4 

976 ) 

977 

978 return gathered_args, gathered_kwargs 

979 

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

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

982 

983 Args: 

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

985 

986 Returns: 

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

988 """ 

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

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

991 

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

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

994 

995 Args: 

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

997 

998 Returns: 

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

1000 """ 

1001 return is_loss_parallel_active() and is_loss_parallel_op(op_name) 

1002 

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

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

1005 

1006 Args: 

1007 op_name: Canonical operator name. 

1008 args: Positional arguments for op_call. 

1009 kwargs: Keyword arguments for op_call. 

1010 

1011 Raises: 

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

1013 with vocab-sharded DTensor input. 

1014 """ 

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

1016 return 

1017 

1018 has_vocab_sharded_dtensor = False 

1019 for arg in args: 

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

1021 has_vocab_sharded_dtensor = True 

1022 break 

1023 if not has_vocab_sharded_dtensor: 

1024 for val in kwargs.values(): 

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

1026 has_vocab_sharded_dtensor = True 

1027 break 

1028 

1029 if has_vocab_sharded_dtensor: 

1030 raise ValueError( 

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

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

1033 f"For example, replace:\n" 

1034 f" with loss_parallel():\n" 

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

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

1037 f"with:\n" 

1038 f" with loss_parallel():\n" 

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

1040 ) 

1041 

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

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

1044 

1045 Args: 

1046 op_call: The raw operator callable. 

1047 args: Positional arguments for op_call. 

1048 kwargs: Keyword arguments for op_call. 

1049 

1050 Returns: 

1051 Result of the distributed cross_entropy computation. 

1052 """ 

1053 if platform.platform_type == PlatformType.PYTORCH: 

1054 # pylint: disable=C0415 

1055 from hyper_parallel.platform.torch.loss_parallel_ops import distributed_cross_entropy_from_op_call 

1056 elif platform.platform_type == PlatformType.MINDSPORE: 

1057 # pylint: disable=C0415 

1058 from hyper_parallel.platform.mindspore.loss_parallel_ops import distributed_cross_entropy_from_op_call 

1059 else: 

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

1061 return distributed_cross_entropy_from_op_call(op_call, args, kwargs) 

1062 

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

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

1065 

1066 Args: 

1067 op_name: Canonical operator name. 

1068 args: Positional arguments for op_call. 

1069 

1070 Raises: 

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

1072 """ 

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

1074 return 

1075 

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

1077 return 

1078 

1079 logits = args[0] 

1080 if _is_shard_on_last_dim(logits): 

1081 raise ValueError( 

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

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

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

1085 f" with loss_parallel():\n" 

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

1087 f" loss.backward()\n" 

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

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

1090 ) 

1091 

1092 @staticmethod 

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

1094 """ 

1095 Normalize aclop-packed arguments for MindSpore backend operators. 

1096 

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

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

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

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

1101 for layout inference and preprocessing. 

1102 

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

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

1105 

1106 Args: 

1107 op_name (str): Canonical operator name. 

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

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

1110 

1111 Returns: 

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

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

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

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

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

1117 """ 

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

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

1120 return None, args 

1121 

1122 @staticmethod 

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

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

1125 return ( 

1126 op_name in unpack_ops 

1127 and len(args) == 3 

1128 and isinstance(args[1], str) 

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

1130 ) 

1131 

1132 @staticmethod 

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

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

1135 

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

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

1138 as positional arguments in the usual way. 

1139 

1140 Args: 

1141 op_impl: The op implementation callable. 

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

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

1144 kwargs: Keyword arguments dict. 

1145 

1146 Returns: 

1147 Result of the *op_impl* invocation. 

1148 """ 

1149 if packed_call is not None: 

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

1151 return op_impl(*args, **kwargs) 

1152 

1153 def _dispatch_layout_infer( 

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

1155 ): 

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

1157 

1158 Args: 

1159 op_name: Canonical operator name. 

1160 op_call: The raw operator callable. 

1161 args: Positional arguments for op_call. 

1162 kwargs: Keyword arguments for op_call. 

1163 

1164 Returns: 

1165 Result of the layout-infer dispatch. 

1166 

1167 Raises: 

1168 RuntimeError: If op_name is not registered or has an unknown suffix. 

1169 """ 

1170 if op_name not in self.layout_infer_ops: 

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

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

1173 if has_dtensor: 

1174 self._check_ce_op_without_loss_parallel_context(op_name, args) 

1175 

1176 if not is_loss_parallel_op(op_name): 

1177 raise RuntimeError( 

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

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

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

1181 ) 

1182 

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

1184 

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

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

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

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

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

1190 logits = gathered_args[0] 

1191 targets = gathered_args[1] 

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

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

1194 vocab_size = logits.shape[-1] 

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

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

1197 

1198 return op_call(*gathered_args, **gathered_kwargs) 

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

1200 

1201 cache_manager = LayoutCacheManager.get_instance() 

1202 distribute_op = cache_manager.distributed_op(op_name) 

1203 

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

1205 # This allows all distributed op classes (ElementWiseDistributedOp, 

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

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

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

1209 

1210 result = distribute_op.preprocess(args, kwargs) 

1211 if result is not None: 

1212 return self._dispatch_new(op_call, distribute_op, packed_call, result) 

1213 

1214 suffix = self.layout_infer_ops[op_name].get('infer_layout_suffix', '') 

1215 if not suffix: 

1216 return self._with_layout_infer(op_call, *args, _packed_call=packed_call, **kwargs) 

1217 

1218 if suffix == 'WithShape': 

1219 return self._with_layout_infer_with_shape(op_call, *args, _packed_call=packed_call, **kwargs) 

1220 

1221 if suffix == 'WithTupleExpand': 

1222 return self._with_layout_infer_with_tuple_expand(op_call, *args, _packed_call=packed_call, **kwargs) 

1223 

1224 handler_name = self._suffix_dispatch.get(suffix) 

1225 if handler_name is None: 

1226 raise RuntimeError(f"Operator {op_name} specified wrong suffix in parallel yaml.") 

1227 return getattr(self, handler_name)(op_call, *args, **kwargs) 

1228 

1229 @staticmethod 

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

1231 """New dispatch flow using preprocess result. 

1232 

1233 Args: 

1234 func: Original function. 

1235 distribute_op: Distributed operation instance. 

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

1237 or None for regular ops. 

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

1239 

1240 Returns: 

1241 Tensor: Dispatched result as DTensor. 

1242 """ 

1243 local_args, local_kwargs, cache_values = result 

1244 cache_key = LayoutCacheKey.from_cache_values(cache_values) 

1245 func_name = platform.get_op_name(func) 

1246 

1247 infer_result, op_impl = OpDispatcher._lookup_or_infer_layout_new( 

1248 func, func_name, cache_key, cache_values, distribute_op 

1249 ) 

1250 

1251 op_impl = func if op_impl is None else op_impl 

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

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

1254 

1255 @staticmethod 

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

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

1258 

1259 Returns: 

1260 (infer_result, op_impl) 

1261 """ 

1262 cache_manager = LayoutCacheManager.get_instance() 

1263 layout_cache = cache_manager.get_layout_cache() 

1264 if func_name not in layout_cache: 

1265 layout_cache[func_name] = {} 

1266 op_layout_cache = layout_cache[func_name] 

1267 if cache_key in op_layout_cache: 

1268 return op_layout_cache[cache_key] 

1269 infer_result = distribute_op.infer_layout(cache_values) 

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

1271 op_layout_cache[cache_key] = (infer_result, op_impl) 

1272 return infer_result, op_impl 

1273 

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

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

1276 

1277 Args: 

1278 op_call: The raw operator callable. 

1279 args: Positional arguments for op_call. 

1280 kwargs: Keyword arguments for op_call. 

1281 

1282 Returns: 

1283 Result of the dispatched op call. 

1284 """ 

1285 op_name = platform.get_op_name(op_call) 

1286 

1287 if self._should_bypass_dispatch(op_name): 

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

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

1290 return args[0] 

1291 return result 

1292 

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

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

1295 

1296 self._check_decomposed_ce_op_in_loss_parallel(op_name, args, kwargs) 

1297 

1298 if self._should_dispatch_loss_parallel(op_name): 

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

1300 

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

1302 self.layout_infer_ops[op_name] = {} 

1303 

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

1305 

1306_OP_DISPATCHER = OpDispatcher()