Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / platform / mindspore / activation_checkpoint / activation_swap.py: 69%

209 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# Adapted from 

16# hyper_parallel/platform/torch/activation_checkpoint/activation_swap.py 

17# adapted for MindSpore Cell API. 

18# ============================================================================ 

19"""Activation Swap Wrapper implementation for MindSpore.""" 

20from abc import ABC, abstractmethod 

21from collections.abc import Iterator 

22from typing import Optional, Callable, Any, Union 

23import types 

24import warnings 

25import mindspore as ms 

26from mindspore import Tensor 

27from mindspore.common.parameter import Parameter 

28from mindspore.nn import Cell 

29 

30 

31_CKPT_WRAPPED_MODULE = "_ckpt_wrapped_module" 

32 

33 

34def _strip_ckpt_wrapped_module_prefix(name: str) -> str: 

35 """Remove the wrapper cell segment from a dotted MindSpore cell name.""" 

36 return ".".join(part for part in name.split(".") if part != _CKPT_WRAPPED_MODULE) 

37 

38 

39class FuncCell(Cell): 

40 """ 

41 Thin :class:`~mindspore.nn.Cell` adapter that wraps a plain callable. 

42 

43 Allows ordinary Python functions (or any callable without Cell 

44 parameters) to be passed to :func:`checkpoint_wrapper` and 

45 :func:`swap_wrapper` in place of a :class:`~mindspore.nn.Cell`. 

46 The wrapped function is stored as ``_fn`` and invoked in 

47 :meth:`construct`; the cell has no trainable parameters. 

48 

49 Args: 

50 fn (callable): The function to wrap. 

51 

52 Example: 

53 >>> wrapped = checkpoint_wrapper(lambda x: x * 2) 

54 """ 

55 

56 def __init__(self, fn: Callable): 

57 super().__init__() 

58 self._fn = fn 

59 

60 def construct(self, *args, **kwargs): 

61 """Delegate to the wrapped function.""" 

62 return self._fn(*args, **kwargs) 

63 

64 

65def _is_shared_function_callable(callable_obj: Callable) -> bool: 

66 """Return True for stateless function objects commonly shared by modules.""" 

67 return isinstance(callable_obj, (types.FunctionType, types.BuiltinFunctionType, types.MethodType)) 

68 

69 

70def _iter_wrappable_callable_attrs(module: Cell) -> Iterator[tuple[str, Callable]]: 

71 """Yield public per-instance callable attributes not registered as child cells. 

72 

73 Plain functions, builtins and bound methods are skipped: these are stateless 

74 module-level utilities shared by reference across many modules (e.g. 

75 ``self.reshape = mint.reshape`` / ``self.cast = ops.cast`` repeated in every 

76 layer). They are never standalone checkpoint regions, and marking a shared 

77 function's ``_is_wrapped`` flag would both mutate a global object and falsely 

78 flag every sibling module that references the same function as an overlapping 

79 wrap. Only per-instance callables (e.g. MindSpore ``Primitive`` objects) 

80 participate in overlap tracking. 

81 """ 

82 for attr_name, attr_value in vars(module).items(): 

83 if attr_name.startswith("_") or isinstance(attr_value, Cell): 

84 continue 

85 if _is_shared_function_callable(attr_value): 

86 continue 

87 if callable(attr_value): 

88 yield attr_name, attr_value 

89 

90 

91def _mark_wrapped(obj: Any) -> None: 

92 try: 

93 obj._is_wrapped = True # pylint: disable=W0212 

94 except (AttributeError, TypeError): 

95 pass 

96 

97 

98def _get_wrapped_callable(cell: Cell) -> Optional[Callable]: 

99 wrapped_module = getattr(cell, _CKPT_WRAPPED_MODULE, None) 

100 if isinstance(wrapped_module, FuncCell): 

101 return getattr(wrapped_module, "_fn", None) 

102 if isinstance(cell, FuncCell): 

103 return getattr(cell, "_fn", None) 

104 return None 

105 

106 

107def _raise_callable_already_wrapped(callable_obj: Callable) -> None: 

