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

377 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-10-25 05:45 +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 warnings 

23from contextvars import ContextVar 

24from functools import lru_cache 

25from itertools import chain 

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

27 

28import torch 

29import yaml 

30 

31from hyper_parallel.core.shard.ops.parallel_ops import DistributedOp 

32from hyper_parallel.core.shard.utils import distributed_cross_entropy_from_op_call, get_op_name 

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

34from hyper_parallel.core.dtensor.dtensor import DTensor 

35from hyper_parallel.core.dtensor.layout import RaggedShardInfo 

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

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

38 

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

40from hyper_parallel.core.tensor_parallel.loss_parallel import is_loss_parallel_active 

41from hyper_parallel.core.tensor_parallel.loss_parallel_ops_common import _is_shard_on_last_dim 

42 

43logger = logging.getLogger(__name__) 

44 

45_DISTRIBUTED_OPS_PACKAGE = "hyper_parallel.core.shard.ops" 

46_DISTRIBUTED_OPS_DIR = os.path.realpath(os.path.join(os.path.dirname(__file__), "ops")) 

47_DISTRIBUTED_OPS_YAML_DIR = os.path.join(_DISTRIBUTED_OPS_DIR, "yaml") 

48 

49 

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

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

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

53 

54_RAGGED_ELEMENTWISE_OPS = { 

55 # Torch public APIs dispatch with these lowercase names. 

56 "abs": "unary", "absolute": "unary", "clamp": "unary", 

57 "clamp_max": "unary", "clamp_min": "unary", "clone": "unary", "cos": "unary", 

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

59 "isinf": "unary", "isnan": "unary", 

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

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

62 "sqrt": "unary", "square": "unary", "zeros_like": "unary", 

63 "add": "binary", "add_": "binary", "addcdiv_": "binary", 

64 "addcmul_": "binary", "div": "binary", "lerp_": "binary", 

65 "mul": "binary", "mul_": "binary", "pow": "binary", 

66 "sub": "binary", "__rsub__": "binary", "__rpow__": "binary", 

67 "true_divide": "binary", 

68} 

69 

70_RAGGED_INPLACE_ELEMENTWISE_OPS = frozenset({ 

71 "add_", 

72 "addcdiv_", 

73 "addcmul_", 

74 "lerp_", 

75 "mul_", 

76}) 

77 

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

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

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

81_RAGGED_METADATA_BYPASS_OPS = frozenset({ 

82 "requires_grad_", 

83 "__get__", 

84 "__set__", 

85 "register_hook", 

86 "_has_compatible_shallow_copy_type", 

87 "is_complex", 

88 "is_floating_point", 

89 "is_contiguous", 

90}) 

91 

92 

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

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

95 return _no_skip_ops.get() 

96 

97 

98def get_dtensor_dispatch() -> bool: 

99 """ 

100 Get the current DTensor dispatch status. 

101 

102 Returns: 

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

104 """ 

105 return not _dtensor_dispatch_disabled.get() 

106 

107 

108def _stringify_cache_value(value) -> str: 

109 """Return the cached compact string when available.""" 

110 compact_str = getattr(value, '_compact_str', None) 

111 return compact_str if compact_str is not None else str(value) 

112 

113 

114class LayoutCacheKey: 

115 """Immutable layout cache key.""" 

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

117 

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

119 self._tuple = tuple(layout_ids) 

120 self._hash = hash(self._tuple) 

121 

122 @classmethod 

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

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

125 

126 Args: 

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

128 

129 Returns: 

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

131 """ 

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

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

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

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

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

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

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

139 return cls([_stringify_cache_value(value) for value in cache_values]) 

140 

141 def __eq__(self, other): 

142 if not isinstance(other, LayoutCacheKey): 

143 return False 

144 return self._tuple == other._tuple 

145 

146 def __hash__(self): 

147 return self._hash 

148 

149 def __repr__(self): 

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

151 

152 

153class LayoutCacheManager: 

154 """ 

155 Cache layout in infer layout. 

156 

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

158 It caches the inferred layouts and operation implementations to avoid 

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

160 """ 

161 _instance = None 

162 

163 def __init__(self): 

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

165 atexit.register(self.clear_cache) 

166 

167 @classmethod 

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

169 """ 

170 Get the singleton instance of LayoutCacheManager. 

171 

172 Returns: 

173 LayoutCacheManager: The singleton instance. 

