Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / core / pipeline_parallel / pipeline_swap.py: 0%
146 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-07-06 05:41 +0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-07-06 05:41 +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."""
17from collections import defaultdict
18from enum import IntEnum
20from hyper_parallel.core.activation_checkpoint.swap import SwapManager
22MIN_SWAP_GAP = 4
25class _BeforeActionPriority(IntEnum):
26 SET_GROUP = 10
27 WAIT_LOAD = 20
28 LAUNCH_LOAD = 30
31class _AfterActionPriority(IntEnum):
32 WAIT_OFFLOAD = 10
33 LAUNCH_OFFLOAD = 20
36def pp_swap_group_name(stage_index: int, micro_index: int) -> str:
37 """Return the swap group name for a pipeline chunk."""
38 return f"pp_swap_s{stage_index}_m{micro_index}"
41def _is_compute_step(step) -> bool:
42 from hyper_parallel.core.pipeline_parallel.scheduler import MetaStepType # pylint: disable=C0415
44 return step is not None and step.type in (MetaStepType.FWD, MetaStepType.BWD)
47def _is_comm_step(step) -> bool:
48 from hyper_parallel.core.pipeline_parallel.scheduler import MetaStepType # pylint: disable=C0415
50 return step is not None and step.type in (
51 MetaStepType.FWD_RECV,
52 MetaStepType.FWD_SEND,
53 MetaStepType.BWD_RECV,
54 MetaStepType.BWD_SEND,
55 )
58def _is_composite_compute_step(step) -> bool:
59 from hyper_parallel.core.pipeline_parallel.scheduler import MetaStepType # pylint: disable=C0415
61 return (
62 step is not None
63 and step.type in (MetaStepType.OVERLAP_F_B, MetaStepType.OVERLAP_B_F)
64 and step.sub_steps
65 )
68class _ComputeLeaf:
69 """A real FWD/BWD leaf and the top-level container that owns it."""
71 __slots__ = ("step", "container_index", "compute_index")
73 def __init__(self, step, container_index, compute_index):
74 self.step = step
75 self.container_index = container_index
76 self.compute_index = compute_index
79def _iter_compute_leaf_steps(step):
80 """Yield real FWD/BWD steps, expanding composite containers."""
81 if _is_compute_step(step):
82 yield step
83 return
84 if _is_composite_compute_step(step):
85 for sub_step in step.sub_steps:
86 if _is_compute_step(sub_step):
87 yield sub_step
90def _collect_compute_leaves(order):
91 """Collect compute leaves while counting each composite as one slot."""
92 leaves = []
93 container_by_compute_index = {}
94 compute_index = 0
95 for container_index, step in enumerate(order):
96 leaf_steps = list(_iter_compute_leaf_steps(step))
97 if not leaf_steps:
98 continue
99 container_by_compute_index[compute_index] = container_index
100 for leaf_step in leaf_steps:
101 leaves.append(_ComputeLeaf(leaf_step, container_index, compute_index))
102 compute_index += 1
103 return leaves, container_by_compute_index
106def _append_after(after_steps, index, priority, step):
107 after_steps[index].append((priority, step))
110def _append_before(before_steps, index, priority, step):
111 before_steps[index].append((priority, step))
114def _iter_steps_by_priority(priority_steps):
115 """Yield steps from high priority to low priority."""
116 for _, step in sorted(priority_steps, key=lambda item: item[0], reverse=True):
117 yield step
120def _comm_block_anchor(order, index):
121 """Return the last immediately following communication step."""
122 anchor = index
123 for next_index in range(index + 1, len(order)):
124 next_step = order[next_index]
125 if _is_comm_step(next_step):
126 anchor = next_index
127 continue
128 break
129 return anchor
132def _post_compute_anchor(order, index, leaf_step=None):
133 """Return the safe index after which post-compute swap steps may run."""
134 from hyper_parallel.core.pipeline_parallel.scheduler import MetaStepType # pylint: disable=C0415
136 step = leaf_step if leaf_step is not None else order[index]
137 fallback_anchor = _comm_block_anchor(order, index)
138 if step.type == MetaStepType.FWD:
139 send_type = MetaStepType.FWD_SEND
140 elif step.type == MetaStepType.BWD:
141 send_type = MetaStepType.BWD_SEND
142 else:
143 return fallback_anchor
145 for next_index in range(index + 1, fallback_anchor + 1):
146 next_step = order[next_index]
147 next_valid = (next_step is not None and next_step.type == send_type
148 and next_step.stage_index == step.stage_index and next_step.micro_index == step.micro_index)
149 if next_valid:
150 return next_index
151 return fallback_anchor
154def inject_pipeline_swap_steps(order):
155 """Inject SWAP_* steps into one rank's pipeline order.
157 The injected order preserves the required lifecycle:
158 SET_GROUP -> FWD -> LAUNCH_OFFLOAD -> WAIT_OFFLOAD ->
159 LAUNCH_LOAD -> WAIT_LOAD -> BWD.
160 """
161 from hyper_parallel.core.pipeline_parallel.scheduler import MetaStep, MetaStepType # pylint: disable=C0415
163 fwd_index = {}
164 bwd_index = {}
165 compute_leaves, container_by_compute_index = _collect_compute_leaves(order)
166 for leaf in compute_leaves:
167 step = leaf.step
168 key = (step.stage_index, step.micro_index)
169 if step.type == MetaStepType.FWD:
170 fwd_index[key] = leaf
171 elif step.type == MetaStepType.BWD:
172 bwd_index[key] = leaf
174 before_steps = defaultdict(list)
175 after_steps = defaultdict(list)
176 for key, fwd_leaf in fwd_index.items():
177 bwd_leaf = bwd_index.get(key)
178 if bwd_leaf is None:
179 continue
180 if bwd_leaf.compute_index - fwd_leaf.compute_index < MIN_SWAP_GAP:
181 continue
182 compute_between = [
183 container_by_compute_index[index]
184 for index in range(fwd_leaf.compute_index + 1, bwd_leaf.compute_index)
185 ]
186 if not compute_between:
187 continue
188 stage_index, micro_index = key
190 _append_before(
191 before_steps, fwd_leaf.container_index, _BeforeActionPriority.SET_GROUP,
192 MetaStep(micro_index, MetaStepType.SWAP_SET_GROUP, stage_index),
193 )
194 fwd_anchor = _post_compute_anchor(order, fwd_leaf.container_index, fwd_leaf.step)
195 first_between_anchor = _post_compute_anchor(order, compute_between[0])
197 _append_after(
198 after_steps, fwd_anchor, _AfterActionPriority.LAUNCH_OFFLOAD,
199 MetaStep(micro_index, MetaStepType.SWAP_LAUNCH_OFFLOAD, stage_index),
200 )
202 _append_after(
203 after_steps, first_between_anchor, _AfterActionPriority.WAIT_OFFLOAD,
204 MetaStep(micro_index, MetaStepType.SWAP_WAIT_OFFLOAD, stage_index),
205 )
207 _append_before(
208 before_steps, compute_between[-1], _BeforeActionPriority.LAUNCH_LOAD,
209 MetaStep(micro_index, MetaStepType.SWAP_LAUNCH_LOAD, stage_index),
210 )
211 _append_before(
212 before_steps, bwd_leaf.container_index, _BeforeActionPriority.WAIT_LOAD,
213 MetaStep(micro_index, MetaStepType.SWAP_WAIT_LOAD, stage_index),
214 )
216 injected = []
217 for index, step in enumerate(order):
218 injected.extend(_iter_steps_by_priority(before_steps[index]))
219 injected.append(step)
220 injected.extend(_iter_steps_by_priority(after_steps[index]))
221 return injected
224def _protect_pipeline_owned_tensors(step, schedule, arg_mbs, kwarg_mbs) -> None:
225 """Keep pipeline-owned boundary tensors alive on device.
227 Swap offload clears the device storage of saved tensors after D2H copy.
228 If a saved tensor aliases a pipeline boundary tensor, clearing it would
229 also invalidate the object still held by the pipeline runtime. The alias
230 protection below marks those saved tensors as keep-on-device.
231 """
232 stage = schedule._stage_dict[step.stage_index] # pylint: disable=protected-access
233 group_name = pp_swap_group_name(step.stage_index, step.micro_index)
234 manager = SwapManager()
236 if stage.is_first_stage:
237 # First-stage inputs come from split_microbatches(), outside the
238 # wrapped stage. They are not stage outputs, but they can alias
239 # tensors saved by the first layer and must not have their storage
240 # resized by the swap group.
241 manager.protect_alias_tensors(
242 group_name,
243 (arg_mbs[step.micro_index], kwarg_mbs[step.micro_index]),
244 )
246 if stage.is_last_stage:
247 # Last-stage outputs are consumed by the schedule as losses / sens
248 # roots, so they must stay device-valid until backward has used them.
249 outputs = stage.fwd_outputs_cache.get(step.micro_index)
250 if outputs is None:
251 outputs = stage.last_stage_outputs
252 if outputs is not None:
253 manager.protect_alias_tensors(group_name, outputs)
256def swap_set_group(step) -> None:
257 """Set the active SwapManager group for the next pipeline forward chunk."""
258 group_name = pp_swap_group_name(step.stage_index, step.micro_index)
259 manager = SwapManager()
260 manager.ensure_group(group_name)
261 manager.set_current_group_name(group_name)
264def swap_launch_offload(step, schedule, arg_mbs, kwarg_mbs) -> None:
265 """Launch D2H for a pipeline swap group."""
266 group_name = pp_swap_group_name(step.stage_index, step.micro_index)
267 manager = SwapManager()
268 _protect_pipeline_owned_tensors(step, schedule, arg_mbs, kwarg_mbs)
269 manager.launch_offload(group_name)
270 # Only the immediately preceding forward belongs to this swap group.
271 manager.set_current_group_name("")
274def swap_wait_offload(step) -> None:
275 """Wait for a pipeline swap group's D2H and release device storage."""
276 SwapManager().wait_offload(pp_swap_group_name(step.stage_index, step.micro_index))
279def swap_launch_load(step) -> None:
280 """Launch H2D for a pipeline swap group."""
281 SwapManager().launch_load(pp_swap_group_name(step.stage_index, step.micro_index))
284def swap_wait_load(step) -> None:
285 """Wait for a pipeline swap group's H2D before backward uses activations."""
286 SwapManager().wait_load(pp_swap_group_name(step.stage_index, step.micro_index))