Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / core / activation_memory / sac.py: 98%

130 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-10-25 05:45 +0800

1# Copyright 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 https://github.com/pytorch/pytorch/blob/release/2.6/torch/utils/checkpoint.py 

16# enhanced with selective checkpoint support swap 

17# ============================================================================ 

18"""enhanced with selective checkpoint support swap""" 

19# pylint: disable=W0212, W0613, C0115, C0116, C0103, R1705 

20from collections import defaultdict 

21from functools import partial 

22from typing import Any, Dict, List, Optional, Union 

23 

24import torch 

25import torch.fx.traceback as fx_traceback 

26from torch._functorch._aot_autograd.functional_utils import is_fun 

27from torch.utils._pytree import tree_map 

28from torch.utils._python_dispatch import TorchDispatchMode 

29from hyper_parallel.core.activation_memory.policy import CheckpointPolicy 

30from hyper_parallel.core.activation_memory.swap import ( # patch code 

31 SwapManager, 

32 SwapTensor, 

33 Storage, 

34) 

35 

36 

37def _is_compiling(func, args, kwargs): 

38 # Check if we are under AOTAutograd tracing 

39 # There should probably be a better way to do this... 

40 # NOTE: unify _is_compiling across all compile stacks 

41 for arg in args: 

42 if isinstance(arg, torch.Tensor) and is_fun(arg): 

43 return True 

44 return False 

45 

46 

47class _VersionWrapper: 

48 # Check that cached tensors are not mutated. 

49 def __init__(self, val): 

50 self.val: Union[torch.Tensor, Any] = val 

51 self.version: Optional[int] = ( 

52 val._version if isinstance(val, torch.Tensor) else None 

53 ) 

54 

55 def get_val(self, allow_cache_entry_mutation): 

56 if self.version is not None and not allow_cache_entry_mutation: 

57 if self.val._version != self.version: 

58 # Can we give user a stack trace of where the mutation happened? 

59 raise RuntimeError( 

60 "Tensor cached during selective activation checkpoint has been mutated" 

61 ) 

62 return self.val 

63 

64 

65class _SwapCacheEntry: 

66 """Pair the recompute cache and swap record around the same tensor object.""" 

67 def __init__(self, val, funcname, group_swap=False, cpu_pool=None): 

68 self.save = _VersionWrapper(val) 

69 self.swap = SwapTensor(val, funcname, group_swap=group_swap, cpu_pool=cpu_pool) 

70 

71 

72def _maybe_detach(x, any_ret_has_alias_info): 

73 # We detach for two separate reasons: 

74 # - For view ops, we need to ensure that when the tensor is returned from 

75 # CachedDispatchMode, as_view sees that the AutogradMeta is nullptr 

76 # - Avoid reference cycles 

77 # For case 1, it is not enough to check whether x has differentiable dtype 

78 # because non-differentiable dtype can have non-nullptr AutogradMeta, e.g. 

79 # when the tensor is a view. 

80 need_detach = (isinstance(x, torch.Tensor) 

81 and (x.is_floating_point() or x.is_complex() or any_ret_has_alias_info)) 

82 if need_detach: 

83 with torch._C._SetExcludeDispatchKeyGuard(torch._C.DispatchKey.ADInplaceOrView, False): 

84 # Ensure that view performed beneath autograd properly propagates 

85 # version counter. TODO: Use reentrant_dispatch instead of 

86 # manually manipulating dispatch keys. Using reentrant_dispatch 

87 # would respect inference_mode, though that is not relevant for 

88 # this case. 

89 x = x.detach() 

90 return x 

91 

92 

93class SelectiveCheckpointContext: 

