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

392 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-11-15 07:18 +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"""Saved-tensor helpers for regions excluded from checkpoint recomputation.""" 

16import weakref 

17from collections import defaultdict, deque 

18from contextvars import ContextVar 

19from dataclasses import dataclass 

20from functools import cached_property, lru_cache 

21from typing import Any, Deque, Dict, List, Optional, Tuple 

22 

23import torch 

24 

25 

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

27_TensorInput = Tuple[_InputPath, Any] 

28 

29 

30@dataclass(frozen=True) 

31class _TensorMetadata: 

32 """Alias-relevant metadata captured while an excluded call is running.""" 

33 

34 tensor_id: int 

35 storage_ref: Any 

36 storage_nbytes: int 

37 dtype: torch.dtype 

38 shape: Tuple[int, ...] 

39 stride: Tuple[int, ...] 

40 storage_offset: int 

41 version: int 

42 itemsize: int 

43 numel: int 

44 is_contiguous: bool 

45 

46 @cached_property 

47 def layout_key(self) -> Tuple[Any, ...]: 

48 """Return the dtype/layout identity used to validate a rebuilt alias.""" 

49 return (self.dtype, self.shape, self.stride, self.version, self.is_contiguous) 

50 

51 

52@dataclass(frozen=True) 

53class _InputInfo: 

54 """Describe one canonical local input without retaining its tensor/storage.""" 

55 

56 path: _InputPath 

57 metadata: _TensorMetadata 

58 

59 

60@dataclass(frozen=True) 

61class _ViewRecipe: 

62 """Rebuild one saved input alias from its matching replay input.""" 

63 

64 shape: Tuple[int, ...] 

65 stride: Tuple[int, ...] 

66 relative_offset: int 

67 dtype: torch.dtype 

68 base_shape: Tuple[int, ...] 

69 base_stride: Tuple[int, ...] 

70 base_version: int 

71 base_is_contiguous: bool 

72 exact_input: bool 

73 

74 @cached_property 

75 def base_layout_key(self) -> Tuple[Any, ...]: 

76 """Return the replay-input layout identity this recipe was built from.""" 

77 return (self.dtype, self.base_shape, self.base_stride, self.base_version, self.base_is_contiguous) 

78 

79 

80class _RecomputedInputHandle: 

81 """Defer one excluded-region saved alias until checkpoint replay.""" 

82 

83 def __init__(self, recipe: _ViewRecipe) -> None: 

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

85 self.recipe = recipe 

86 self._tensor = None 

87 

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

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

90 self._tensor = tensor 

91 

92 def get_recomputed_tensor(self) -> Any: 

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

94 if self._tensor is None: 

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

96 if self._tensor._version != self.recipe.base_version: # pylint: disable=protected-access 

97 raise RuntimeError( 

98 "Checkpoint-excluded saved tensor was modified in-place after replay materialization" 

99 ) 

100 return self._tensor 

101 

102 

103@dataclass(frozen=True) 

104class _SavedTensorData: 

105 """Hold one detached saved tensor with its save-time version counter.""" 

106 

107 tensor: torch.Tensor 

108 version: int 

109 swap_hooks: Optional[Any] = None 

110 

111 

112@dataclass(frozen=True) 

113class _InputBinding: 

114 """Map one replay input path to a deferred saved-alias handle.""" 

115 

116 path: _InputPath 

117 handle: _RecomputedInputHandle 

118 

119 

120@dataclass 

121class _PackState: 

122 """Inputs and matched saved aliases for the active excluded call.""" 

123 

124 inputs: Tuple[_InputInfo, ...] 

125 bindings: List[_InputBinding] 

126 swap_hooks: Optional[Any] = None 

127 

128 

129_ACTIVE_PACK_STATE: ContextVar[Any] = ContextVar( 

130 "hyper_parallel_checkpoint_exclude_pack_state", 

131 default=None, 

132) 

133 

134 

135@dataclass(frozen=True) 

136class _DeviceRNGState: 

137 """One accelerator device's RNG state captured at a SAVE boundary.""" 

138 

139 device: torch.device 

140 state: Any 

141 

142 

143@dataclass(frozen=True) 

144class _RNGStateSnapshot: 

145 """CPU and accelerator RNG states captured after one excluded call.""" 

146 

147 cpu_state: Any 

148 device_states: Tuple[_DeviceRNGState, ...] 

149 

150 

151@dataclass 

152class _ExcludeCacheEntry: 

153 """Store one excluded call's replay value, deferred inputs, and RNG state.""" 

154 

155 output: Any 

156 input_bindings: List[_InputBinding] 

157 rng_state: Optional[_RNGStateSnapshot] = None 