174 """ 

175 if cls._instance is None: 

176 cls._instance = LayoutCacheManager() 

177 return cls._instance 

178 

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

180 """ 

181 Get the layout cache dictionary. 

182 

183 Returns: 

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

185 operation names to their layout caches. 

186 """ 

187 return self.layout_cache 

188 

189 @staticmethod 

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

191 """ 

192 Get the distributed operation implementation by name. 

193 

194 Args: 

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

196 

197 Returns: 

198 Any: The distributed operation class or implementation. 

199 """ 

200 op = get_distributed_op(op_name) 

201 return op 

202 

203 def clear_cache(self) -> None: 

204 """ 

205 Clear all cached layouts. 

206 

207 This method is automatically registered with atexit to ensure 

208 cache is cleared when the program exits. 

209 """ 

210 self.layout_cache.clear() 

211 

212 

213class OpDispatcher: 

214 """ 

215 OpDispatcher 

216 """ 

217 

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

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

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

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

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

223 _INPLACE_BYPASS_OPS = frozenset( 

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

225 

226 def __init__(self): 

227 self.yaml_dir = _DISTRIBUTED_OPS_YAML_DIR 

228 

229 self.layout_infer_ops = self.safe_load_yaml_from_dir() 

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

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

232 "DistCommIrecv", "DistCommBroadcast", "DistCommAllReduce", "DistCommAllGather", 

233 "DistCommBatchIsendIrecv", 

234 "DistCommReduceScatter", "requires_grad_", "item", "__get__", "__set__", 

235 "register_hook", 

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

237 "_has_compatible_shallow_copy_type", "is_floating_point", "is_contiguous", 

238 "get_device"}) 

239 

240 self._random_ops = { 

241 "normal_", "uniform_", "bernoulli", "bernoulli_", 

242 "native_dropout", "rand", "rand_like", "randn", 

243 "randn_like", "randint_like", "kaiming_uniform_", 

244 "multinomial", 

245 } 

246 self._rng_tracker: Optional[OffsetBasedRNGTracker] = None 

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

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

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

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

251 self._non_loss_ops: set = set() 

252 

253 self._register_distributed_ops() 

254 

255 def _register_distributed_ops(self): 

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

257 self._register_single_distributed_op(op_name, config) 

258 

259 @staticmethod 

260 @lru_cache(maxsize=None) 

261 def _resolve_distributed_op_module(module_file: str) -> str: 

262 """Resolve a distributed-op module after validating its package source. 

263 

264 Args: 

265 module_file: Simple module name declared by a package-owned YAML file. 

266 

267 Returns: 

268 Fully qualified module name within the built-in distributed-op package. 

269 

270 Raises: 

271 ValueError: If module_file is not a simple Python identifier. 

272 ImportError: If the module cannot be resolved from the trusted ops directory. 

273 """ 

274 if not isinstance(module_file, str) or not module_file.isidentifier(): 

275 raise ValueError(f"Invalid distributed op module name: {module_file!r}") 

276 

277 module_name = f"{_DISTRIBUTED_OPS_PACKAGE}.{module_file}" 

278 spec = importlib.util.find_spec(module_name) 

279 if spec is None or spec.origin is None: 

280 raise ImportError(f"Distributed op module cannot be resolved: {module_name}") 

281 

282 module_origin = os.path.realpath(spec.origin) 

283 try: 

284 common_path = os.path.commonpath((_DISTRIBUTED_OPS_DIR, module_origin)) 

285 except ValueError as exc: 

286 raise ImportError(f"Distributed op module has an untrusted source: {module_name}") from exc 

287 if common_path != _DISTRIBUTED_OPS_DIR: 

288 raise ImportError(f"Distributed op module has an untrusted source: {module_name}") 

289 return module_name 

290 

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

292 """Import and register a package-owned distributed operator implementation.""" 

293 if "distributed_op_module" in config: 

294 raise ValueError("distributed_op_module is not supported; use distributed_op_file") 

295 

296 class_name = config["distributed_op_class"] 

297 if not isinstance(class_name, str) or not class_name.isidentifier(): 

298 raise ValueError(f"Invalid distributed op class name: {class_name!r}") 

299 

300 module_file = config["distributed_op_file"] 

301 module_name = self._resolve_distributed_op_module(module_file) 

302 module = importlib.import_module(module_name) 

303 op_class = getattr(module, class_name) 

304 if not isinstance(op_class, type) or not issubclass(op_class, DistributedOp): 

305 raise TypeError(f"Distributed op class must inherit DistributedOp: {class_name}") 

306 _ = op_class(op_name) 

307 

308 @staticmethod 

309 def _merge_default(config: dict): 

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

311 if "__default__" not in config: 

312 return config 

313 

314 default_cfg = config["__default__"] 

315 merged = {} 

316 

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

318 if op_name == "__default__": 

319 continue 

320 

321 new_cfg = default_cfg.copy() 

322 new_cfg.update(op_cfg) 

323 merged[op_name] = new_cfg 

324 

325 return merged 

326 

327 def safe_load_yaml_from_dir(self) -> dict: 

328 """ 

