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

169 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"""Local autograd graphs for excluded regions inside a reentrant checkpoint.""" 

16import contextlib 

17import contextvars 

18import weakref 

19from typing import Any, Iterator, Optional, Tuple 

20 

21import torch 

22from torch.utils._pytree import tree_flatten, tree_unflatten 

23 

24from . import checkpoint_exclude as recipes 

25 

26 

27_CURRENT_FRAME = contextvars.ContextVar("hyper_reentrant_frame", default=None) 

28 

29 

30@contextlib.contextmanager 

31def execution_frame(frame: Any) -> Iterator[None]: 

32 """Expose a frame only while running its forward or replay function.""" 

33 token = _CURRENT_FRAME.set(frame) 

34 try: 

35 yield 

36 finally: 

37 _CURRENT_FRAME.reset(token) 

38 

39 

40class _BorrowedInputs: 

41 """Pass real activations to a Function without making them autograd inputs.""" 

42 

43 def __init__(self, tensors: Tuple[torch.Tensor, ...]) -> None: 

44 """Borrow Tensor data only until the boundary forward has detached it.""" 

45 self.tensors = tensors 

46 self.slots = [] 

47 

48 

49class _InputBoundary(torch.autograd.Function): 

50 """Connect local gradients to a zero-element root without retaining inputs.""" 

51 

52 @staticmethod 

53 def forward(ctx: Any, borrowed: _BorrowedInputs, dummy: torch.Tensor) -> tuple: 

54 """Return detached aliases while storing gradient slots, not activations.""" 

55 del dummy 

56 ctx.set_materialize_grads(False) 

57 ctx.slots = [None] * len(borrowed.tensors) 

58 borrowed.slots = ctx.slots 

59 outputs = tuple(tensor.detach() for tensor in borrowed.tensors) 

60 borrowed.tensors = None 

61 return outputs 

62 

63 @staticmethod 

64 def backward(ctx: Any, *grads: torch.Tensor) -> tuple: 

65 """Record local input sensitivities for the replay bridge.""" 

66 ctx.slots[:] = grads 

67 return None, None 

68 

69 

70class _LocalTape: 

71 """Keep the original excluded graph and only lightweight input gradient edges.""" 

72 

73 def __init__(self, outputs: tuple, input_edges: tuple, slots: list, frame: Any) -> None: 

74 """Retain the local graph while holding only gradient edges to its inputs.""" 

75 self.outputs = outputs 

76 self.input_edges = input_edges 

77 self.slots = slots 

78 self.frame_ref = weakref.ref(frame) 

79 self.output_mask = tuple(item.requires_grad for item in outputs) 

80 self.param_input_mask = () 

81 self.param_aliases = () 

82 

83 def gradients(self, grad_outputs: tuple) -> tuple: 

84 """Compute local gradients according to the owning frame's explicit phase.""" 

85 frame = self.frame_ref() 

86 if frame is None: 

87 raise RuntimeError("Reentrant checkpoint frame was released before exclude backward") 

88 pairs = [(out, grad) for out, grad in zip(self.outputs, grad_outputs) if out.requires_grad and grad is not None] 

89 if not pairs: 

90 return (None,) * len(self.input_edges) 

91 outputs, grads = zip(*pairs) 

92 if frame.phase in {"ordinary", "dw"}: 

93 return self._backward(outputs, grads, frame.retain_graph) 

94 return self._dx_gradients(outputs, grads) 

95 

96 def _backward(self, outputs: tuple, grads: tuple, retain_graph: bool) -> tuple: 

97 """Accumulate captured parameters once and return explicit input sensitivities.""" 

98 self.slots[:] = [None] * len(self.slots) 

99 for alias in self.param_aliases: 

100 if alias is not None: 

101 alias.grad = None 

102 torch.autograd.backward(outputs, grads, retain_graph=retain_graph) 

103 slot_grads = iter(self.slots) 

104 seen_aliases = set() 

105 

106 def alias_grad(alias: Optional[torch.Tensor]) -> Optional[torch.Tensor]: 

107 """Return a shared explicit proxy gradient only at its first input edge.""" 

108 if alias is None or id(alias) in seen_aliases: 

109 return None 

110 seen_aliases.add(id(alias)) 

111 return alias.grad 

112 