158 tape: Any = None 

159 

160 

161class _ExcludeCache: 

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

163 

164 def __init__(self) -> None: 

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

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

167 

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

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

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

171 

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

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

174 entries = self._entries.get(wrapper_id) 

175 if not entries: 

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

177 entry = entries.popleft() 

178 if not entries: 

179 self._entries.pop(wrapper_id) 

180 return entry 

181 

182 def clear(self) -> None: 

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

184 for entries in self._entries.values(): 

185 for entry in entries: 

186 if entry.tape is not None: 

187 entry.tape.release() 

188 entry.output = None 

189 self._entries.clear() 

190 

191 

192@lru_cache(maxsize=1) 

193def _get_dtensor_type() -> Optional[type]: 

194 """Return the local DTensor type without importing it during module import.""" 

195 try: 

196 from hyper_parallel.core.dtensor.dtensor_base import DTensorBase # pylint: disable=C0415 

197 except ImportError: 

198 return None 

199 return DTensorBase 

200 

201 

202def _is_dtensor(tensor: Any) -> bool: 

203 """Return whether a value is a distributed tensor wrapper.""" 

204 dtensor_type = _get_dtensor_type() 

205 return dtensor_type is not None and isinstance(tensor, dtensor_type) 

206 

207 

208def _canonical_input_tensor(tensor: Any) -> Any: 

209 """Return the local tensor carried by a DTensor wrapper.""" 

210 if _is_dtensor(tensor): 

211 return tensor.to_local() 

212 return tensor 

213 

214 

215def _tensor_metadata(tensor: Any) -> Optional[_TensorMetadata]: 

216 """Return storage/layout metadata, or ``None`` when aliasing is unavailable.""" 

217 if not isinstance(tensor, torch.Tensor) or tensor.numel() == 0: 

218 return None 

219 try: 

220 storage = tensor.untyped_storage() 

221 storage_nbytes = storage.size() 

222 itemsize = tensor.element_size() 

223 if storage_nbytes == 0 or itemsize <= 0: 

224 return None 

225 return _TensorMetadata( 

226 tensor_id=id(tensor), 

227 storage_ref=weakref.ref(storage), 

228 storage_nbytes=storage_nbytes, 

229 dtype=tensor.dtype, 

230 shape=tuple(tensor.shape), 

231 stride=tuple(tensor.stride()), 

232 storage_offset=tensor.storage_offset(), 

233 version=tensor._version, # pylint: disable=protected-access 

234 itemsize=itemsize, 

235 numel=tensor.numel(), 

236 is_contiguous=tensor.is_contiguous(), 

237 ) 

238 except (AttributeError, RuntimeError, TypeError, ValueError): 

239 return None 

240 

241 

242def _storage_span(metadata: _TensorMetadata) -> Optional[Tuple[int, int]]: 

243 """Return inclusive element offsets touched by a non-negative-stride tensor.""" 

244 if metadata.numel == 0 or any(step < 0 for step in metadata.stride): 

245 return None 

246 max_offset = metadata.storage_offset 

247 for size, step in zip(metadata.shape, metadata.stride): 

248 if size == 0: 

249 return None 

250 max_offset += (size - 1) * step 

251 if metadata.storage_offset < 0 or (max_offset + 1) * metadata.itemsize > metadata.storage_nbytes: 

252 return None 

253 return metadata.storage_offset, max_offset 

254 

255 

256def _exact_layout(saved: _TensorMetadata, base: _TensorMetadata) -> bool: 

257 """Return whether two tensors have identical dtype and logical layout.""" 

258 return ( 

259 saved.dtype == base.dtype 

260 and saved.shape == base.shape 

261 and saved.stride == base.stride 

262 and saved.storage_offset == base.storage_offset 

263 ) 

264 

265 

266def _is_non_overlapping_and_dense(metadata: _TensorMetadata) -> bool: 

267 """Return whether a non-contiguous layout covers every element exactly once.""" 

268 expected_stride = 1 

269 for stride, size in sorted( 

270 (stride, size) 

271 for size, stride in zip(metadata.shape, metadata.stride) 

272 if size > 1 

273 ): 

274 if stride != expected_stride: 

275 return False 

276 expected_stride *= size 

277 return True 

278 

279 

280def _make_exact_recipe(base: _TensorMetadata) -> _ViewRecipe: 

281 """Build a replay recipe for an unchanged exact input.""" 

282 return _ViewRecipe( 

283 shape=base.shape, 

284 stride=base.stride, 

285 relative_offset=0, 

286 dtype=base.dtype, 

287 base_shape=base.shape, 

288 base_stride=base.stride, 

289 base_version=base.version, 

290 base_is_contiguous=base.is_contiguous, 

291 exact_input=True, 

292 ) 

