Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / platform / torch / activation_checkpoint / checkpoint_exclude_wrapper.py: 91%

198 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-08-21 04:29 +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"""PyTorch wrapper for regions that should be saved instead of recomputed.""" 

16from collections import defaultdict, deque 

17from dataclasses import dataclass 

18from functools import lru_cache 

19from typing import Any, Callable, cast, Deque, Dict, List, Optional, Tuple 

20 

21import torch 

22 

23from hyper_parallel.core.activation_checkpoint.recompute_state import get_recompute_state 

24from hyper_parallel.platform.torch.activation_checkpoint.activation_swap import ActivationWrapper 

25 

26 

27_RECOMPUTE_INPUT_HANDLE_ATTR = "_hyper_parallel_recompute_input_handle" 

28_SAVE_OUTPUT_SOURCE_ATTR = "_hyper_parallel_save_output_source" 

29_MISSING = object() 

30_InputPath = Tuple[Tuple[str, Any], ...] 

31_TensorInput = Tuple[_InputPath, Any] 

32 

33 

34class _RecomputedInputHandle: 

35 """Defer one excluded-region saved input until checkpoint replay.""" 

36 

37 def __init__(self) -> None: 

38 """Initialize an unused and unresolved handle.""" 

39 self.used = False 

40 self._tensor = None 

41 

42 def mark_used(self) -> None: 

43 """Record that an exclude operation saved this input for backward.""" 

44 self.used = True 

45 

46 def materialize(self, tensor: Any) -> None: 

47 """Bind the handle to the matching input produced during replay.""" 

48 self._tensor = tensor 

49 

50 def get_recomputed_tensor(self) -> Any: 

51 """Return the replay-produced input for backward.""" 

52 if self._tensor is None: 

53 raise RuntimeError("Checkpoint-excluded input was requested before recomputation") 

54 return self._tensor 

55 

56 

57@dataclass(frozen=True) 

58class _InputBinding: 

59 """Map one input path to its deferred saved-tensor handle.""" 

60 

61 path: _InputPath 

62 handle: _RecomputedInputHandle 

63 

64 

65@dataclass 

66class _ExcludeCacheEntry: 

67 """Store one excluded call's replay value and deferred input bindings.""" 

68 

69 output: Any 

70 input_bindings: List[_InputBinding] 

71 output_tensor_count: int = 1 

72 

73 

74class _ExcludeCache: 

75 """Store excluded-region call entries for one checkpoint invocation.""" 

76 

77 def __init__(self) -> None: 

78 """Initialize an empty per-checkpoint output cache.""" 

79 self._entries: Dict[int, Deque[_ExcludeCacheEntry]] = defaultdict(deque) 

80 

81 def save(self, wrapper_id: int, entry: _ExcludeCacheEntry) -> None: 

82 """Save one call entry produced by a checkpoint-excluded region.""" 

83 self._entries[wrapper_id].append(entry) 

84 

85 def pop(self, wrapper_id: int) -> _ExcludeCacheEntry: 

86 """Return the matching forward call entry during recomputation.""" 

87 entries = self._entries.get(wrapper_id) 

88 if not entries: 

89 raise RuntimeError("No cached forward output is available for this checkpoint exclusion wrapper") 

90 entry = entries.popleft() 

91 if not entries: 

92 self._entries.pop(wrapper_id) 

93 return entry 

94 

95 def clear(self) -> None: 

96 """Release outputs not consumed because recomputation stopped early.""" 

97 self._entries.clear() 

98 

99 

100def _pack_saved_tensor(tensor: Any) -> Any: 

101 """Return a deferred input handle or detached tensor data.""" 

102 handle = getattr(tensor, _RECOMPUTE_INPUT_HANDLE_ATTR, None) 

103 if isinstance(handle, _RecomputedInputHandle): 

104 handle.mark_used() 

105 return handle 

106 return tensor.detach() if tensor.requires_grad else tensor 

107 

108 

109def _unpack_saved_tensor(value: Any) -> Any: 

110 """Restore the saved tensor for backward.""" 

111 if isinstance(value, _RecomputedInputHandle): 

