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

209 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"""Pipeline-parallel activation swap scheduling helpers.""" 

16 

17import warnings 

18from collections import defaultdict 

19from contextlib import nullcontext 

20from enum import IntEnum 

21import itertools 

22from typing import Any, ContextManager, FrozenSet, Iterable, List 

23 

24from hyper_parallel.core.activation_memory.swap import SwapManager 

25from hyper_parallel.core.pipeline_parallel.utils import MetaStep, MetaStepType 

26 

27MIN_SWAP_GAP = 4 

28 

29 

30class _BeforeActionPriority(IntEnum): 

31 WAIT_LOAD = 10 

32 LAUNCH_LOAD = 20 

33 

34 

35class _AfterActionPriority(IntEnum): 

36 WAIT_OFFLOAD = 10 

37 LAUNCH_OFFLOAD = 20 

38 

39 

40def unregister_layer_swap_hooks(stages: Iterable[Any]) -> int: 

41 """Remove layer-level swap hooks from pipeline stage modules. 

42 

43 Pipeline schedule swap groups are keyed by stage and micro-batch, while 

44 hooks installed by ``SwapManager.set_forward_prefetch_layer`` replace the 

45 active group with a layer-level group. When pipeline swap is enabled, the 

46 schedule takes precedence and the layer hooks are permanently removed. 

47 

48 Args: 

49 stages: Pipeline stages whose submodules are managed by the schedule. 

50 

51 Returns: 

52 Number of removed hook handles. 

53 """ 

54 removed_count = 0 

55 visited_modules = set() 

56 manager = SwapManager() 

57 for stage in stages: 

58 for _, module in stage.submodule.named_modules(): 

59 module_id = id(module) 

60 if module_id in visited_modules: 

61 continue 

62 visited_modules.add(module_id) 

63 # Full teardown, not just handle removal: also drops the module's 

64 # swap group from the process-wide singleton plus the 

65 # _swap_group_name/_swap_group_order/_swap_state attributes, so a 

66 # permanently-removed layer does not leave an orphaned SwapGroup 

67 # holding pinned host/device storage (see B.cache_no_eviction). 

68 removed_count += manager.unregister_forward_prefetch_layer(module) 

69 

70 if removed_count: 

71 warnings.warn( 

72 "Pipeline schedule swap=True takes precedence over layer-level activation swap; " 

73 "hooks registered by SwapManager.set_forward_prefetch_layer() were removed.", 

74 UserWarning, 

75 stacklevel=2, 

76 ) 

77 return removed_count 

78 

79 

80class PipelineSwapSession: 

81 """Own activation-swap groups for one pipeline schedule run.""" 

82 

83 _generation = itertools.count() 

84 

85 def __init__(self, eligible_keys: FrozenSet[tuple[int, int]]) -> None: 

86 """Create one run-scoped session from build-time eligible chunk keys.""" 

87 self._generation_id = next(self._generation) 

88 self._manager = SwapManager() 

89 self._eligible_keys = eligible_keys 

90 self._group_names = { 

91 key: f"pp_swap_run{self._generation_id}_s{key[0]}_m{key[1]}" 

92 for key in self._eligible_keys 

93 } 

94 self._forward_context_keys = set() 

95 

96 @staticmethod 

97 def _key(step: Any) -> tuple[int, int]: 

98 """Return the logical chunk key carried by a swap or compute step.""" 

99 return step.stage_index, step.micro_index 

100 

101 def manages(self, step: Any) -> bool: 

102 """Return whether this session manages the step's pipeline chunk.""" 

103 return self._key(step) in self._eligible_keys 

104 

105 def group_context(self, step: Any) -> ContextManager[None]: 

106 """Enter the run-scoped swap group for one forward leaf.""" 

107 if not self.manages(step): 

108 return nullcontext() 

109 key = self._key(step) 

110 group_name = self._group_names[key] 

111 self._manager.ensure_group(group_name) 

112 self._forward_context_keys.add(key) 

113 return self._manager.group_context(group_name) 

114 

115 def require_forward_context(self, step: Any) -> None: 

116 """Require the managed forward leaf to have entered its swap context.""" 

117 key = self._key(step) 

118 if key not in self._forward_context_keys: 

119 raise RuntimeError( 

120 "Pipeline swap did not observe the matching forward leaf. " 

121 "Custom overlap callbacks must call schedule.execute_fwd_leaf()." 

122 ) 

123 

124 def group_name(self, step: Any) -> str: 

125 """Return the physical group name for a managed step.""" 

126 key = self._key(step) 

127 if key not in self._group_names: 

128 raise RuntimeError(f"Pipeline swap does not manage chunk {key}.") 