113 return tuple( 

114 alias_grad(alias) if is_param else next(slot_grads) 

115 for is_param, alias in zip(self.param_input_mask, self.param_aliases) 

116 ) 

117 

118 def _dx_gradients(self, outputs: tuple, grads: tuple) -> tuple: 

119 """Target only activation boundaries, leaving parameter hooks untouched.""" 

120 targets = [edge for edge, is_param in zip(self.input_edges, self.param_input_mask) 

121 if edge is not None and not is_param] 

122 if not targets: 

123 return (None,) * len(self.input_edges) 

124 result = iter(torch.autograd.grad(outputs, targets, grads, retain_graph=True, allow_unused=True)) 

125 return tuple( 

126 next(result) if edge is not None and not is_param else None 

127 for edge, is_param in zip(self.input_edges, self.param_input_mask) 

128 ) 

129 

130 def release(self) -> None: 

131 """Drop graph outputs and boundary gradient edges after use.""" 

132 self.outputs = () 

133 self.input_edges = () 

134 self.slots = [] 

135 self.param_aliases = () 

136 self.param_input_mask = () 

137 

138 

139class _ReplayBridge(torch.autograd.Function): 

140 """Connect the cached local graph to current replay inputs and its trigger.""" 

141 

142 @staticmethod 

143 def forward(ctx: Any, entry: Any, input_count: int, *tensors: torch.Tensor) -> tuple: 

144 """Return detached outputs with the recorded differentiability mask.""" 

145 del tensors 

146 ctx.entry = entry 

147 ctx.input_count = input_count 

148 ctx.set_materialize_grads(False) 

149 outputs = tuple(tensor.detach() for tensor in entry.tape.outputs) 

150 ctx.mark_non_differentiable(*( 

151 output for output, differentiable in zip(outputs, entry.tape.output_mask) 

152 if not differentiable 

153 )) 

154 return outputs 

155 

156 @staticmethod 

157 def backward(ctx: Any, *grad_outputs: torch.Tensor) -> tuple: 

158 """Propagate through the original local graph using the frame phase.""" 

159 input_grads = ctx.entry.tape.gradients(grad_outputs) 

160 return (None, None, *input_grads, None) 

161 

162 

163def _replace_tensor_leaves(value, tensors): 

164 leaves, spec = tree_flatten(value) 

165 replacements = iter(tensors) 

166 return tree_unflatten([next(replacements) if isinstance(item, torch.Tensor) else item for item in leaves], spec) 

167 

168 

169def _tensor_leaves(value: Any) -> tuple: 

170 """Flatten the supported input/output pytree to its Tensor leaves.""" 

171 return tuple(item for item in tree_flatten(value)[0] if isinstance(item, torch.Tensor)) 

172 

173 

174def _replay_exclude(wrapper: Any, args: tuple, kwargs: dict, frame: Any) -> Any: 

175 """Bind recipes and bridge cached outputs to the replay graph.""" 

176 entry = frame.exclude_cache.pop(id(wrapper)) 

177 frame.entries.append(entry) 

178 recipes._restore_rng_state(entry.rng_state) # pylint: disable=protected-access 

179 recipes._materialize_recompute_inputs(entry, args, kwargs) # pylint: disable=protected-access 

180 current = _tensor_leaves((args, kwargs)) 

181 if len(current) != len(entry.tape.input_edges): 

182 raise RuntimeError("Checkpoint exclusion Tensor input count changed during replay") 

183 result = _ReplayBridge.apply(entry, len(current), *current, torch.empty(0, requires_grad=True)) 

184 tensors = (result,) if isinstance(result, torch.Tensor) else result 

185 return _replace_tensor_leaves(entry.output, tensors) 

186 

187 

188def _local_inputs(original: tuple, param_ids: set) -> tuple: 

189 """Build a local activation boundary and explicit parameter proxies.""" 

190 borrowed = _BorrowedInputs(tuple(item for item in original if id(item) not in param_ids)) 

191 boundary = _InputBoundary.apply(borrowed, torch.empty(0, requires_grad=True)) if borrowed.tensors else () 

192 boundary = (boundary,) if isinstance(boundary, torch.Tensor) else boundary 

193 replacements = iter(boundary) 

194 param_aliases = {} 

195 for item in original: 

196 if id(item) in param_ids and id(item) not in param_aliases: 