329 Load yaml dictionary from directory. 

330 

331 Returns: 

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

333 """ 

334 yaml_dict = {} 

335 yaml_path = self.yaml_dir 

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

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

338 

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

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

341 yaml_data = yaml.safe_load(f) 

342 

343 yaml_data = OpDispatcher._merge_default(yaml_data) 

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

345 if name in yaml_dict: 

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

347 yaml_dict[name] = data 

348 

349 return yaml_dict 

350 

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

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

353 first_arg = next( 

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

355 None, 

356 ) 

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

358 if first_arg is None: 

359 return op_call(*args, **kwargs) 

360 

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

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

363 first_local_arg = first_arg.to_local() 

364 

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

366 self._rng_tracker = OffsetBasedRNGTracker() 

367 

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

369 if ( 

370 self._rng_tracker is not None 

371 and not first_local_arg.is_meta 

372 and self._rng_tracker.distribute_region_enabled 

373 ): 

374 # pylint: disable=W0212 

375 with self._rng_tracker._distribute_region( 

376 device_mesh=first_arg.device_mesh, 

377 placements=first_arg.placements, 

378 global_shape=first_arg.shape, 

379 generator=maybe_user_generator, 

380 ): 

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

382 else: 

383 if maybe_user_generator is not None: 

384 local_kwargs["generator"] = maybe_user_generator 

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

386 

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

388 

389 @staticmethod 

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

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

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

393 if len(args) >= 4: 

394 return bool(args[3]) 

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

396 

397 @staticmethod 

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

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

400 if op_name == "FuncDropoutExt": 

401 return OpDispatcher._func_dropout_ext_inplace(args, kwargs) 

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

403 return op_name.endswith('_') 

404 

405 @staticmethod 

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

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

408 

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

410 the ATen '_' suffix. ``FuncDropoutExt`` is handled separately because the 

411 same kernel serves both modes via its ``inplace`` argument. 

412 """ 

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

414 return first_arg 

415 mesh = first_arg.device_mesh 

416 placements = first_arg.layout.alias_placements 

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

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

419 return tuple( 

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

421 for r in local_results 

422 ) 

423 if isinstance(local_results, torch.Tensor): 

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

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

426 return local_results 

427 

428 @staticmethod 

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

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

431 

432 Args: 

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

434 

435 Returns: 

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

437 """ 

438 if isinstance(value, DTensor): 

439 return value.to_local() 

440 if isinstance(value, tuple): 

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

442 if isinstance(value, list): 

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

444 return value 

445 

446 @staticmethod 

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

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

449 

450 Args: 

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

452 

453 Returns: 

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

455 """ 

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

457 

458 @staticmethod 

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

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

461 

462 Args: 

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

464 

465 Returns: 

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

467 """ 

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

469 

470 @staticmethod 

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

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

473 if isinstance(value, DTensor): 

474 return [value] 

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

476 return list(chain.from_iterable( 

477 OpDispatcher._collect_dtensors(item) for item in value 

478 )) 

479 if isinstance(value, dict): 

480 return list(chain.from_iterable( 

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

482 )) 

483 return [] 

484 

485 def _validate_ragged_dispatch( 

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

487 ) -> Optional[DTensor]: 

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

489 reference = next( 

490 ( 

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

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

493 ), 

494 None, 

495 ) 

496 if reference is None: 

497 return None 

498 if op_name not in _RAGGED_ELEMENTWISE_OPS: 

499 raise RuntimeError( 

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

501 ) 

502 return reference 

503 

504 def _dispatch_ragged_elementwise( 

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

506 reference: DTensor, 

507 ) -> DTensor: 

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

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

510 local_kwargs = self._unwrap_kwargs(kwargs) 

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

512 op_name = get_op_name(op_call) 

513 if op_name in _RAGGED_INPLACE_ELEMENTWISE_OPS: 

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

515 raise ValueError( 

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

517 ) 

518 return args[0] 

519 return DTensor.from_local_with_layout( 

520 py_output, 

521 copy.deepcopy(reference.layout), 

522 shape=tuple(reference.shape), 

523 ) 

524 

525 @staticmethod 

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

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

528 

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

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

531 

532 Args: 

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

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

535 

536 Returns: 

537 Tuple of (unwrapped_args, unwrapped_kwargs) with DTensor values 

538 replaced by their full tensor representations. 

539 

540 Warning: 

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

542 Operators without layout implementations should be registered properly. 

543 """ 

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