112 return value.get_recomputed_tensor() 

113 return value 

114 

115 

116def _saved_tensors_context() -> Any: 

117 """Create an inner hook that stores real tensors instead of outer holders.""" 

118 return torch.autograd.graph.saved_tensors_hooks(_pack_saved_tensor, _unpack_saved_tensor) 

119 

120 

121_EXCLUDE_CACHE_KEY = object() 

122 

123 

124def _append_tensor_inputs( 

125 value: Any, 

126 path: _InputPath, 

127 leaves: List[_TensorInput], 

128 tensor_type: type, 

129) -> None: 

130 """Append tensor leaves without creating a self-referential local function.""" 

131 if isinstance(value, tensor_type): 

132 leaves.append((path, value)) 

133 return 

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

135 for index, item in enumerate(value): 

136 _append_tensor_inputs(item, path + (("index", index),), leaves, tensor_type) 

137 return 

138 if isinstance(value, dict): 

139 for key, item in value.items(): 

140 _append_tensor_inputs(item, path + (("key", key),), leaves, tensor_type) 

141 

142 

143def _collect_tensor_inputs(args: Tuple[Any, ...], kwargs: Dict[str, Any]) -> List[_TensorInput]: 

144 """Return tensor leaves and self-describing paths from one excluded-region call.""" 

145 leaves = [] 

146 for index, arg in enumerate(args): 

147 _append_tensor_inputs(arg, (("arg", index),), leaves, torch.Tensor) 

148 for key, value in kwargs.items(): 

149 _append_tensor_inputs(value, (("kwarg", key),), leaves, torch.Tensor) 

150 return leaves 

151 

152 

153def _mark_recompute_inputs( 

154 invocation_id: object, 

155 args: Tuple[Any, ...], 

156 kwargs: Dict[str, Any], 

157) -> Tuple[List[_InputBinding], List[Tuple[Any, Any]]]: 

158 """Attach deferred handles to inputs that replay reproduces.""" 

159 bindings = [] 

160 previous_handles = [] 

161 seen_tensor_ids = set() 

162 try: 

163 for path, tensor in _collect_tensor_inputs(args, kwargs): 

164 if ( 

165 isinstance(tensor, torch.nn.Parameter) 

166 or id(tensor) in seen_tensor_ids 

167 or getattr(tensor, _SAVE_OUTPUT_SOURCE_ATTR, None) is invocation_id 

168 ): 

169 continue 

170 handle = _RecomputedInputHandle() 

171 previous = getattr(tensor, _RECOMPUTE_INPUT_HANDLE_ATTR, _MISSING) 

172 previous_handles.append((tensor, previous)) 

173 setattr(tensor, _RECOMPUTE_INPUT_HANDLE_ATTR, handle) 

174 bindings.append(_InputBinding(path, handle)) 

175 seen_tensor_ids.add(id(tensor)) 

176 except BaseException: 

177 _restore_recompute_inputs(previous_handles) 

178 raise 

179 return bindings, previous_handles 

180 

181 

182def _restore_recompute_inputs(previous_handles: List[Tuple[Any, Any]]) -> None: 

183 """Restore Tensor attributes overwritten for one excluded call.""" 

184 for tensor, previous in previous_handles: 

185 if previous is _MISSING: 

186 if hasattr(tensor, _RECOMPUTE_INPUT_HANDLE_ATTR): 

187 delattr(tensor, _RECOMPUTE_INPUT_HANDLE_ATTR) 

188 else: 

189 setattr(tensor, _RECOMPUTE_INPUT_HANDLE_ATTR, previous) 

190 

191 

192def _resolve_input(args: Tuple[Any, ...], kwargs: Dict[str, Any], path: _InputPath) -> Any: 

193 """Resolve one replay input from its forward argument path.""" 

194 root_kind, root_key = path[0] 

195 value = args[root_key] if root_kind == "arg" else kwargs[root_key] 

196 for _, token_value in path[1:]: 

197 value = value[token_value] 

198 return value 

199 

200 