129 return self._group_names[key] 

130 

131 def wait_load(self, step: Any) -> None: 

132 """Wait for H2D on the scheduler's current compute stream.""" 

133 if self.manages(step): 

134 self._manager.wait_load(self.group_name(step)) 

135 

136 def protect_aliases(self, step: Any, tensors: Any) -> None: 

137 """Keep pipeline-owned aliases resident for a managed chunk.""" 

138 if self.manages(step): 

139 self._manager.protect_alias_tensors(self.group_name(step), tensors) 

140 

141 def close(self) -> None: 

142 """Release every group still owned by this run.""" 

143 for group_name in self._group_names.values(): 

144 self._manager.abort_group(group_name) 

145 

146 

147def _is_compute_step(step) -> bool: 

148 return step is not None and step.type in ( 

149 MetaStepType.FWD, 

150 MetaStepType.BWD, 

151 MetaStepType.BWD_INPUT, 

152 MetaStepType.BWD_WEIGHT, 

153 ) 

154 

155 

156def _is_comm_step(step) -> bool: 

157 return step is not None and step.type in ( 

158 MetaStepType.FWD_RECV, 

159 MetaStepType.FWD_SEND, 

160 MetaStepType.BWD_RECV, 

161 MetaStepType.BWD_SEND, 

162 MetaStepType.BATCH_SEND_RECV, 

163 ) 

164 

165 

166def _is_composite_compute_step(step) -> bool: 

167 return ( 

168 step is not None 

169 and step.type in (MetaStepType.OVERLAP_F_B, MetaStepType.OVERLAP_B_F) 

170 and step.sub_steps 

171 ) 

172 

173 

174class _ComputeLeaf: 

175 """A real FWD/BWD leaf and the top-level container that owns it.""" 

176 

177 __slots__ = ("step", "container_index", "compute_index") 

178 

179 def __init__(self, step: Any, container_index: int, compute_index: int) -> None: 

180 """Record a compute leaf and its physical container positions.""" 

181 self.step = step 

182 self.container_index = container_index 

183 self.compute_index = compute_index 

184 

185 

186def _iter_compute_leaf_steps(step): 

187 """Yield real FWD/BWD steps, expanding composite containers.""" 

188 if _is_compute_step(step): 

189 yield step 

190 return 

191 if _is_composite_compute_step(step): 

192 for sub_step in step.sub_steps: 

193 if _is_compute_step(sub_step): 

194 yield sub_step 

195 

196 

197def _collect_compute_leaves(order): 

198 """Collect compute leaves while counting each composite as one slot.""" 

199 leaves = [] 

200 container_by_compute_index = {} 

201 compute_index = 0 

202 for container_index, step in enumerate(order): 

203 leaf_steps = list(_iter_compute_leaf_steps(step)) 

204 if not leaf_steps: 

205 continue 

206 container_by_compute_index[compute_index] = container_index 

207 for leaf_step in leaf_steps: 

208 leaves.append(_ComputeLeaf(leaf_step, container_index, compute_index)) 

209 compute_index += 1 

210 return leaves, container_by_compute_index 

211 

212 

213def _iter_steps_by_priority(priority_steps): 

214 """Yield steps from high priority to low priority.""" 

215 for _, step in sorted(priority_steps, key=lambda item: item[0], reverse=True): 

216 yield step 

217 

218 

219def _comm_block_anchor(order, index): 

220 """Return the last immediately following communication step.""" 

221 anchor = index 

222 for next_index in range(index + 1, len(order)): 

223 next_step = order[next_index] 

224 if _is_comm_step(next_step): 

225 anchor = next_index 

226 continue 

227 break 

228 return anchor 

229 

230 

231def _post_compute_anchor(order, index, leaf_step=None): 

232 """Return the safe index after which post-compute swap steps may run.""" 

233 step = leaf_step if leaf_step is not None else order[index] 

234 fallback_anchor = _comm_block_anchor(order, index) 

235 if step.type == MetaStepType.FWD: 

236 send_type = MetaStepType.FWD_SEND 

237 elif step.type in (MetaStepType.BWD, MetaStepType.BWD_INPUT): 

238 send_type = MetaStepType.BWD_SEND 

239 else: 

240 return fallback_anchor 

241 

242 for next_index in range(index + 1, fallback_anchor + 1): 

243 next_step = order[next_index] 

244 next_valid = (next_step is not None and next_step.type == send_type 

245 and next_step.stage_index == step.stage_index and next_step.micro_index == step.micro_index) 

246 if next_valid: 

247 return next_index 

248 if next_step is not None and next_step.type == MetaStepType.BATCH_SEND_RECV: 

249 for sub_step in next_step.sub_steps: 