108 warnings.warn( 

109 f"Callable '{callable_obj.__class__.__name__}' is already wrapped. " 

110 "Wrapping overlapping module regions is not allowed." 

111 ) 

112 

113 

114def _check_callable_attr_not_wrapped(owner: Cell, attr_name: str, attr_value: Callable) -> None: 

115 del owner, attr_name 

116 if getattr(attr_value, '_is_wrapped', False): 

117 _raise_callable_already_wrapped(attr_value) 

118 

119 

120def _check_and_mark_callable(callable_obj: Callable) -> None: 

121 if _is_shared_function_callable(callable_obj): 

122 return 

123 if getattr(callable_obj, '_is_wrapped', False): 

124 warnings.warn( 

125 f"Callable '{callable_obj.__class__.__name__}' or one of its ancestors is already wrapped. " 

126 "Wrapping overlapping module regions is not allowed." 

127 ) 

128 _mark_wrapped(callable_obj) 

129 

130 

131def _check_and_mark_wrapped(module: Cell) -> None: 

132 """Validate no wrapping overlap, then mark module and all descendants as wrapped. 

133 

134 Raises: 

135 ValueError: If ``module`` or any of its descendants is already wrapped. 

136 """ 

137 if getattr(module, '_is_wrapped', False): 

138 warnings.warn( 

139 f"Module '{module.__class__.__name__}' or one of its ancestors is already wrapped. " 

140 "Wrapping overlapping module regions is not allowed." 

141 ) 

142 for _, submodule in module.cells_and_names(): 

143 if submodule is module: 

144 continue 

145 wrapped_callable = _get_wrapped_callable(submodule) 

146 if wrapped_callable is not None and _is_shared_function_callable(wrapped_callable): 

147 continue 

148 if getattr(submodule, '_is_wrapped', False): 

149 if wrapped_callable is not None: 

150 _raise_callable_already_wrapped(wrapped_callable) 

151 warnings.warn( 

152 f"Submodule '{getattr(submodule, '_ckpt_wrapped_module', submodule).__class__.__name__}' of " 

153 f"'{module.__class__.__name__}' is already wrapped. " 

154 "Wrapping overlapping module regions is not allowed." 

155 ) 

156 for _, submodule in module.cells_and_names(): 

157 for attr_name, attr_value in _iter_wrappable_callable_attrs(submodule): 

158 _check_callable_attr_not_wrapped(submodule, attr_name, attr_value) 

159 for _, submodule in module.cells_and_names(): 

160 _mark_wrapped(submodule) 

161 for _, attr_value in _iter_wrappable_callable_attrs(submodule): 

162 _mark_wrapped(attr_value) 

163 

164 

165class ActivationWrapper(Cell, ABC): 

166 """ 

167 Base class for Activation Checkpoint Wrapper in MindSpore. 

168 

169 Wraps a :class:`mindspore.nn.Cell` and forwards attribute lookups, 

170 parameter iteration, and indexing to the inner cell. Concrete 

171 sub-classes must implement :meth:`construct`. 

172 

173 Not meant to be instantiated directly. 

174 """ 

175 

176 def __init__(self, module: Union[Cell, Callable]): 

177 if callable(module) and not isinstance(module, Cell): 

178 _check_and_mark_callable(module) 

179 module = FuncCell(module) 

180 _mark_wrapped(module) 

181 else: 

182 _check_and_mark_wrapped(module) 

183 super().__init__(auto_prefix=False) 

184 self._ckpt_wrapped_module = module 

185 self._is_wrapped = True 

186 self._wrapped_param_names = { 

187 id(param): param.name for _, param in module.parameters_and_names() 

188 } 

189 

190 @property 

191 def _wrapped_module(self): 

192 return self._ckpt_wrapped_module 

193 

194 @abstractmethod 

195 def construct(self, *args, **kwargs): 

196 """Abstract construct method — subclasses must override.""" 

197 raise ValueError("Subclasses should implement construct().") 

198 

199 def __getattr__(self, name: str) -> Any: 