293 

294 

295def _make_view_recipe(saved: _TensorMetadata, base: _TensorMetadata) -> Optional[_ViewRecipe]: 

296 """Return a safe exact-input/view recipe, or ``None`` for a real save.""" 

297 base_storage = base.storage_ref() 

298 if base_storage is None or base_storage is not saved.storage_ref(): 

299 return None 

300 if saved.dtype != base.dtype or saved.version != base.version: 

301 return None 

302 

303 exact_input = _exact_layout(saved, base) 

304 if not exact_input: 

305 if not base.is_contiguous and not _is_non_overlapping_and_dense(base): 

306 return None 

307 saved_span = _storage_span(saved) 

308 base_span = _storage_span(base) 

309 if saved_span is None or base_span is None: 

310 return None 

311 if saved_span[0] < base_span[0] or saved_span[1] > base_span[1]: 

312 return None 

313 

314 return _ViewRecipe( 

315 shape=saved.shape, 

316 stride=saved.stride, 

317 relative_offset=saved.storage_offset - base.storage_offset, 

318 dtype=saved.dtype, 

319 base_shape=base.shape, 

320 base_stride=base.stride, 

321 base_version=base.version, 

322 base_is_contiguous=base.is_contiguous, 

323 exact_input=exact_input, 

324 ) 

325 

326 

327def _match_recompute_input( 

328 tensor: torch.Tensor, 

329 inputs: Tuple[_InputInfo, ...], 

330) -> Optional[Tuple[_InputInfo, _ViewRecipe]]: 

331 """Match a saved tensor to an excluded-call input by storage and layout.""" 

332 if _is_dtensor(tensor): 

333 return None 

334 for input_info in inputs: 

335 base = input_info.metadata 

336 if id(tensor) != base.tensor_id: 

337 continue 

338 if ( 

339 tensor.dtype != base.dtype 

340 or tuple(tensor.shape) != base.shape 

341 or tuple(tensor.stride()) != base.stride 

342 or tensor.storage_offset() != base.storage_offset 

343 or tensor._version != base.version # pylint: disable=protected-access 

344 ): 

345 return None 

346 return input_info, _make_exact_recipe(base) 

347 

348 saved = _tensor_metadata(tensor) 

349 if saved is None: 

350 return None 

351 

352 exact_identity = [] 

353 exact_layout = [] 

354 views = [] 

355 for input_info in inputs: 

356 recipe = _make_view_recipe(saved, input_info.metadata) 

357 if recipe is None: 

358 continue 

359 candidate = (input_info, recipe) 

360 if recipe.exact_input and saved.tensor_id == input_info.metadata.tensor_id: 

361 exact_identity.append(candidate) 

362 elif recipe.exact_input: 

363 exact_layout.append(candidate) 

364 else: 

365 views.append(candidate) 

366 

367 if exact_identity: 

368 return exact_identity[0] 

369 if exact_layout: 

370 return exact_layout[0] 

371 if views: 

372 return min(views, key=lambda item: item[0].metadata.numel) 

373 return None 

374 

375 

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

377 """Return a replayable input-alias handle or detached tensor data.""" 

378 pack_state = _ACTIVE_PACK_STATE.get() 

379 if pack_state is not None: 

380 if ( 

381 isinstance(tensor, torch.nn.Parameter) 

382 or isinstance(tensor._base, torch.nn.Parameter) # pylint: disable=protected-access 

383 ): 

384 return _SavedTensorData(tensor.detach(), tensor._version) # pylint: disable=protected-access 

385 matched = _match_recompute_input(tensor, pack_state.inputs) 

386 if matched is not None: 

387 input_info, recipe = matched 

388 handle = _RecomputedInputHandle(recipe) 

389 pack_state.bindings.append(_InputBinding(input_info.path, handle)) 

390 return handle 

391 if pack_state.swap_hooks is not None: 

392 detached = pack_state.swap_hooks.pack_hook(tensor) 

393 return _SavedTensorData( 

394 detached, 

395 tensor._version, # pylint: disable=protected-access 

396 pack_state.swap_hooks, 

397 ) 

398 return _SavedTensorData(tensor.detach(), tensor._version) # pylint: disable=protected-access 

399 

400 

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

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

403 if isinstance(value, _RecomputedInputHandle): 

404 return value.get_recomputed_tensor() 

405 if isinstance(value, _SavedTensorData): 

406 tensor = value.tensor 

407 if value.swap_hooks is not None: 

408 tensor = value.swap_hooks.unpack_hook(tensor) 