545 if isinstance(value, DTensor): 

546 return value.full_tensor() 

547 if isinstance(value, tuple): 

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

549 if isinstance(value, list): 

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

551 return value 

552 

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

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

555 

556 warnings.warn( 

557 "Operator has no distributed layout implementation. " 

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

559 "Consider registering a proper distributed operator.", 

560 UserWarning, 

561 stacklevel=4 

562 ) 

563 

564 return gathered_args, gathered_kwargs 

565 

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

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

568 

569 Args: 

570 op_name: Canonical operator name from get_op_name(). 

571 

572 Returns: 

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

574 """ 

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

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

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

578 return True 

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

580 

581 @staticmethod 

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

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

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

585 return 

586 first = args[0] 

587 if len(args) >= 2: 

588 second = args[1] 

589 elif "other" in kwargs: 

590 second = kwargs["other"] 

591 else: 

592 return 

593 if not isinstance(first, DTensor): 

594 return 

595 mesh_ndim = len(first.layout.partial) 

596 first_partial = tuple(first.layout.partial) 

597 if isinstance(second, DTensor): 

598 second_partial = tuple(second.layout.partial) 

599 if len(second_partial) != mesh_ndim: 

600 raise ValueError( 

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

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

603 ) 

604 else: 

605 second_partial = (None,) * mesh_ndim 

606 if first_partial != second_partial: 

607 raise ValueError( 

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

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

610 ) 

611 

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

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

614 

615 Args: 

616 op_name: Canonical operator name from get_op_name(). 

617 

618 Returns: 

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

620 """ 

621 return is_loss_parallel_active() and is_loss_parallel_op(op_name) 

622 

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

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

625 

626 Args: 

627 op_name: Canonical operator name. 

628 args: Positional arguments for op_call. 

629 kwargs: Keyword arguments for op_call. 

630 

631 Raises: 

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

633 with vocab-sharded DTensor input. 

634 """ 

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

636 return 

637 

638 has_vocab_sharded_dtensor = False 

639 for arg in args: 

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

641 has_vocab_sharded_dtensor = True 

642 break 

643 if not has_vocab_sharded_dtensor: 

644 for val in kwargs.values(): 

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

646 has_vocab_sharded_dtensor = True 

647 break 

648 

649 if has_vocab_sharded_dtensor: 

650 raise ValueError( 

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

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

653 f"For example, replace:\n" 

654 f" with loss_parallel():\n" 

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

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

657 f"with:\n" 

658 f" with loss_parallel():\n" 

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

660 ) 

661 

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

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

664 

665 Args: 

666 op_call: The raw operator callable. 

667 args: Positional arguments for op_call. 

668 kwargs: Keyword arguments for op_call. 

669 

670 Returns: 

671 Result of the distributed cross_entropy computation. 

672 """ 

673 return distributed_cross_entropy_from_op_call(op_call, args, kwargs) 

674 

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

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

677 

678 Args: 

679 op_name: Canonical operator name. 

680 args: Positional arguments for op_call. 

681 

682 Raises: 

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

684 """ 

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

686 return 

687 

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

689 return 

690 

691 logits = args[0] 

692 if _is_shard_on_last_dim(logits): 

693 raise ValueError( 

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

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

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

697 f" with loss_parallel():\n" 

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

699 f" loss.backward()\n" 

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

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

702 ) 

703 

704 def _handle_unregistered_op( 

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

706 ): 

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

708 

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

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

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

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

713 

714 Args: 

715 op_name: Canonical operator name. 

716 op_call: The raw operator callable. 

717 args: Positional arguments for op_call. 

718 kwargs: Keyword arguments for op_call. 

719 

720 Returns: 

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

722 

723 Raises: 

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

725 """ 

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

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