200 """Forward missing attributes to the wrapped cell. 

201 

202 .. warning:: 

203 Do **not** call ``super().__getattr__(name)`` here. 

204 MindSpore's ``Cell.__init__`` calls ``hasattr(self, "bprop")`` at 

205 line 252 of ``cell.py`` *after* ``_cells`` is initialised as an 

206 empty ``OrderedDict`` but *before* ``ActivationWrapper.__init__`` 

207 has registered ``_ckpt_wrapped_module`` into ``_cells``. The 

208 PyTorch ``nn.Module.__init__`` is pure Python and never calls 

209 ``hasattr`` on ``self``, so this issue does not arise there. 

210 

211 Using ``super().__getattr__`` here would raise ``AttributeError`` 

212 (``_ckpt_wrapped_module`` not yet in ``_cells``), the fallback 

213 ``getattr(self._ckpt_wrapped_module, name)`` would access 

214 ``self._ckpt_wrapped_module`` — triggering another 

215 ``__getattr__("_ckpt_wrapped_module")`` — and the cycle repeats 

216 as infinite recursion. 

217 

218 Instead we replicate ``Cell.__getattr__``'s own dict-probe logic 

219 and fall through to the wrapped module only when it is already 

220 registered. 

221 """ 

222 for attr_dict in ('_params', '_buffers', '_cells', '_params_list'): 

223 d = self.__dict__.get(attr_dict) 

224 if d is not None and name in d: 

225 return d[name] 

226 cells = self.__dict__.get('_cells', {}) 

227 wrapped = cells.get(_CKPT_WRAPPED_MODULE) 

228 if wrapped is not None: 

229 return getattr(wrapped, name) 

230 raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'") 

231 

232 @property 

233 def unwrap_cell(self) -> Cell: 

234 """Recursively return the innermost wrapped cell.""" 

235 return self._ckpt_wrapped_module 

236 

237 def __getitem__(self, key: int) -> Any: 

238 """Forward indexing calls in case the wrapped cell is a SequentialCell.""" 

239 return self._ckpt_wrapped_module.__getitem__(key) # type: ignore[operator] 

240 

241 def cells_and_names(self, cells=None, name_prefix=''): 

242 """ 

243 Return wrapped cells without exposing the wrapper storage prefix. 

244 

245 MindSpore registers ``_ckpt_wrapped_module`` as a real child cell, so 

246 the default :meth:`Cell.cells_and_names` would expose names such as 

247 ``layer._ckpt_wrapped_module.attn``. Strip that implementation detail 

248 so downstream code sees the same names as it would for the unwrapped 

249 model. 

250 """ 

251 for cell_name, cell in super().cells_and_names(cells, name_prefix): 

252 yield _strip_ckpt_wrapped_module_prefix(cell_name), cell 

253 

254 def parameters_and_names( 

255 self, 

256 name_prefix: str = '', 

257 expand: bool = True, 

258 ) -> Iterator[tuple[str, Parameter]]: 

259 """ 

260 Override :meth:`parameters_and_names` to strip the wrapper prefix. 

261 

262 Removes all occurrences of ``_ckpt_wrapped_module.`` from parameter 

263 names so that a checkpoint saved from this wrapper is compatible with 

264 the unwrapped cell. 

265 

266 Args: 

267 name_prefix (str): Prefix prepended to every parameter name. 

268 expand (bool): Whether to recursively expand sub-cells. 

269 

270 Yields: 

271 tuple[str, Parameter]: ``(name, parameter)`` pairs with the 

272 wrapper prefix removed. 

273 """ 

274 for param_name, param in super().parameters_and_names(name_prefix, expand): 

275 yield _strip_ckpt_wrapped_module_prefix(param_name), param 

276 

277 def update_parameters_name(self, prefix='', recurse=True): 