409 if tensor._version != value.version: # pylint: disable=protected-access 

410 raise RuntimeError("Checkpoint-excluded saved tensor was modified in-place after it was saved") 

411 return tensor 

412 return value 

413 

414 

415def _saved_tensors_context() -> Any: 

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

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

418 

419 

420_EXCLUDE_CACHE_KEY = object() 

421 

422 

423def _append_tensor_inputs(value: Any, path: _InputPath, leaves: List[_TensorInput]) -> None: 

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

425 if isinstance(value, torch.Tensor): 

426 leaves.append((path, value)) 

427 return 

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

429 for index, item in enumerate(value): 

430 _append_tensor_inputs(item, path + (("index", index),), leaves) 

431 return 

432 if isinstance(value, dict): 

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

434 _append_tensor_inputs(item, path + (("key", key),), leaves) 

435 

436 

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

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

439 leaves = [] 

440 for index, arg in enumerate(args): 

441 _append_tensor_inputs(arg, (("arg", index),), leaves) 

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

443 _append_tensor_inputs(value, (("kwarg", key),), leaves) 

444 return leaves 

445 

446 

447def _iter_tensor_leaves(value: Any) -> List[torch.Tensor]: 

448 """Return tensor leaves from one structured excluded-region value.""" 

449 if isinstance(value, torch.Tensor): 

450 return [value] 

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

452 leaves = [] 

453 for item in value: 

454 leaves.extend(_iter_tensor_leaves(item)) 

455 return leaves 

456 if isinstance(value, dict): 

457 leaves = [] 

458 for item in value.values(): 

459 leaves.extend(_iter_tensor_leaves(item)) 

460 return leaves 

461 return [] 

462 

463 

464def _capture_recompute_inputs( 

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

466 kwargs: Dict[str, Any], 

467) -> Tuple[_InputInfo, ...]: 

468 """Capture canonical local input storage/layout without retaining tensors.""" 

469 inputs = [] 

470 seen_tensor_ids = set() 

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

472 if isinstance(tensor, torch.nn.Parameter): 

473 continue 

474 canonical = _canonical_input_tensor(tensor) 

475 if id(canonical) in seen_tensor_ids: 

476 continue 

477 metadata = _tensor_metadata(canonical) 

478 if metadata is None: 

479 continue 

480 inputs.append(_InputInfo(path, metadata)) 

481 seen_tensor_ids.add(id(canonical)) 

482 return tuple(inputs) 

483 

484 

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

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

487 root_kind, root_key = path[0] 

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

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

490 value = value[token_value] 

491 return value 

492 

493 

494def _rebuild_saved_alias(tensor: Any, recipe: _ViewRecipe) -> Any: 

495 """Rebuild a detached saved input/view from one replay-produced base.""" 

496 canonical = _canonical_input_tensor(tensor) 

497 metadata = _tensor_metadata(canonical) 

498 if metadata is None: 

499 raise RuntimeError("Checkpoint replay input does not expose usable storage metadata") 

500 if metadata.layout_key != recipe.base_layout_key: 

501 raise RuntimeError( 

502 "Checkpoint replay input layout/version changed for a checkpoint-excluded saved tensor" 

503 ) 

504 

505 if recipe.exact_input: 

506 return canonical.detach() 

507 

508 view_offset = metadata.storage_offset + recipe.relative_offset 

509 max_offset = view_offset 

510 numel = 1 

511 for size, step in zip(recipe.shape, recipe.stride): 

512 if size == 0: 

513 raise RuntimeError("Checkpoint-excluded empty saved views should have been saved normally") 

514 numel *= size 

515 max_offset += (size - 1) * step 

516 if numel == 0 or view_offset < 0 or (max_offset + 1) * metadata.itemsize > metadata.storage_nbytes: 

517 raise RuntimeError("Checkpoint-excluded saved view is outside the replay input storage") 

518 

519 return canonical.as_strided(recipe.shape, recipe.stride, view_offset).detach() 

520 

521 

522def _materialize_recompute_inputs( 

523 entry: _ExcludeCacheEntry, 

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

525 kwargs: Dict[str, Any], 

526) -> None: 

527 """Bind saved-alias handles to tensors rebuilt from checkpoint replay inputs.""" 

528 for binding in entry.input_bindings: 

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

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

531 raise RuntimeError( 

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

533 ) 

534 binding.handle.materialize(_rebuild_saved_alias(tensor, binding.handle.recipe)) 

535 

536 

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

538 """Return whether the excluded call saved any replayable input alias.""" 

539 return bool(input_bindings) 

540 

541 