197 param_aliases[id(item)] = item.detach().requires_grad_(item.requires_grad) 

198 local = (param_aliases[id(item)] if id(item) in param_ids else next(replacements) for item in original) 

199 return tuple(local), boundary, borrowed.slots, param_aliases 

200 

201 

202def _input_edges(original: tuple, boundary: tuple, param_ids: set, param_aliases: dict) -> tuple: 

203 """Expose one gradient edge per original input without retaining activations.""" 

204 boundary_edges = iter( 

205 torch.autograd.graph.get_gradient_edge(tensor) if tensor.requires_grad else None for tensor in boundary 

206 ) 

207 seen_params = set() 

208 edges = [] 

209 for item in original: 

210 if id(item) not in param_ids: 

211 edges.append(next(boundary_edges)) 

212 elif item.requires_grad and id(item) not in seen_params: 

213 edges.append(torch.autograd.graph.get_gradient_edge(param_aliases[id(item)])) 

214 seen_params.add(id(item)) 

215 else: 

216 edges.append(None) 

217 return tuple(edges) 

218 

219 

220def _record_exclude(wrapper: Any, module: Any, args: tuple, kwargs: dict, frame: Any) -> Any: 

221 """Execute an excluded region once under localized saved-tensor hooks.""" 

222 if frame.recording_exclude: 

223 raise ValueError("Nested checkpoint exclusion wrappers are not supported") 

224 original = _tensor_leaves((args, kwargs)) 

225 param_ids = {id(param) for param in frame.params or ()} if frame.split else set() 

226 with torch.enable_grad(): 

227 local, boundary, slots, param_aliases = _local_inputs(original, param_ids) 

228 local_args, local_kwargs = _replace_tensor_leaves((args, kwargs), local) 

229 infos = recipes._capture_recompute_inputs(local_args, local_kwargs) # pylint: disable=protected-access 

230 bindings = [] 

231 swap_hooks = wrapper._make_exclude_swap_hooks() # pylint: disable=protected-access 

232 token = recipes._ACTIVE_PACK_STATE.set( # pylint: disable=protected-access 

233 recipes._PackState(infos, bindings, swap_hooks=swap_hooks) # pylint: disable=protected-access 

234 ) 

235 frame.recording_exclude = True 

236 try: 

237 with recipes._saved_tensors_context(): # pylint: disable=protected-access 

238 output = module(*local_args, **local_kwargs) 

239 finally: 

240 frame.recording_exclude = False 

241 recipes._ACTIVE_PACK_STATE.reset(token) # pylint: disable=protected-access 

242 if swap_hooks is not None: 

243 wrapper._register_exclude_outputs(output) # pylint: disable=protected-access 

244 outputs = _tensor_leaves(output) 

245 if not outputs: 

246 raise ValueError("Reentrant checkpoint exclusion requires at least one Tensor output") 

247 tape = _LocalTape(outputs, _input_edges(original, boundary, param_ids, param_aliases), slots, frame) 

248 tape.param_input_mask = tuple(id(item) in param_ids for item in original) 

249 tape.param_aliases = tuple(param_aliases.get(id(item)) for item in original) 

250 entry = recipes._ExcludeCacheEntry( # pylint: disable=protected-access 

251 output, bindings, rng_state=recipes._capture_rng_state( # pylint: disable=protected-access 

252 args, kwargs, output, frame.preserve_rng_state 

253 ), 

254 tape=tape, 

255 ) 

256 frame.exclude_cache.save(id(wrapper), entry) 

257 detached = tuple(tensor.detach() for tensor in outputs) 

258 frame.excluded_output_mask.update( 

259 (id(item), (weakref.ref(item), mask)) for item, mask in zip(detached, tape.output_mask) 

260 ) 

261 return _replace_tensor_leaves(output, detached) 

262 

263 

264def run_exclude(wrapper: Any, module: Any, args: tuple, kwargs: dict, frame: Any) -> Any: 

265 """Record an excluded body once, or bridge its output during replay.""" 

266 if frame.replaying: 

267 return _replay_exclude(wrapper, args, kwargs, frame) 

268 return _record_exclude(wrapper, module, args, kwargs, frame) 

269 

270 

271def current_frame() -> Optional[Any]: 

272 """Return the frame active in the caller's forward/replay thread.""" 

273 return _CURRENT_FRAME.get()