250 if ( 

251 sub_step.type == send_type 

252 and sub_step.stage_index == step.stage_index 

253 and sub_step.micro_index == step.micro_index): 

254 return next_index 

255 return fallback_anchor 

256 

257 

258def _post_compute_launch_anchor(leaf): 

259 """Return the fallback point after compute where D2H may be launched.""" 

260 return leaf.container_index 

261 

262 

263def _load_launch_anchor( 

264 order: List[Any], fwd_leaf: _ComputeLeaf, bwd_leaf: _ComputeLeaf, 

265 compute_between: List[int]) -> int: 

266 """Choose the latest safe H2D launch point for plain or FSDP execution.""" 

267 has_fsdp_steps = any( 

268 step is not None and step.type in ( 

269 MetaStepType.FSDP_UNSHARD, 

270 MetaStepType.FSDP_RESHARD, 

271 MetaStepType.FSDP_REDUCE_GRAD, 

272 ) 

273 for step in order 

274 ) 

275 if not has_fsdp_steps: 

276 return compute_between[-1] 

277 

278 for index in range(bwd_leaf.container_index - 1, fwd_leaf.container_index, -1): 

279 step = order[index] 

280 if ( 

281 step is not None 

282 and step.type == MetaStepType.FSDP_UNSHARD 

283 and step.stage_index == bwd_leaf.step.stage_index): 

284 return index 

285 return bwd_leaf.container_index 

286 

287 

288def _index_compute_leaves(order): 

289 """Index compute leaves by chunk key, separating FWD from BWD consumers.""" 

290 fwd_index = {} 

291 bwd_index = {} 

292 leaves, container_by_compute_index = _collect_compute_leaves(order) 

293 for leaf in leaves: 

294 step = leaf.step 

295 key = (step.stage_index, step.micro_index) 

296 if step.type == MetaStepType.FWD: 

297 fwd_index[key] = leaf 

298 elif step.type in (MetaStepType.BWD, MetaStepType.BWD_INPUT): 

299 # BWD_INPUT (dxdw split) needs activations → place WAIT_LOAD before it. 

300 # BWD_WEIGHT is intentionally excluded: it does not consume the 

301 # original forward activations restored by swap. 

302 bwd_index[key] = leaf 

303 return fwd_index, bwd_index, container_by_compute_index 

304 

305 

306def _chunk_swap_actions(order, fwd_leaf, bwd_leaf, container_by_compute_index): 

307 """Return the swap actions for one chunk as ``(before, after)`` entries. 

308 

309 Each entry is ``(anchor_index, priority, meta_step)``. Returns ``None`` 

310 when the chunk is not swappable. 

311 """ 

312 if bwd_leaf.compute_index - fwd_leaf.compute_index < MIN_SWAP_GAP: 

313 return None 

314 compute_between = [ 

315 container_by_compute_index[index] 

316 for index in range(fwd_leaf.compute_index + 1, bwd_leaf.compute_index) 

317 ] 

318 if not compute_between: 

319 return None 

320 

321 stage_index = fwd_leaf.step.stage_index 

322 micro_index = fwd_leaf.step.micro_index 

323 # Always launch offload immediately after the FWD container so that the 

324 # async D2H starts before any FSDP_RESHARD or FWD_SEND that may sit 

325 # between the FWD and the next compute step. 

326 after = [ 

327 ( 

328 _post_compute_launch_anchor(fwd_leaf), 

329 _AfterActionPriority.LAUNCH_OFFLOAD, 

330 MetaStep(micro_index, MetaStepType.SWAP_LAUNCH_OFFLOAD, stage_index), 

331 ), 

332 ( 

333 _post_compute_anchor(order, compute_between[0]), 

334 _AfterActionPriority.WAIT_OFFLOAD, 

335 MetaStep(micro_index, MetaStepType.SWAP_WAIT_OFFLOAD, stage_index), 

336 ), 

337 ] 

338 before = [ 

339 ( 

340 _load_launch_anchor(order, fwd_leaf, bwd_leaf, compute_between), 

341 _BeforeActionPriority.LAUNCH_LOAD, 

342 MetaStep(micro_index, MetaStepType.SWAP_LAUNCH_LOAD, stage_index), 

343 ), 

344 ( 

345 bwd_leaf.container_index, 

346 _BeforeActionPriority.WAIT_LOAD, 

347 MetaStep(micro_index, MetaStepType.SWAP_WAIT_LOAD, stage_index), 

348 ), 

349 ] 

350 return before, after 

351 

352 

353def _collect_swap_actions(order): 