201def _materialize_recompute_inputs( 

202 entry: _ExcludeCacheEntry, 

203 args: Tuple[Any, ...], 

204 kwargs: Dict[str, Any], 

205) -> None: 

206 """Bind used input handles to tensors produced during checkpoint replay.""" 

207 for binding in entry.input_bindings: 

208 if not binding.handle.used: 

209 continue 

210 tensor = _resolve_input(args, kwargs, binding.path) 

211 if not isinstance(tensor, torch.Tensor): 

212 raise RuntimeError( 

213 "Checkpoint replay did not reproduce a tensor input required by a checkpoint-excluded region" 

214 ) 

215 binding.handle.materialize(tensor.detach()) 

216 

217 

218def _has_used_input(input_bindings: List[_InputBinding]) -> bool: 

219 """Return whether the excluded call saved any marked input.""" 

220 return any(binding.handle.used for binding in input_bindings) 

221 

222 

223@lru_cache(maxsize=1) 

224def _get_replay_placeholder() -> Any: 

225 """Create a zero-element placeholder returned by an elided SAVE replay.""" 

226 return torch.empty(0, device="cpu") 

227 

228 

229def _make_replay_placeholder_output(tensor_count: int) -> Any: 

230 """Create one placeholder leaf for each forward output tensor.""" 

231 if tensor_count == 0: 

232 return () 

233 placeholder = _get_replay_placeholder() 

234 if tensor_count == 1: 

235 return placeholder 

236 return (placeholder,) * tensor_count 

237 

238 

239@lru_cache(maxsize=1) 

240def _get_recompute_trigger() -> Any: 

241 """Create the differentiable zero-element input used by recompute boundaries.""" 

242 return torch.empty(0, device="cpu", requires_grad=True) 

243 

244 

245@lru_cache(maxsize=1) 

246def _get_recompute_boundary() -> Any: 

247 """Create the autograd Function used to trigger outer checkpoint replay.""" 

248 class _RecomputeBoundary(torch.autograd.Function): 

249 """Trigger the outer checkpoint hook before excluded-region backward.""" 

250 

251 @staticmethod 

252 def forward(ctx: Any, tensor: Any, trigger: Any) -> Any: 

253 """Save one zero-element outer-hook dependency and return the tensor unchanged.""" 

254 ctx.save_for_backward(trigger) 

255 return tensor 

256 

257 @staticmethod 

258 def backward(ctx: Any, grad_output: Any) -> Tuple[Any, None]: 

259 """Trigger dependency unpack and pass the gradient through.""" 

260 _ = ctx.saved_tensors 

261 return grad_output, None 

262 

263 return _RecomputeBoundary 

264 

265 

266def _finalize_save_outputs_impl( 

267 output: Any, 

268 add_recompute_boundary: bool, 

269 invocation_id: Optional[object], 

270 tensor_leaf_count: Optional[List[int]], 

271 tensor_type: type, 

272) -> Any: 

273 """Recursively finalize SAVE output leaves.""" 

274 if isinstance(output, tensor_type): 

275 if tensor_leaf_count is not None: 

276 tensor_leaf_count[0] += 1 

277 if add_recompute_boundary: 

278 output = _get_recompute_boundary().apply(output, _get_recompute_trigger()) 

279 if invocation_id is not None: 

280 setattr(output, _SAVE_OUTPUT_SOURCE_ATTR, invocation_id) 

281 return output 

282 if isinstance(output, list): 

283 return [ 

284 _finalize_save_outputs_impl(item, add_recompute_boundary, invocation_id, tensor_leaf_count, tensor_type) 

285 for item in output 

286 ] 

287 if isinstance(output, tuple): 

288 items = [ 

289 _finalize_save_outputs_impl(item, add_recompute_boundary, invocation_id, tensor_leaf_count, tensor_type) 

290 for item in output 

291 ] 

292 if hasattr(output, "_fields"): 

293 return type(output)(*items) 

294 return tuple(items) 

295 if isinstance(output, dict): 

296 return type(output)( 

297 ( 

298 key, 

299 _finalize_save_outputs_impl( 

300 value, 

301 add_recompute_boundary, 

302 invocation_id, 

303 tensor_leaf_count, 

304 tensor_type, 

305 ), 

306 ) 

307 for key, value in output.items() 

308 ) 