278 """ 

279 Update wrapped parameter names without collapsing existing full paths. 

280 

281 When a wrapper replaces an already-registered child cell, the wrapped 

282 parameters usually already have globally unique names such as 

283 ``0.attn.qkv.weight``. MindSpore will still call 

284 ``wrapper.update_parameters_name("attn.")`` during reassignment; if we 

285 blindly apply that prefix again through the wrapper view, those names 

286 are rewritten to ``attn.qkv.weight`` and collide across layers. 

287 

288 For parameters that already contain the requested child prefix in their 

289 existing full name, keep the current name unchanged. For fresh 

290 standalone modules that only have local names like ``qkv.weight``, 

291 synthesize the prefixed name as usual. 

292 """ 

293 if prefix is None: 

294 prefix = '' 

295 for local_name, param in self._ckpt_wrapped_module.parameters_and_names(expand=recurse): 

296 original_name = self._wrapped_param_names.get(id(param), param.name) 

297 if prefix and (original_name.startswith(prefix) or f".{prefix}" in original_name): 

298 new_name = original_name 

299 elif prefix: 

300 new_name = prefix + local_name 

301 else: 

302 new_name = local_name 

303 if new_name != param.name: 

304 param.is_init = False 

305 param.name = new_name 

306 self._wrapped_param_names[id(param)] = new_name 

307 

308 

309def base_check_fn(tensor: Any) -> bool: 

310 """ 

311 Basic eligibility check: returns ``True`` when *tensor* may be offloaded. 

312 

313 Skips: 

314 

315 * Non-tensor objects. 

316 * :class:`~mindspore.common.parameter.Parameter` objects. 

317 * Empty tensors (zero elements). 

318 

319 Args: 

320 tensor: The value to test. 

321 

322 Returns: 

323 bool: ``True`` if the tensor is eligible for CPU offloading. 

324 """ 

325 if not isinstance(tensor, Tensor): 

326 return False 

327 if tensor.param_info is not None: 

328 return False 

329 if tensor.untyped_storage().size() == 0: 

330 return False 

331 return True 

332 

333 

334def _normalize_device(device: str) -> str: 

335 if ":" in device: 

336 return device.split(":", maxsplit=1)[0] 

337 return device 

338 

339 

340class AsyncSaveOnCpu(ms.saved_tensors_hooks): 

341 """ 

342 Context manager to offload tensors to CPU during forward pass. 

343 """ 

344 def __init__(self, policy_fn=None, group_swap: bool = False) -> None: 

345 # pylint: disable=C0415 

346 from hyper_parallel.core.activation_checkpoint.activation_checkpoint import CheckpointPolicy 

347 from hyper_parallel.core.activation_checkpoint.swap import Storage, SwapManager, SwapTensor 

348 self.add_to_storage = False 

349 self.storage = Storage() 

350 self.count_idx = 0 

351 self.policy_fn = policy_fn 

352 

353 

354 # Cache per-context-manager state once to avoid per-tensor singleton lookups. 

355 swap_manager = SwapManager() 

356 

357 def pack_to_cpu(tensor: ms.Tensor): 

358 if not base_check_fn(tensor): 

359 return tensor 

360 if policy_fn is not None: 

361 if policy_fn(tensor) == CheckpointPolicy.MUST_SAVE: 

362 return tensor 

363 if policy_fn(tensor) != CheckpointPolicy.MUST_SWAP: 

364 raise RuntimeError(f"Swap :set an invalid policy {policy_fn(tensor)}") 

365 group_name = swap_manager.get_current_group_name() 

366 if not group_name: 

367 return tensor 

368 if not self.add_to_storage: 

369 swap_manager.add_storage(group_name, self.storage) 

370 self.add_to_storage = True 

371 funcname = f"{group_name}::{tensor.shape}" 

372 self.storage[self.count_idx].append( 

373 SwapTensor(tensor, funcname, group_swap=group_swap) 

374 ) 

375 self.count_idx += 1 

376 return tensor 

377 

378 def unpack_from_cpu(tensor) -> ms.Tensor: 

379 if self.storage is not None: 

380 self.storage.clear() 

381 self.storage = None 

382 return tensor 

383 

384 super().__init__(pack_to_cpu, unpack_from_cpu) 

385 

386 

387class SwapWrapper(ActivationWrapper): 