94 """ 

95 Context passed to policy function during selective checkpointing. 

96 

97 This class is used to pass relevant metadata to the policy function during 

98 selective checkpointing. The metadata includes whether the current invocation 

99 of the policy function is during recomputation or not. 

100 

101 Example: 

102 >>> # xdoctest: +SKIP(stub) 

103 >>> 

104 >>> def policy_fn(ctx, op, *args, **kwargs): 

105 >>> print(ctx.is_recompute) 

106 >>> 

107 >>> context_fn = functools.partial(create_selective_checkpoint_contexts, policy_fn) 

108 >>> 

109 >>> out = torch.utils.checkpoint.checkpoint( 

110 >>> fn, x, y, 

111 >>> use_reentrant=False, 

112 >>> context_fn=context_fn, 

113 >>> ) 

114 """ 

115 def __init__(self, *, is_recompute): 

116 self.is_recompute = is_recompute 

117 

118 

119def _policy_from_bool(b): 

120 # For backward compatibility 

121 return CheckpointPolicy.MUST_SAVE if b else CheckpointPolicy.PREFER_RECOMPUTE 

122 

123 

124def _make_swap_entry(x, *, has_alias, funcname, group_swap, cpu_pool): 

125 return _SwapCacheEntry( 

126 _maybe_detach(x, has_alias), funcname, group_swap=group_swap, cpu_pool=cpu_pool 

127 ) 

128 

129 

130SAC_IGNORED_OPS = { 

131 # AC inserts different number of detach during forward and recompute. 

132 torch.ops.aten.detach.default, 

133 # AC's determinism check invokes additional metadata ops during forward. 

134 # With subclasses involved, these metadata ops become dispatchable, this 

135 # can result in incorrectness if these ops are selected cached. 

136 torch.ops.prim.device.default, 

137} | set(torch._subclasses.functional_tensor.FunctionalTensor.metadata_fns) 

138 

139 

140def ignore_sac_ops(ignore_ops: List[Optional[object]]) -> None: 

141 """Add available operators to the selective-checkpoint ignore set. 

142 

143 Args: 

144 ops (List[Optional[object]]): Operators to execute without selective-AC replay accounting. 

145 ``None`` entries are ignored for optional-version compatibility. 

146 """ 

147 SAC_IGNORED_OPS.update(op for op in ignore_ops if op is not None) 

148 

149 

150class _CachingTorchDispatchMode(TorchDispatchMode): 

151 # Used together with _CachedTorchDispatchMode to implement SAC. 

152 def __init__(self, policy_fn, swap_storage, storage, group_swap=False, cpu_pool=None): 

153 self.policy_fn = policy_fn 

154 self.swap_storage = swap_storage 

155 self.storage = storage 

156 self.add_to_storage = False 

157 self.group_swap = group_swap 

158 self.cpu_pool = cpu_pool 

159 # Cache context and singleton to avoid per-dispatch allocation / lookup. 

160 self._swap_manager = SwapManager() 

161 self._group_prefix = "" 

162 

163 def __torch_dispatch__(self, func, types, args=(), kwargs=None): 

164 if func in SAC_IGNORED_OPS: 

165 return func(*args, **kwargs) 

166 

167 kwargs = {} if kwargs is None else kwargs 

168 policy = self.policy_fn(SelectiveCheckpointContext(is_recompute=False), 

169 func, *args, **kwargs) 

170 if isinstance(policy, bool): 

171 policy = _policy_from_bool(policy) 

172 

173 is_compiling = _is_compiling(func, args, kwargs) 

174 

175 if is_compiling: 

176 # Overwrite each node's "recompute" tag to add in the user annotation. 

177 fx_traceback.current_meta["recompute"] = policy 

178 

179 out = func(*args, **kwargs) 

180 

181 has_alias = any(ret.alias_info is not None for ret in func._schema.returns) 

182 

183 if policy in (CheckpointPolicy.MUST_SAVE, CheckpointPolicy.PREFER_SAVE): 

184 self.storage[func].append( 

185 tree_map(lambda x: _VersionWrapper(_maybe_detach(x, has_alias)), out) 

186 ) 

187 elif policy == CheckpointPolicy.MUST_SWAP: # patch code 

188 group_name = self._swap_manager.get_current_group_name() 

189 if not group_name: 

190 # No swap group is active for this region, which is the normal 