309 return output 

310 

311 

312def _finalize_save_outputs( 

313 output: Any, 

314 add_recompute_boundary: bool, 

315 invocation_id: Optional[object], 

316 tensor_leaf_count: Optional[List[int]] = None, 

317) -> Any: 

318 """Apply the required boundary and SAVE provenance to output tensor leaves.""" 

319 return _finalize_save_outputs_impl( 

320 output, 

321 add_recompute_boundary, 

322 invocation_id, 

323 tensor_leaf_count, 

324 torch.Tensor, 

325 ) 

326 

327 

328class CheckpointExcludeWrapper(ActivationWrapper): 

329 """Exclude a callable region from checkpoint recomputation.""" 

330 

331 def __init__(self, module: Callable[..., Any], *, save_output: bool = True) -> None: 

332 """Initialize a checkpoint exclusion wrapper for a PyTorch module or function.""" 

333 if not callable(module): 

334 raise ValueError("module must be a PyTorch Module or callable") 

335 if not isinstance(save_output, bool): 

336 raise ValueError(f"save_output must be a bool, got {type(save_output).__name__}") 

337 super().__init__(module, track_overlaps=False) 

338 self.save_output = save_output 

339 

340 def forward(self, *args: Any, **kwargs: Any) -> Any: 

341 """Execute normally outside recompute and return the cached output in recompute.""" 

342 wrapped_module = cast(Callable[..., Any], self._wrapped_module) 

343 state = get_recompute_state() 

344 if state is None: 

345 return wrapped_module(*args, **kwargs) 

346 cache = state.get_resource(_EXCLUDE_CACHE_KEY, _ExcludeCache) 

347 if state.is_recomputing: 

348 entry = cache.pop(id(self)) 

349 _materialize_recompute_inputs(entry, args, kwargs) 

350 output = ( 

351 entry.output 

352 if self.save_output 

353 else _make_replay_placeholder_output(entry.output_tensor_count) 

354 ) 

355 return _finalize_save_outputs(output, _has_used_input(entry.input_bindings), None) 

356 

357 input_bindings, previous_handles = _mark_recompute_inputs(state.invocation_id, args, kwargs) 

358 try: 

359 with _saved_tensors_context(): 

360 output = wrapped_module(*args, **kwargs) 

361 finally: 

362 _restore_recompute_inputs(previous_handles) 

363 needs_recompute_boundary = _has_used_input(input_bindings) 

364 tensor_leaf_count = None if self.save_output else [0] 

365 finalized_output = _finalize_save_outputs( 

366 output, 

367 needs_recompute_boundary, 

368 state.invocation_id, 

369 tensor_leaf_count, 

370 ) 

371 replay_output = output if self.save_output else None 

372 output_tensor_count = 1 if tensor_leaf_count is None else tensor_leaf_count[0] 

373 cache.save(id(self), _ExcludeCacheEntry(replay_output, input_bindings, output_tensor_count)) 

374 return finalized_output 

375 

376 

377def checkpoint_exclude_wrapper( 

378 module: Callable[..., Any], 

379 *, 

380 save_output: bool = True, 

381) -> CheckpointExcludeWrapper: 

382 """Wrap a PyTorch module or function so its region is not recomputed. 

383 

384 Args: 

385 module: PyTorch Module or callable to execute only during the original 

386 checkpoint forward pass. 

387 save_output: Whether to retain the region output for checkpoint replay. 

388 Set this to ``False`` only when the output is passed directly as one 

389 argument to another checkpoint exclusion wrapper. Default: ``True``. 

390 

391 Returns: 

392 A wrapper that saves the callable's autograd tensors and reuses its 

393 forward output while replaying a non-reentrant checkpoint. 

394 

395 Note: 

396 This feature requires eager mode and a surrounding HyperParallel 

397 checkpoint configured with ``use_reentrant=False``. Nested checkpoint 

398 exclusion wrappers are not supported. 

399 """ 

400 return CheckpointExcludeWrapper(module, save_output=save_output)