728 if has_dtensor: 

729 self._check_ce_op_without_loss_parallel_context(op_name, args) 

730 

731 if not is_loss_parallel_op(op_name): 

732 raise RuntimeError( 

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

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

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

736 ) 

737 

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

739 

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

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

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

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

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

745 logits = gathered_args[0] 

746 targets = gathered_args[1] 

747 if isinstance(logits, torch.Tensor) and isinstance(targets, torch.Tensor): 

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

749 vocab_size = logits.shape[-1] 

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

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

752 

753 return op_call(*gathered_args, **gathered_kwargs) 

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

755 

756 def _dispatch_layout_infer( 

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

758 ): 

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

760 

761 Args: 

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

763 op_call: The raw operator callable. 

764 args: Positional arguments for op_call. 

765 kwargs: Keyword arguments for op_call. 

766 

767 Returns: 

768 DTensor: Dispatched result wrapped as DTensor. 

769 

770 Raises: 

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

772 """ 

773 if op_name not in self.layout_infer_ops: 

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

775 

776 cache_manager = LayoutCacheManager.get_instance() 

777 distribute_op = cache_manager.distributed_op(op_name) 

778 

779 result = distribute_op.preprocess(args, kwargs) 

780 if result is None: 

781 raise RuntimeError( 

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

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

784 ) 

785 local_args, local_kwargs, cache_values = result 

786 cache_key = LayoutCacheKey.from_cache_values(cache_values) 

787 

788 infer_result, op_impl = OpDispatcher._lookup_or_infer_layout( 

789 op_call, op_name, cache_key, cache_values, distribute_op, cache_manager 

790 ) 

791 

792 op_impl = op_call if op_impl is None else op_impl 

793 py_output = op_impl(*local_args, **local_kwargs) 

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

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

796 

797 @staticmethod 

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

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

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

801 return args[0] 

802 return output 

803 

804 @staticmethod 

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

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

807 

808 Returns: 

809 (infer_result, op_impl) 

810 """ 

811 layout_cache = cache_manager.get_layout_cache() 

812 if func_name not in layout_cache: 

813 layout_cache[func_name] = {} 

814 op_layout_cache = layout_cache[func_name] 

815 if cache_key in op_layout_cache: 

816 return op_layout_cache[cache_key] 

817 infer_result = distribute_op.infer_layout(cache_values) 

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

819 op_layout_cache[cache_key] = (infer_result, op_impl) 

820 return infer_result, op_impl 

821 

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

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

824 

825 Args: 

826 op_call: The raw operator callable. 

827 args: Positional arguments for op_call. 

828 kwargs: Keyword arguments for op_call. 

829 

830 Returns: 

831 Result of the dispatched op call. 

832 """ 

833 op_name = get_op_name(op_call) 

834 if logger.isEnabledFor(logging.DEBUG): 

835 log_dispatch_enter(op_name, args, kwargs) 

836 

837 observer = _debug_mode_observer.get() 

838 if observer is not None: 

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

840 

841 result = None 

842 try: 

843 should_bypass = self._should_bypass_dispatch(op_name) 

844 ragged_dtensor = None 

845 if not should_bypass or op_name not in _RAGGED_METADATA_BYPASS_OPS: 

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

847 if ragged_dtensor is not None: 

848 result = self._dispatch_ragged_elementwise( 

849 op_call, args, kwargs, ragged_dtensor 

850 ) 

851 return result 

852 

853 if should_bypass: 

854 self._validate_inplace_partial_inputs(op_name, args, kwargs) 

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

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

857 result = args[0] 

858 return result 

859 

860 if op_name in self._random_ops: 

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

862 return result 

863 

864 self._check_decomposed_ce_op_in_loss_parallel(op_name, args, kwargs) 

865 

866 if self._should_dispatch_loss_parallel(op_name): 

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

868 return result 

869 

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

871 self.layout_infer_ops[op_name] = {} 

872 

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

874 return result 

875 finally: 

876 if logger.isEnabledFor(logging.DEBUG): 

877 log_dispatch_exit(op_name, result) 

878 

879 if observer is not None: 

880 observer.on_op_dispatch_exit(op_name, result) 

881 

882_OP_DISPATCHER = OpDispatcher()