191 # case for pipeline chunks the schedule did not mark swappable 

192 # (forward/backward distance below MIN_SWAP_GAP). Degrade to 

193 # MUST_SAVE: cache the activation on device and register it 

194 # nowhere, so the empty-named global group never accumulates 

195 # storage that is neither offloaded nor released. 

196 self._swap_manager.warn_missing_group_once() 

197 self.storage[func].append( 

198 tree_map(lambda x: _VersionWrapper(_maybe_detach(x, has_alias)), out) 

199 ) 

200 else: 

201 if not self.add_to_storage: 

202 self._group_prefix = f"{group_name}::" 

203 self._swap_manager.add_storage(group_name, self.swap_storage) 

204 self.add_to_storage = True 

205 funcname = f"{self._group_prefix}{func}" 

206 entries = tree_map( 

207 partial( 

208 _make_swap_entry, 

209 has_alias=has_alias, 

210 funcname=funcname, 

211 group_swap=self.group_swap, 

212 cpu_pool=self.cpu_pool, 

213 ), 

214 out, 

215 ) 

216 self.storage[func].append(tree_map(lambda x: x.save, entries)) 

217 self.swap_storage[func].append(tree_map(lambda x: x.swap, entries)) 

218 elif policy != CheckpointPolicy.MUST_RECOMPUTE: 

219 raise RuntimeError(f"Checkpoint Activation: {func} encountered an invalid policy {policy}") 

220 return out 

221 

222 

223class _CachedTorchDispatchMode(TorchDispatchMode): 

224 # Used together with _CachingTorchDispatchMode to implement SAC. 

225 def __init__(self, policy_fn, swap_storage, storage, allow_cache_entry_mutation): 

226 self.policy_fn = policy_fn 

227 self.swap_storage = swap_storage 

228 self.storage = storage 

229 self.allow_cache_entry_mutation = allow_cache_entry_mutation 

230 self._swap_cleared = False 

231 

232 def __torch_dispatch__(self, func, types, args=(), kwargs=None): 

233 if func in SAC_IGNORED_OPS: 

234 return func(*args, **kwargs) 

235 

236 kwargs = {} if kwargs is None else kwargs 

237 policy = self.policy_fn(SelectiveCheckpointContext(is_recompute=True), 

238 func, *args, **kwargs) 

239 if isinstance(policy, bool): 

240 policy = _policy_from_bool(policy) 

241 

242 is_compiling = _is_compiling(func, args, kwargs) 

243 

244 if not self._swap_cleared: 

245 self.swap_storage.clear() 

246 self._swap_cleared = True 

247 

248 # MUST_SAVE, PREFER_SAVE, and MUST_SWAP all restore from storage identically. 

249 if (policy in (CheckpointPolicy.MUST_SAVE, CheckpointPolicy.PREFER_SAVE, CheckpointPolicy.MUST_SWAP) 

250 or is_compiling): 

251 storage = self.storage.get(func) # patch code 

252 if storage is None: 

253 raise RuntimeError(f"{func} encountered during backward, but not found in storage") 

254 if len(storage) == 0: 

255 raise RuntimeError( 

256 "Trying to backward an extra time. You are only allowed to backward once " 

257 "on any region computed under selective activation checkpoint." 

258 ) 

259 out = tree_map(lambda x: x.get_val(self.allow_cache_entry_mutation), storage.pop(0)) 

260 else: 

261 out = func(*args, **kwargs) 

262 return out 

263 

264 

265def create_selective_checkpoint_contexts( 

266 policy_fn_or_list, allow_cache_entry_mutation=False, group_swap=False, cpu_pool=None 

267): 