354 """Bucket swap actions by the order index they attach to. 

355 

356 Returns a ``(before_pairs, after_pairs)`` tuple of 

357 ``index -> [(priority, MetaStep), ...]`` maps. Each map holds the actions 

358 to emit immediately before and immediately after the top-level step at 

359 that index. 

360 """ 

361 fwd_index, bwd_index, container_by_compute_index = _index_compute_leaves(order) 

362 

363 maps = (defaultdict(list), defaultdict(list)) 

364 for key, fwd_leaf in fwd_index.items(): 

365 bwd_leaf = bwd_index.get(key) 

366 if bwd_leaf is None: 

367 continue 

368 actions = _chunk_swap_actions(order, fwd_leaf, bwd_leaf, container_by_compute_index) 

369 if actions is None: 

370 continue 

371 for bucket, action_group in enumerate(actions): 

372 for anchor, priority, swap_step in action_group: 

373 maps[bucket][anchor].append((priority, swap_step)) 

374 return maps 

375 

376 

377def inject_pipeline_swap_steps(order: List[Any]) -> List[Any]: 

378 """Inject asynchronous transfer steps into one rank's pipeline order. 

379 

380 Forward collection is executed directly by the forward leaf executor. 

381 Transfer launch/wait actions, including the H2D wait before the backward 

382 consumer container, appear in the top-level order. 

383 """ 

384 before_steps, after_steps = _collect_swap_actions(order) 

385 

386 injected = [] 

387 for index, step in enumerate(order): 

388 injected.extend(_iter_steps_by_priority(before_steps[index])) 

389 injected.append(step) 

390 injected.extend(_iter_steps_by_priority(after_steps[index])) 

391 return injected 

392 

393 

394def _protect_pipeline_owned_tensors(step, schedule, arg_mbs, kwarg_mbs, group_name: str) -> None: 

395 """Keep long-lived module and pipeline-owned tensors alive on device. 

396 

397 Swap offload clears the device storage of saved tensors after D2H copy. 

398 If a saved tensor aliases a parameter, registered buffer, or pipeline 

399 boundary tensor, clearing it would invalidate the long-lived owner. The 

400 alias protection below marks those saved tensors as keep-on-device. 

401 """ 

402 stage = schedule._stage_dict[step.stage_index] # pylint: disable=protected-access 

403 manager = SwapManager() 

404 

405 # Saved-tensor hooks may receive a plain Tensor view of a Parameter. Some 

406 # backends do not preserve parameter metadata on that view, so protect by 

407 # storage ownership before any group member can be resized. 

408 parameters = tuple(stage.submodule.parameters()) 

409 buffers = tuple(stage.submodule.buffers()) 

410 if stage.is_first_stage: 

411 # First-stage inputs come from split_microbatches(), outside the 

412 # wrapped stage. They are not stage outputs, but they can alias 

413 # tensors saved by the first layer and must not have their storage 

414 # resized by the swap group. 

415 boundary_inputs = (arg_mbs[step.micro_index], kwarg_mbs[step.micro_index]) 

416 else: 

417 recv_infos = stage.args_recv_info.get(step.micro_index, ()) 

418 boundary_inputs = tuple(info.buffer for info in recv_infos if info.buffer is not None) 

419 

420 # Protect parameters, registered buffers, and pipeline-owned inputs in one 

421 # pass over the collected saved tensors. Forward outputs are protected 

422 # earlier by execute_fwd_leaf(), while the direct return value is still 

423 # available. 

424 manager.protect_alias_tensors(group_name, (parameters, buffers, boundary_inputs)) 

425 

426 

427def swap_launch_offload( 

428 step: Any, 

429 schedule: Any, 

430 arg_mbs: List[Any], 

431 kwarg_mbs: List[Any], 

432 session: PipelineSwapSession) -> None: 

433 """Launch D2H for a pipeline swap group.""" 

434 session.require_forward_context(step) 

435 group_name = session.group_name(step) 

436 manager = SwapManager() 

437 _protect_pipeline_owned_tensors(step, schedule, arg_mbs, kwarg_mbs, group_name) 

438 manager.launch_offload(group_name) 

439 

440 

441def swap_wait_offload(step: Any, session: PipelineSwapSession) -> None: 

442 """Wait for a pipeline swap group's D2H and release device storage.""" 

443 SwapManager().wait_offload(session.group_name(step)) 

444 

445 

446def swap_launch_load(step: Any, session: PipelineSwapSession) -> None: 

447 """Launch H2D for a pipeline swap group.""" 

448 SwapManager().launch_load(session.group_name(step)) 

449 

450 

451def swap_wait_load(step: Any, session: PipelineSwapSession) -> None: 

452 """Wait for a pipeline swap group's H2D before its backward container.""" 

453 session.wait_load(step)