388 """ 

389 MindSpore counterpart of :class:`~hyper_parallel.platform.torch 

390 .activation_checkpoint.activation_swap.SwapWrapper`. 

391 

392 Wraps a :class:`~mindspore.nn.Cell` and applies async activation swap 

393 during the forward pass via the platform's ``async_save_on_cpu`` context 

394 manager. Falls back to a no-op context when that context is not yet 

395 available on the current platform. 

396 

397 Args: 

398 mod (Cell): The cell whose intermediate activations should be swapped. 

399 policy_fn (callable, optional): Per-tensor swap policy; see 

400 :class:`AsyncSaveOnCpu`. 

401 

402 Example: 

403 >>> from hyper_parallel.platform.mindspore.activation_checkpoint import swap_wrapper 

404 >>> model.layers[i].attn = swap_wrapper(model.layers[i].attn, policy_fn) 

405 """ 

406 

407 def __init__( 

408 self, 

409 mod: Union[Cell, Callable], 

410 policy_fn: Optional[Callable] = None, 

411 group_swap: bool = False, 

412 ): 

413 super().__init__(mod) 

414 self.policy_fn = policy_fn 

415 self.group_swap = group_swap 

416 

417 def construct(self, *args, **kwargs): 

418 """Execute the wrapped module inside an async CPU-swap context.""" 

419 with AsyncSaveOnCpu(policy_fn=self.policy_fn, group_swap=self.group_swap): 

420 return self._ckpt_wrapped_module(*args, **kwargs) 

421 

422 

423def swap_wrapper( 

424 module: Union[Cell, Callable], 

425 policy_fn: Optional[Callable] = None, 

426 group_swap: bool = False, 

427) -> SwapWrapper: 

428 """ 

429 Wrap *module* with async activation swap. 

430 

431 Args: 

432 module (Cell or callable): The cell or plain function to wrap. 

433 If a plain callable is passed it is automatically wrapped in a 

434 :class:`FuncCell` before being stored. 

435 policy_fn (callable, optional): Per-tensor swap policy; see 

436 :class:`AsyncSaveOnCpu`. 

437 

438 Returns: 

439 SwapWrapper: The wrapped cell with activation swap enabled. 

440 """ 

441 return SwapWrapper(module, policy_fn, group_swap) 

442 

443 

444def swap_tensor_wrapper(target, tag: Optional[str] = None, group_swap: bool = False): 

445 """Register selected tensors into the current swap group. 

446 

447 This helper is intended to be used inside a forward path that already 

448 participates in the existing swap scheduling managed by ``SwapManager``. 

449 It preserves the input structure and returns the original tensors. 

450 """ 

451 # pylint: disable=C0415 

452 from hyper_parallel.core.activation_checkpoint.swap import Storage, SwapManager, SwapTensor 

453 swap_manager = SwapManager() 

454 group_name = swap_manager.get_current_group_name() 

455 if not group_name: 

456 warnings.warn( 

457 f"Tensor {tag} cannot be swapped, for its group is unregistered." 

458 ) 

459 return target 

460 if swap_manager.is_last_group(group_name): 

461 return target 

462 

463 storage = Storage() 

464 count_idx = 0 

465 

466 def _apply(x): 

467 nonlocal count_idx 

468 if isinstance(x, Tensor) and base_check_fn(x): 

469 tensor_tag = tag or f"{group_name}_swap_tensor" 

470 funcname = f"{tensor_tag}::{tuple(x.shape)}" 

471 storage[count_idx].append(SwapTensor(x, funcname, group_swap=group_swap)) 

472 count_idx += 1 

473 return x 

474 

475 def _map(tree): 

476 if isinstance(tree, dict): 

477 return type(tree)((k, _map(v)) for k, v in tree.items()) 

478 if isinstance(tree, tuple): 

479 return tuple(_map(v) for v in tree) 

480 if isinstance(tree, list): 

481 return [_map(v) for v in tree] 

482 return _apply(tree) 

483 

484 wrapped = _map(target) 

485 if count_idx > 0: 

486 swap_manager.add_storage(group_name, storage) 

487 return wrapped