268 """ 

269 Helper to avoid recomputing certain ops during activation checkpointing. 

270 

271 Use this with `torch.utils.checkpoint.checkpoint` to control which 

272 operations are recomputed during the backward pass. 

273 

274 Args: 

275 policy_fn_or_list (Callable or List): 

276 - If a policy function is provided, it should accept a 

277 :class:`SelectiveCheckpointContext`, the :class:`OpOverload`, args and 

278 kwargs to the op, and return a :class:`CheckpointPolicy` enum value 

279 indicating whether the execution of the op should be recomputed or not. 

280 - If a list of operations is provided, it is equivalent to a policy 

281 returning `CheckpointPolicy.MUST_SAVE` for the specified 

282 operations and `CheckpointPolicy.PREFER_RECOMPUTE` for all other 

283 operations. 

284 allow_cache_entry_mutation (bool, optional): By default, an error is 

285 raised if any tensors cached by selective activation checkpoint are 

286 mutated in order to ensure correctness. If set to `True`, this check 

287 is disabled. 

288 Returns: 

289 A tuple of two context managers. 

290 

291 Example: 

292 >>> # xdoctest: +REQUIRES(LINUX) 

293 >>> import functools 

294 >>> 

295 >>> x = torch.rand(10, 10, requires_grad=True) 

296 >>> y = torch.rand(10, 10, requires_grad=True) 

297 >>> 

298 >>> ops_to_save = [ 

299 >>> torch.ops.aten.mm.default, 

300 >>> ] 

301 >>> 

302 >>> def policy_fn(ctx, op, *args, **kwargs): 

303 >>> if op in ops_to_save: 

304 >>> return CheckpointPolicy.MUST_SAVE 

305 >>> else: 

306 >>> return CheckpointPolicy.PREFER_RECOMPUTE 

307 >>> 

308 >>> context_fn = functools.partial(create_selective_checkpoint_contexts, policy_fn) 

309 >>> 

310 >>> # or equivalently 

311 >>> context_fn = functools.partial(create_selective_checkpoint_contexts, ops_to_save) 

312 >>> 

313 >>> def fn(x, y): 

314 >>> return torch.sigmoid(torch.matmul(torch.matmul(x, y), y)) * y 

315 >>> 

316 >>> out = torch.utils.checkpoint.checkpoint( 

317 >>> fn, x, y, 

318 >>> use_reentrant=False, 

319 >>> context_fn=context_fn, 

320 >>> ) 

321 """ 

322 # NB: If grad_mode is disabled, checkpoint would not run forward under 

323 # context_fn anyway, so proceed as usual. 

324 if policy_fn_or_list is None: 

325 def policy_fn(_ctx, _op, *_args, **_kwargs): 

326 return CheckpointPolicy.PREFER_RECOMPUTE 

327 elif isinstance(policy_fn_or_list, list): 

328 for op in policy_fn_or_list: 

329 if not isinstance(op, torch._ops.OpOverload): 

330 _extra_msg = ( 

331 "Please update the OpOverloadPacket to a specific OpOverload." 

332 "For example, if you have `torch.ops.aten.mm`, change it to `torch.ops.aten.mm.default`." 

333 ) if isinstance(op, torch._ops.OpOverloadPacket) else "" 

334 raise ValueError( 

335 f"Expected op in `op_list` to be an OpOverload but got: {op} " 

336 f"of type {type(op)}. {_extra_msg}" 

337 ) 

338 

339 def policy_fn(ctx, op, *args, **kwargs): 

340 if op in policy_fn_or_list: 

341 return CheckpointPolicy.MUST_SAVE 

342 else: 

343 return CheckpointPolicy.PREFER_RECOMPUTE 

344 elif callable(policy_fn_or_list): 

345 policy_fn = policy_fn_or_list 

346 else: 

347 raise TypeError("policy_fn_or_list must be either a function or a list of ops.") 

348 

349 swap_storage = Storage() # patch code 

350 storage: Dict[Any, List[Any]] = defaultdict(list) 

351 return ( 

352 _CachingTorchDispatchMode( 

353 policy_fn, swap_storage, storage, group_swap=group_swap, cpu_pool=cpu_pool 

354 ), 

355 _CachedTorchDispatchMode(policy_fn, swap_storage, storage, allow_cache_entry_mutation), 

356 )