542def _collect_rng_devices(args: Tuple[Any, ...], kwargs: Dict[str, Any], output: Any) -> Tuple[torch.device, ...]: 

543 """Return distinct accelerator devices touched by one excluded call.""" 

544 devices = [] 

545 seen = set() 

546 tensors = _iter_tensor_leaves((args, kwargs)) 

547 tensors.extend(_iter_tensor_leaves(output)) 

548 for tensor in tensors: 

549 device = _canonical_input_tensor(tensor).device 

550 if device.type in {"cpu", "meta"}: 

551 continue 

552 key = (device.type, device.index) 

553 if key in seen: 

554 continue 

555 devices.append(device) 

556 seen.add(key) 

557 return tuple(devices) 

558 

559 

560def _get_device_rng_state(device: torch.device) -> Any: 

561 """Read one accelerator RNG state when its backend exposes the API.""" 

562 device_module = getattr(torch, device.type, None) 

563 get_rng_state = getattr(device_module, "get_rng_state", None) 

564 if not callable(get_rng_state): 

565 return None 

566 try: 

567 return get_rng_state(device) 

568 except TypeError: 

569 return get_rng_state() 

570 

571 

572def _set_device_rng_state(device: torch.device, state: Any) -> None: 

573 """Restore one accelerator RNG state when its backend exposes the API.""" 

574 device_module = getattr(torch, device.type, None) 

575 set_rng_state = getattr(device_module, "set_rng_state", None) 

576 if not callable(set_rng_state): 

577 return 

578 try: 

579 set_rng_state(state, device) 

580 except TypeError: 

581 set_rng_state(state) 

582 

583 

584def _capture_rng_state( 

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

586 kwargs: Dict[str, Any], 

587 output: Any, 

588 enabled: bool, 

589) -> Optional[_RNGStateSnapshot]: 

590 """Snapshot RNG at a SAVE exit so replay can reproduce its state advancement.""" 

591 if not enabled: 

592 return None 

593 device_states = [] 

594 for device in _collect_rng_devices(args, kwargs, output): 

595 state = _get_device_rng_state(device) 

596 if state is not None: 

597 device_states.append(_DeviceRNGState(device, state)) 

598 return _RNGStateSnapshot(torch.get_rng_state(), tuple(device_states)) 

599 

600 

601def _restore_rng_state(snapshot: Optional[_RNGStateSnapshot]) -> None: 

602 """Restore the RNG state that the skipped SAVE body would have produced.""" 

603 if snapshot is None: 

604 return 

605 torch.set_rng_state(snapshot.cpu_state) 

606 for device_state in snapshot.device_states: 

607 _set_device_rng_state(device_state.device, device_state.state) 

608 

609 

610@lru_cache(maxsize=1) 

611def _get_recompute_trigger() -> Any: 

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

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

614 

615 

616@lru_cache(maxsize=1) 

617def _get_recompute_boundary() -> Any: 

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

619 class _RecomputeBoundary(torch.autograd.Function): 

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

621 

622 @staticmethod 

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

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

625 ctx.save_for_backward(trigger) 

626 return tensor 

627 

628 @staticmethod 

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

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

631 _ = ctx.saved_tensors 

632 return grad_output, None 

633 

634 return _RecomputeBoundary 

635 

636 

637def _finalize_save_outputs_impl( 

638 output: Any, 

639 add_recompute_boundary: bool, 

640 tensor_type: type, 

641) -> Any: 

642 """Recursively finalize SAVE output leaves.""" 

643 if isinstance(output, tensor_type): 

644 if add_recompute_boundary: 

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

646 return output 

647 if isinstance(output, list): 

648 return [ 

649 _finalize_save_outputs_impl(item, add_recompute_boundary, tensor_type) 

650 for item in output 

651 ] 

652 if isinstance(output, tuple): 

653 items = [ 

654 _finalize_save_outputs_impl(item, add_recompute_boundary, tensor_type) 

655 for item in output 

656 ] 

657 if hasattr(output, "_fields"): 

658 return type(output)(*items) 

659 return tuple(items) 

660 if isinstance(output, dict): 

661 return type(output)( 

662 ( 

663 key, 

664 _finalize_save_outputs_impl(value, add_recompute_boundary, tensor_type), 

665 ) 

666 for key, value in output.items() 

667 ) 

668 return output 

669 

670 

671def _finalize_save_outputs( 

672 output: Any, 

673 add_recompute_boundary: bool, 

674) -> Any: 

675 """Apply the required replay boundary to tensor output leaves.""" 

676 return _finalize_save_outputs_impl( 

677 output, 

678 add_recompute_boundary, 

679 torch.Tensor, 

680 )