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

1120 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-07-13 05:07 +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 schedule""" 

16from abc import ABC, abstractmethod 

17from enum import Enum, auto 

18from collections import defaultdict 

19import itertools 

20import bisect 

21import logging 

22import re 

23import hyper_parallel 

24from hyper_parallel.platform import get_platform 

25from hyper_parallel.core.fully_shard.api import HSDPModule 

26from hyper_parallel.core.pipeline_parallel.utils import BatchDimSpec 

27platform = get_platform() 

28logger = logging.getLogger(__name__) 

29 

30 

31class MetaStepType(Enum): 

32 """Specify the enumeration type for MetaStep.""" 

33 FWD = auto() 

34 BWD = auto() 

35 BWD_INPUT = auto() 

36 BWD_WEIGHT = auto() 

37 FWD_RECV = auto() 

38 FWD_SEND = auto() 

39 BWD_RECV = auto() 

40 BWD_SEND = auto() 

41 # Composite P2P: a contiguous run of FWD_SEND/FWD_RECV/BWD_SEND/BWD_RECV 

42 # coalesced by ``coalesce_p2p`` into one step whose ``sub_steps`` the runtime 

43 # groups by peer and issues as ``batch_isend_irecv`` (same-peer send+recv -> 

44 # duplex). Only produced under ``p2p_transport="batch"``. 

45 BATCH_SEND_RECV = auto() 

46 OVERLAP_F_B = auto() 

47 OVERLAP_B_F = auto() 

48 FSDP_UNSHARD = auto() 

49 FSDP_RESHARD = auto() 

50 FSDP_REDUCE_GRAD = auto() 

51 SWAP_SET_GROUP = auto() 

52 SWAP_LAUNCH_OFFLOAD = auto() 

53 SWAP_WAIT_OFFLOAD = auto() 

54 SWAP_LAUNCH_LOAD = auto() 

55 SWAP_WAIT_LOAD = auto() 

56 

57 

58class MetaStep: 

59 """ 

60 Meta step of PipelineSchedule. 

61 An execution list composed of MetaStep can be constructed 

62 and fed into the PipelineSchedule for execution. 

63 

64 Args: 

65 micro_index (int | None): The index of micro-batch. ``None`` for 

66 composite types (``OVERLAP_F_B`` / ``OVERLAP_B_F``) whose real 

67 micro index lives in each ``sub_steps`` entry. 

68 type (MetaStepType): Specify the type of current step. 

69 stage_index (int | None): Stage index of current step. ``None`` 

70 for composite types; use ``sub_steps`` to get each direction's 

71 stage. 

72 sub_steps (tuple[MetaStep, MetaStep] | None): For composite types 

73 only: ``(fwd, bwd)`` for ``OVERLAP_F_B``, ``(bwd, fwd)`` for 

74 ``OVERLAP_B_F``. 

75 boundary_p2p (tuple[MetaStep, ...] | None): For ``OVERLAP_B_F`` under 

76 the ``"boundary"`` P2P transport only: P2P steps to issue at the 

77 fwd/bwd boundary inside the overlap (the forward's ``FWD_SEND`` 

78 plus the next slot's recvs), hoisted out of the following gap by 

79 ``attach_fwd_boundary_p2p``. Issued via 

80 :meth:`PipelineScheduleRuntime.exec_boundary_p2p`. 

81 """ 

82 def __init__(self, micro_index, meta_type, stage_index, sub_steps=None, 

83 boundary_p2p=None): 

84 self._type = meta_type 

85 self._micro_index = micro_index 

86 self._stage_index = stage_index 

87 self._sub_steps = sub_steps 

88 self._boundary_p2p = boundary_p2p 

89 

90 @property 

91 def micro_index(self): 

92 """Return the micro-batch index of this step.""" 

93 return self._micro_index 

94 

95 @property 

96 def stage_index(self): 

97 """Return the stage index of this step.""" 

98 return self._stage_index 

99 

100 @property 

101 def type(self): 

102 """Return the MetaStepType of this step.""" 

103 return self._type 

104 

105 @property 

106 def sub_steps(self): 

107 """Sub-steps for composite types: ``(fwd, bwd)`` for OVERLAP_F_B, 

108 ``(bwd, fwd)`` for OVERLAP_B_F, or ``None``.""" 

109 return self._sub_steps 

110 

111 @property 

112 def boundary_p2p(self): 

113 """P2P steps to issue at the overlap's fwd/bwd boundary, or ``None``.""" 

114 return self._boundary_p2p 

115 

116 def __eq__(self, value): 

117 if not isinstance(value, MetaStep): 

118 return NotImplemented 

119 return (self.type == value.type 

120 and self.micro_index == value.micro_index 

121 and self.stage_index == value.stage_index 

122 and self.sub_steps == value.sub_steps) 

123 

124 def __ne__(self, value): 

125 if not isinstance(value, MetaStep): 

126 return NotImplemented 

127 return not self.__eq__(value) 

128 

129 def __hash__(self): 

130 return hash((self.type, self.micro_index, self.stage_index)) 

131 

132 def __str__(self): 

133 if self.sub_steps: 

134 sub = ", ".join(str(s) for s in self.sub_steps) 

135 return (f"MetaStep(type={self.type}, micro_index={self.micro_index}, " 

136 f"stage_index={self.stage_index}, sub_steps=[{sub}])") 

137 return f"MetaStep(type={self.type}, micro_index={self.micro_index}, stage_index={self.stage_index})" 

138 

139 def __repr__(self): 

140 return self.__str__() 

141 

142 @staticmethod 

143 def from_str(step_str): 

144 """Parse a MetaStep from its string representation.""" 

145 pass 

146 

147 

148def generate_stage_to_rank_mapping(real_stage_num, stage_num, style='loop'): 

149 """Generate stage to rank mapping for loop or V schedules.""" 

150 if style == 'loop': 

151 return {stage_index: stage_index % real_stage_num for stage_index in range(stage_num)} 

152 if style == 'v': 

153 if stage_num % real_stage_num != 0: 

154 raise ValueError( 

155 f"stage_num {stage_num} must be evenly divisible by real_stage_num {real_stage_num} for V schedules." 

156 ) 

157 mapping = {} 

158 rank_index = 0 

159 for stage_index in range(stage_num): 

160 mapping[stage_index] = rank_index 

161 if (stage_index + 1) % real_stage_num == 0: 

162 continue 

163 if (stage_index // real_stage_num) % 2 == 0: 

164 rank_index += 1 

165 else: 

166 rank_index -= 1 

167 return mapping 

168 raise ValueError(f"Unsupported stage rank mapping style: {style}") 

169 

170 

171def generate_rank_to_stage_mapping(real_stage_num, stage_num, style='loop'): 

172 """Invert the stage to rank mapping.""" 

173 stage_to_rank = generate_stage_to_rank_mapping(real_stage_num, stage_num, style) 

174 rank_to_stages = defaultdict(list) 

175 for stage_index, rank in stage_to_rank.items(): 

176 rank_to_stages[rank].append(stage_index) 

177 

178 for stages in rank_to_stages.values(): 

179 stages.sort() 

180 return dict(rank_to_stages) 

181 

182 

183def iter_leaf_meta_steps(step): 

184 """Yield leaf MetaSteps, recursively expanding OVERLAP containers. 

185 

186 Both ``OVERLAP_F_B`` and ``OVERLAP_B_F`` carry their real FWD/BWD work in 

187 ``sub_steps``; the FSDP unshard/reshard injection pass relies on this to 

188 see the FWD/BWD buried inside an overlap. Missing ``OVERLAP_B_F`` here let 

189 an overlapped FWD run against a resharded stage → "expected HSDPModule 

190 parameters in unsharded state". Mirror the other expansion sites 

191 (run/_expand/add_fsdp_*) which already handle both composite types. 

192 """ 

193 if step is None: 

194 return 

195 if step.type in (MetaStepType.OVERLAP_F_B, MetaStepType.OVERLAP_B_F) and step.sub_steps: 

196 for sub_step in step.sub_steps: 

197 

198 yield from iter_leaf_meta_steps(sub_step) 

199 return 

200 yield step 

201 

202 

203class PipelineContext: 

204 """Per-run state handed to a custom execution function (see 

205 :meth:`PipelineScheduleRuntime.register_custom_function`). 

206 

207 A plain data carrier for one :meth:`PipelineScheduleRuntime.run_microbatches` 

208 call. The P2P helpers (``wait_fwd_recv`` / ``wait_bwd_recv`` / ``send_fwd`` 

209 / ``send_bwd``) and the ``enable_dxdw_split`` flag live on the schedule, so a 

210 callback reaches them through :attr:`schedule`, e.g. 

211 ``ctx.schedule.send_bwd(stage, micro_index)``. 

212 

213 Attributes: 

214 schedule: The owning :class:`PipelineScheduleRuntime`. 

215 arg_mbs: Per-micro-batch positional args. 

216 kwarg_mbs: Per-micro-batch keyword args. 

217 losses: Mutable list collecting per-step losses. 

218 """ 

219 

220 def __init__(self, schedule: "PipelineScheduleRuntime", arg_mbs: list, 

221 kwarg_mbs: list, losses: list) -> None: 

222 """Bundle the active schedule with one run's micro-batch inputs and losses.""" 

223 self.schedule = schedule 

224 self.arg_mbs = arg_mbs 

225 self.kwarg_mbs = kwarg_mbs 

226 self.losses = losses 

227 

228 

229def _exec_fsdp_unshard(stage): 

230 """Unshard every HSDPModule in the stage's submodule tree.""" 

231 for _, module in platform.get_cells_and_names(stage.submodule): 

232 if isinstance(module, HSDPModule): 

233 module.unshard() 

234 

235 

236def _exec_fsdp_reshard(stage): 

237 """Reshard every HSDPModule in the stage's submodule tree.""" 

238 for _, module in platform.get_cells_and_names(stage.submodule): 

239 if isinstance(module, HSDPModule): 

240 module.reshard() 

241 

242 

243def _exec_fsdp_reduce_grad(stage): 

244 """Run the stage's FSDP post-backward gradient reduction.""" 

245 stage.execute_reduce_grad() 

246 

247 

248# FSDP control MetaStep -> handler(stage). Membership also marks which 

249# MetaStepTypes are FSDP control steps, so the runtime loop dispatches with a 

250# single table lookup instead of re-switching on the step type. 

251_FSDP_STEP_HANDLERS = { 

252 MetaStepType.FSDP_UNSHARD: _exec_fsdp_unshard, 

253 MetaStepType.FSDP_RESHARD: _exec_fsdp_reshard, 

254 MetaStepType.FSDP_REDUCE_GRAD: _exec_fsdp_reduce_grad, 

255} 

256 

257 

258class PipelineScheduleRuntime(ABC): 

259 """ 

260 Base class for pipeline schedule. 

261 Implements the `split_microbatches` and `run_microbatches` method. 

262 Derived classes should implement `run_microbatches` method and `run` method. 

263 

264 Supports registering **custom execution functions** for any 

265 :class:`MetaStepType` via :meth:`register_custom_function`. When 

266 ``run_microbatches`` encounters a step whose type has a registered 

267 handler, it creates a :class:`PipelineContext` and delegates execution 

268 to the handler instead of using the built-in logic. 

269 

270 Args: 

271 stages (list[PipelineStage], PipelineStage): PipelineStage used to run_microbatches. 

272 micro_batch_num (int): The number of micro-batch. 

273 args_batch_dim (int | BatchDimSpec | list | tuple, optional): Per 

274 positional-arg batch dim, indexed by arg position. Entries may be 

275 plain ``int`` (or ``None`` to keep the default); a single-input 

276 model may pass a bare ``int``/``BatchDimSpec`` instead of a 

277 one-element list (wrapped automatically). Default ``None``. 

278 kwargs_batch_dim (dict, optional): Per keyword-arg batch dim, mapping 

279 arg name to a plain ``int`` or ``BatchDimSpec``. Default ``None``. 

280 swap (bool, optional): Whether to inject pipeline activation swap 

281 control steps. Supported by ``ScheduleGPipe``, ``Schedule1F1B``, 

282 and ``ScheduleInterleaved1F1B``. Default ``False``. 

283 p2p_transport (str, optional): How pipeline send/recv are issued. 

284 ``"auto"`` (default) — gap-time duplex batching on overlap_b_f 

285 schedules (``coalesce_p2p``: same-peer send+recv as one 

286 ``batch_isend_irecv``, TX||RX; hardware-validated and measured a 

287 net win on real workloads), plain per-op ``isend``/``irecv`` 

288 everywhere else. ``"plain"`` — force per-op ``isend``/``irecv`` 

289 (escape hatch for transports or topologies where batching 

290 misbehaves). ``"batch"`` — duplex batching explicitly (what 

291 ``"auto"`` picks under overlap_b_f). ``"boundary"`` — 

292 EXPERIMENTAL fwd-boundary batching: each overlap's ``F_SEND`` + 

293 the next slot's recvs go out mid-overlap, right after the forward, 

294 as per-op solo batches; only ``B_SEND`` waits for the backward. 

295 Avoids the duplex handle's send-coupling (a2a-friendly) and posts 

296 the activation send ~half a slot early, but is not yet 

297 hardware-validated — opt in deliberately. Must be set identically 

298 on every rank — HCCL cannot match a batched op against a plain one 

299 (EI0005). 

300 """ 

301 

302 _P2P_TRANSPORTS = ("auto", "plain", "batch", "boundary") 

303 

304 def __init__(self, 

305 stages, 

306 micro_batch_num, 

307 args_batch_dim=None, 

308 kwargs_batch_dim=None, 

309 output_concat_dim=None, 

310 overlap_p2p=False, 

311 swap=False, 

312 p2p_transport="auto"): 

313 if p2p_transport not in self._P2P_TRANSPORTS: 

314 raise ValueError( 

315 f"p2p_transport must be one of {self._P2P_TRANSPORTS}, got " 

316 f"{p2p_transport!r}" 

317 ) 

318 self.stages = self._check_stages(stages) 

319 self.micro_batch_num = micro_batch_num 

320 self._args_batch_dim = self._normalize_args_batch_dim(args_batch_dim) 

321 self._kwargs_batch_dim = self._normalize_kwargs_batch_dim(kwargs_batch_dim) 

322 self._output_concat_dim = output_concat_dim 

323 self.split_micro_batch = platform.micro_batch(self.micro_batch_num, 

324 self._args_batch_dim, self._kwargs_batch_dim) 

325 self.n_local_stages = len(self.stages) 

326 self._stage_dict = self.convert_stages_dict() 

327 self.real_stage_num = self.stages[0].stage_num // self.n_local_stages 

328 self._stage_num = self.stages[0].stage_num 

329 self._stage_to_rank_index = None 

330 self._overlap_p2p = overlap_p2p 

331 self.exec_order = {} 

332 self._init_stages() 

333 self._build_stage_to_rank_index() 

334 self.fwd_handle_cache = {} 

335 self.bwd_handle_cache = {} 

336 self._custom_fn_map = {} 

337 self._pp_swap_enabled = swap 

338 # Outstanding async send handle groups for the in-flight 

339 # ``run_microbatches`` call; reset per run and drained at its end. 

340 self._send_handles = [] 

341 # ``p2p_transport`` resolves in ``build_exec_order`` (it needs the 

342 # subclass's ``_overlap_b_f``) to one of: 

343 # 

344 # * ``"batch"`` (the ``"auto"`` default on overlap_b_f schedules) — 

345 # gap-time duplex via ``coalesce_p2p``: same-peer send+recv as one 

346 # ``batch_isend_irecv`` (TX||RX on the full-duplex link). 

347 # Hardware-validated and MEASURED a net win on real workloads — the 

348 # duplex saving outweighs its known cost (MS's single handle couples 

349 # the riding send into the compute-gating recv wait, which can 

350 # shave EP a2a overlap). 

351 # * ``"plain"`` (the ``"auto"`` default elsewhere) — per-op 

352 # ``isend``/``irecv``, the upstream-original path. 

353 # * ``"boundary"`` (EXPERIMENTAL, explicit opt-in only) — fwd-boundary 

354 # batching. ``attach_fwd_boundary_p2p`` hangs each steady gap's 

355 # ``F_SEND`` (its data is ready when the overlap's forward finishes 

356 # — the backward, ~2x FLOPs, is the long pole) plus the next slot's 

357 # recvs on the OVERLAP_B_F step; the stage's after-forward hook 

358 # fires ``exec_boundary_p2p`` at the fwd/bwd boundary, while the 

359 # backward is still running. The send leaves roughly half a slot 

360 # early — the only mode that moves the SENDER's post time — and the 

361 # recv handles carry no send (a2a-friendly). Every op is a per-op 

362 # solo batch and ``coalesce_p2p`` is NOT run: a hoisted recv cannot 

363 # stay duplexed with a send that is only ready later, and 

364 # asymmetric shapes (one end duplex, other end split) hang — 

365 # all-solo keeps per-pair batch sequences complementary 

366 # ([S,R] vs [R,S]), safe under both candidate HCCL pairing 

367 # semantics. Promote to the auto default only after it earns both 

368 # a hardware accuracy pass and a perf win over "batch". 

369 # 

370 # Two transport invariants for any future rewrite: plain per-pair 

371 # streams need the gap's recv-first/send-first complementarity (a 

372 # send-crossing hoist made both ends recv-first -> rendezvous deadlock, 

373 # 2026-06), and batch pairing needs per-pair shape mirroring (a 

374 # one-sided split made 2 solos face 1 duplex -> hang, 2026-06). 

375 self._p2p_transport = p2p_transport 

376 # Effective mode + per-op batch gating; set by ``build_exec_order``. 

377 self._p2p_mode = None 

378 self._batch_p2p = False 

379 # OVERLAP steps whose boundary_p2p was already issued this run (the 

380 # stage after-forward hook and the post-step safety net are both 

381 # allowed to call exec_boundary_p2p; reset per run_microbatches call). 

382 self._boundary_issued = set() 

383 # (fwd stage_index, micro_index) -> armed OVERLAP step, consumed by the 

384 # stage after-forward hook to fire the boundary issue mid-overlap. 

385 self._pending_boundary = {} 

386 

387 def register_custom_function(self, step_type: MetaStepType, fn) -> None: 

388 """Register a custom execution function for the given step type. 

389 

390 When :meth:`run_microbatches` encounters a :class:`MetaStep` whose 

391 ``type`` matches ``step_type``, it calls ``fn(step, ctx)`` instead 

392 of the built-in logic. 

393 

394 Args: 

395 step_type: The :class:`MetaStepType` to intercept. 

396 fn: A callable with signature ``(step: MetaStep, ctx: PipelineContext) -> None``. 

397 

398 Example: 

399 >>> def my_overlap_callback(step, ctx): 

400 ... fwd_step, bwd_step = step.sub_steps 

401 ... # custom parallel execution logic 

402 >>> schedule.register_custom_function(MetaStepType.OVERLAP_F_B, my_overlap_callback) 

403 """ 

404 self._custom_fn_map[step_type] = fn 

405 

406 def _inject_local_fsdp_actions(self): 

407 """Annotate the local rank schedule with optional FSDP control actions.""" 

408 current_rank = self._stage_to_rank_index[self.stages[0].stage_index] 

409 managed_stage_indices = { 

410 stage.stage_index 

411 for stage in self.stages 

412 if any( 

413 isinstance(module, HSDPModule) 

414 for _, module in platform.get_cells_and_names(stage.submodule) 

415 ) 

416 } 

417 if not managed_stage_indices: 

418 return 

419 if len(managed_stage_indices) != len(self.stages): 

420 raise RuntimeError( 

421 "When injecting fsdp_action, expect all stages to be HSDPModule. " 

422 "Check whether all separated modules are wrapped with 'fully_shard'." 

423 ) 

424 rank_actions = add_fsdp_unshard_reshard(self.exec_order[current_rank], managed_stage_indices) 

425 self.exec_order[current_rank] = add_fsdp_reduce_grad( 

426 rank_actions, 

427 managed_stage_indices, 

428 self.micro_batch_num, 

429 ) 

430 

431 def _inject_local_pp_swap_actions(self): 

432 """Annotate the local rank schedule with pipeline activation-swap actions.""" 

433 if not self._pp_swap_enabled: 

434 return 

435 current_rank = self._stage_to_rank_index[self.stages[0].stage_index] 

436 from hyper_parallel.core.pipeline_parallel.pipeline_swap import ( # pylint: disable=C0415 

437 inject_pipeline_swap_steps, 

438 ) 

439 self.exec_order[current_rank] = inject_pipeline_swap_steps(self.exec_order[current_rank]) 

440 

441 @abstractmethod 

442 def _build_stage_to_rank_index(self) -> None: 

443 """ 

444 Build attribute of _stage_to_rank_index. 

445 Each subclass constructs it according to its own schedule style. 

446 """ 

447 

448 @abstractmethod 

449 def construct_exec_order(self) -> None: 

450 """Build exec order, PP cmopute and PP comms(Send/Recv)""" 

451 

452 def build_exec_order(self) -> None: 

453 """Build the execution order and inject optional PP-swap/FSDP actions. 

454 

455 Also resolves ``p2p_transport``: ``"auto"`` becomes ``"batch"`` (the 

456 measured-beneficial duplex) on schedules running with ``overlap_b_f`` 

457 and ``"plain"`` everywhere else, then the matching order-rewrite pass 

458 runs last (after swap/FSDP injection) so it sees the final per-rank 

459 order. 

460 """ 

461 mode = self._p2p_transport 

462 if mode == "auto": 

463 mode = "batch" if getattr(self, "_overlap_b_f", False) else "plain" 

464 self._p2p_mode = mode 

465 self._batch_p2p = mode != "plain" 

466 self.construct_exec_order() 

467 self._inject_local_pp_swap_actions() 

468 self._inject_local_fsdp_actions() 

469 if mode == "boundary": 

470 # fwd-boundary mode: hang the forward's F_SEND + next slot's recvs 

471 # on the OVERLAP step (issued mid-overlap, right after the 

472 # forward). Everything stays per-op solo batches — deliberately 

473 # NO coalesce_p2p (see __init__). 

474 self.exec_order = attach_fwd_boundary_p2p(self.exec_order) 

475 elif mode == "batch": 

476 # Coalesce contiguous P2P runs into BATCH_SEND_RECV so the runtime 

477 # issues same-peer send+recv as one duplex batch. NOTE: couples 

478 # the riding send into the compute-gating recv wait — see 

479 # __init__. 

480 self.exec_order = coalesce_p2p(self.exec_order) 

481 

482 def convert_stages_dict(self): 

483 """convert stages to dict.""" 

484 stage_dict = {} 

485 for stage in self.stages: 

486 stage_dict[stage.stage_index] = stage 

487 return stage_dict 

488 

489 def split_microbatches(self, args, kwargs): 

490 """split_microbatches.""" 

491 if args or kwargs: 

492 args_split, kwargs_split = self.split_micro_batch(args, kwargs) 

493 return args_split, kwargs_split 

494 return [[] for _ in range(self.micro_batch_num)], [{} for _ in range(self.micro_batch_num)] 

495 

496 @staticmethod 

497 def _to_spec(elem): 

498 """Normalize one batch-dim entry: ``int`` -> ``BatchDimSpec``. 

499 

500 ``None`` and ``BatchDimSpec`` pass through unchanged. ``bool`` is 

501 rejected even though it is an ``int`` subclass, so ``True``/``False`` 

502 are not silently read as dims 1/0. 

503 """ 

504 if elem is None or isinstance(elem, BatchDimSpec): 

505 return elem 

506 if isinstance(elem, int) and not isinstance(elem, bool): 

507 return BatchDimSpec(elem) 

508 raise TypeError( 

509 f"batch-dim entry must be int, BatchDimSpec or None, but got {type(elem)}.") 

510 

511 @staticmethod 

512 def _normalize_args_batch_dim(args_batch_dim): 

513 """Accept a plain ``int``/``BatchDimSpec`` or a ``list``/``tuple`` of them. 

514 

515 ``args_batch_dim`` is a per-arg spec indexed by positional-arg position 

516 (see ``_MicroBatch``). A single-input model can pass a bare ``int`` / 

517 ``BatchDimSpec`` instead of the awkward one-element 

518 ``BatchDimSpec.from_tuple((0,))``; elements may be plain ``int`` (or 

519 ``None`` to keep the default). Always returns ``None`` or a 

520 ``tuple[BatchDimSpec | None]`` so downstream per-arg indexing is 

521 unchanged. 

522 """ 

523 if args_batch_dim is None: 

524 return None 

525 if isinstance(args_batch_dim, BatchDimSpec) or \ 

526 (isinstance(args_batch_dim, int) and not isinstance(args_batch_dim, bool)): 

527 args_batch_dim = (args_batch_dim,) 

528 if isinstance(args_batch_dim, (list, tuple)): 

529 return tuple(PipelineScheduleRuntime._to_spec(e) for e in args_batch_dim) 

530 raise TypeError( 

531 f"args_batch_dim must be int, BatchDimSpec or a list/tuple of them, " 

532 f"but got {type(args_batch_dim)}.") 

533 

534 @staticmethod 

535 def _normalize_kwargs_batch_dim(kwargs_batch_dim): 

536 """Accept plain ``int`` dict values: ``{\"x\": 0}`` -> ``{\"x\": BatchDimSpec(0)}``. 

537 

538 ``kwargs_batch_dim`` maps each keyword-arg name to its batch dim. 

539 Returns ``None`` or a ``dict[str, BatchDimSpec | None]`` so downstream 

540 per-key indexing is unchanged. 

541 """ 

542 if kwargs_batch_dim is None: 

543 return None 

544 if not isinstance(kwargs_batch_dim, dict): 

545 raise TypeError( 

546 f"kwargs_batch_dim must be a dict[str, int | BatchDimSpec], " 

547 f"but got {type(kwargs_batch_dim)}.") 

548 return {k: PipelineScheduleRuntime._to_spec(v) for k, v in kwargs_batch_dim.items()} 

549 

550 def _check_stages(self, stages): 

551 """check stages type.""" 

552 if isinstance(stages, hyper_parallel.PipelineStage): 

553 return [stages] 

554 if isinstance(stages, (list, tuple)): 

555 for stage in stages: 

556 if not isinstance(stage, hyper_parallel.PipelineStage): 

557 raise TypeError(f"Argument 'stages' must be type of PipelineStage, \ 

558 list or tuple of PipelineStage, but got list or tuple of {type(stage)}.") 

559 return stages 

560 raise TypeError(f"Argument 'stages' must be type of PipelineStage, \ 

561 list or tuple of PipelineStage, but got type of {type(stages)}.") 

562 

563 def _init_stages(self): 

564 """init stages.""" 

565 for stage in self.stages: 

566 stage.init(self.n_local_stages) 

567 # After-forward hook: lets the schedule issue fwd-boundary P2P the 

568 # moment a forward chunk completes (no-op unless an OVERLAP step 

569 # with boundary_p2p was armed for that (stage, micro)). 

570 stage._after_forward_chunk = self._on_forward_chunk_done # pylint: disable=W0212 

571 

572 def _on_forward_chunk_done(self, stage_index, micro_index): 

573 """Stage after-forward hook: fire the armed boundary P2P, if any. 

574 

575 Runs on the thread executing the forward (the overlap callback's 

576 ``fwd_fn`` / the main thread), at the fwd/bwd boundary — the paired 

577 backward is still running, so the boundary ops overlap it. Keyed by 

578 the overlap's forward ``(stage_index, micro_index)``; unrelated 

579 forwards (warm-up steps, recompute re-runs of past micros) miss the 

580 key and no-op. 

581 """ 

582 step = self._pending_boundary.pop((stage_index, micro_index), None) 

583 if step is not None: 

584 self.exec_boundary_p2p(step) 

585 

586 def run(self, *args, **kwargs): 

587 """schedule run.""" 

588 losses = [] 

589 try: 

590 split_args, split_kwargs = self.split_microbatches(args, kwargs) 

591 self.run_microbatches(split_args, split_kwargs, losses) 

592 finally: 

593 # An exception unwinds past run_microbatches' end-of-iteration send 

594 # drain, leaving in-flight isend/irecv handles un-waited. Wait them 

595 # here so the comm contract holds on the error path too. No-op on the 

596 # normal path. See _drain_inflight_p2p. 

597 self._drain_inflight_p2p() 

598 return losses 

599 

600 def sync_shared_parameters_grad(self): 

601 """sync_shared_parameters_grad.""" 

602 for stage in self.stages: 

603 stage.sync_shared_parameters_grad() 

604 

605 def update_losses(self, stage, loss, losses): 

606 """update_losses.""" 

607 if stage.is_last_stage: 

608 losses.append(loss) 

609 

610 @property 

611 def enable_dxdw_split(self) -> bool: 

612 """Whether this schedule splits ``OVERLAP_B_F`` backward into dx/dw.""" 

613 return getattr(self, "_enable_dxdw_split", False) 

614 

615 def _wait_p2p(self, handles): 

616 for handle in handles: 

617 if handle is not None: 

618 handle.wait() 

619 

620 def _drain_inflight_p2p(self): 

621 """Wait every P2P handle still in flight — error-path cleanup. 

622 

623 run_microbatches waits its deferred sends only in the end-of-iteration 

624 drain; an exception mid-iteration unwinds past that drain, leaving issued 

625 isend/irecv handles un-waited in ``_send_handles`` and the recv caches 

626 (the ``CommHandle destroyed without calling wait()`` warning). run()'s 

627 finally calls this so every handle is still ``wait()``-ed — honoring the 

628 comm contract — on the error path too, and pops them so a later run() 

629 does not re-wait stale handles (the recv caches are never reset per run). 

630 No-op on the normal path: the drain already emptied ``_send_handles`` and 

631 every cached recv was consumed. 

632 """ 

633 while self._send_handles: 

634 self._wait_p2p(self._send_handles.pop()) 

635 while self.fwd_handle_cache: 

636 self._wait_p2p(self.fwd_handle_cache.popitem()[1]) 

637 while self.bwd_handle_cache: 

638 self._wait_p2p(self.bwd_handle_cache.popitem()[1]) 

639 

640 def _batched_issue(self, specs): 

641 """Launch same-peer P2P ``specs`` as one ``batch_isend_irecv`` group. 

642 

643 ``specs`` are ``(op_type, tensor, peer_global_rank)`` from the stage's 

644 ``*_specs`` builders (which carry the meta/bookkeeping side effects). 

645 Returns ``[handle]`` (the single batch handle) or ``[]`` — shaped like 

646 the per-op ``exec_*_ops`` return so the cache / drain paths are 

647 unchanged. Only the launch is coalesced; matching stays per-peer FIFO. 

648 """ 

649 if not specs: 

650 return [] 

651 ops = [platform.p2p_op(op_type, tensor, peer) for op_type, tensor, peer in specs] 

652 handle = platform.batch_isend_irecv(ops) 

653 return [handle] if handle is not None else [] 

654 

655 # --- P2P step primitives ------------------------------------------------ 

656 # One method per cross-rank comm action, used both by the runtime loop 

657 # (``_exec_step``) and by OVERLAP callbacks (via ``ctx.schedule``). With 

658 # ``overlap_p2p=True`` comm is decoupled from its compute: a recv caches its 

659 # handles for the consuming step to ``wait_*`` later, and a send defers its 

660 # handles to the end-of-iteration drain. With ``overlap_p2p=False`` every 

661 # op waits inline. 

662 

663 def recv_fwd(self, stage: "hyper_parallel.PipelineStage", micro_index: int) -> None: 

664 """Post the FWD recv for ``micro_index``; cache it (overlap_p2p) or wait now.""" 

665 handles = (self._batched_issue(stage.fwd_recv_specs(micro_index)) 

666 if self._batch_p2p else stage.exec_fwd_recv_ops(micro_index)) 

667 if self._overlap_p2p: 

668 self.fwd_handle_cache[(stage.stage_index, micro_index)] = handles 

669 else: 

670 self._wait_p2p(handles) 

671 

672 def recv_bwd(self, stage: "hyper_parallel.PipelineStage", micro_index: int) -> None: 

673 """Post the BWD recv for ``micro_index``; cache it (overlap_p2p) or wait now.""" 

674 handles = (self._batched_issue(stage.bwd_recv_specs(micro_index)) 

675 if self._batch_p2p else stage.exec_bwd_recv_ops(micro_index)) 

676 if self._overlap_p2p: 

677 self.bwd_handle_cache[(stage.stage_index, micro_index)] = handles 

678 else: 

679 self._wait_p2p(handles) 

680 

681 def wait_fwd_recv(self, stage_index: int, micro_index: int) -> None: 

682 """Wait the FWD recv cached by :meth:`recv_fwd`; no-op if nothing is cached.""" 

683 handles = self.fwd_handle_cache.pop((stage_index, micro_index), None) 

684 if handles: 

685 self._wait_p2p(handles) 

686 

687 def wait_bwd_recv(self, stage_index: int, micro_index: int) -> None: 

688 """Wait the BWD recv cached by :meth:`recv_bwd`; no-op if nothing is cached.""" 

689 handles = self.bwd_handle_cache.pop((stage_index, micro_index), None) 

690 if handles: 

691 self._wait_p2p(handles) 

692 

693 def send_fwd(self, stage: "hyper_parallel.PipelineStage", micro_index: int) -> None: 

694 """Send this stage's forward output for ``micro_index`` to the next stage.""" 

695 handles = (self._batched_issue(stage.fwd_send_specs(micro_index)) 

696 if self._batch_p2p else stage.exec_fwd_send_ops(micro_index)) or [] 

697 if self._overlap_p2p: 

698 # Append the whole handle group: run_microbatches drains _send_handles 

699 # group by group, so a bare handle would be wrongly iterated as a list. 

700 self._send_handles.append(handles) 

701 else: 

702 self._wait_p2p(handles) 

703 

704 def send_bwd(self, stage: "hyper_parallel.PipelineStage", micro_index: int) -> None: 

705 """Send this stage's input-gradient for ``micro_index`` to the previous stage. 

706 

707 Driven by the scheduler's ``BWD_SEND`` step. It pops the input grad that 

708 the backward (unified ``backward_one_chunk`` or, under 

709 ``enable_dxdw_split=True``, ``backward_input_one_chunk``) wrote to the 

710 stage's ``bwd_cache``. Calling it manually in addition to the scheduled 

711 ``BWD_SEND`` would double-send the gradient. 

712 """ 

713 handles = (self._batched_issue(stage.bwd_send_specs(micro_index)) 

714 if self._batch_p2p else stage.exec_bwd_send_ops(micro_index)) or [] 

715 if self._overlap_p2p: 

716 self._send_handles.append(handles) 

717 else: 

718 self._wait_p2p(handles) 

719 

720 def _arm_boundary(self, step): 

721 """Register ``step`` for the stage after-forward hook; return its key. 

722 

723 No-op (returns ``None``) unless ``step`` carries ``boundary_p2p``. The 

724 key is the overlap's forward ``(stage_index, micro_index)`` — exactly 

725 what the hook receives when that forward chunk completes. 

726 """ 

727 if not getattr(step, "boundary_p2p", None) or not step.sub_steps: 

728 return None 

729 fwd_sub = next((s for s in step.sub_steps 

730 if s.type == MetaStepType.FWD), None) 

731 if fwd_sub is None: 

732 return None 

733 key = (fwd_sub.stage_index, fwd_sub.micro_index) 

734 self._pending_boundary[key] = step 

735 return key 

736 

737 def _finish_boundary(self, step, armed_key) -> None: 

738 """Post-step safety net: issue any boundary P2P the hook missed.""" 

739 self.exec_boundary_p2p(step) 

740 if armed_key is not None: 

741 self._pending_boundary.pop(armed_key, None) 

742 

743 def exec_boundary_p2p(self, step) -> None: 

744 """Issue ``step.boundary_p2p`` (fwd-boundary P2P) once per run. 

745 

746 Fired by the stage after-forward hook (``_on_forward_chunk_done``) the 

747 moment the overlap's forward chunk completes — the backward is still 

748 running on its own thread, so the F_SEND leaves ~half a slot early and 

749 the next slot's recvs are already posted when the peers' sends arrive. 

750 Idempotent per ``run_microbatches`` call: the post-step safety net 

751 (``_finish_boundary``) also invokes it, so an overlap whose forward 

752 never went through ``forward_one_chunk`` degrades to gap-time issue 

753 order instead of dropping the ops. 

754 

755 Dispatches through the existing per-op helpers (``send_fwd`` / 

756 ``recv_fwd`` / ``recv_bwd``), so batching, handle caching for 

757 ``wait_*_recv`` and deferred-send bookkeeping behave exactly like the 

758 scheduled steps they replace. No-op for steps without 

759 ``boundary_p2p``. 

760 """ 

761 ops = getattr(step, "boundary_p2p", None) 

762 if not ops or id(step) in self._boundary_issued: 

763 return 

764 self._boundary_issued.add(id(step)) 

765 for sub in ops: 

766 stage = self._stage_dict[sub.stage_index] 

767 if sub.type == MetaStepType.FWD_SEND: 

768 self.send_fwd(stage, sub.micro_index) 

769 elif sub.type == MetaStepType.FWD_RECV: 

770 self.recv_fwd(stage, sub.micro_index) 

771 elif sub.type == MetaStepType.BWD_RECV: 

772 self.recv_bwd(stage, sub.micro_index) 

773 elif sub.type == MetaStepType.BWD_SEND: 

774 # attach_fwd_boundary_p2p never hoists BWD_SEND (its grad is 

775 # produced by the backward still in flight); defensive only. 

776 self.send_bwd(stage, sub.micro_index) 

777 

778 # ``op_type -> (specs builder name, route kind)`` for a coalesced sub-step. 

779 # ``route`` is the recv-cache kind (so wait_*_recv finds the handle), or 

780 # ``None`` for a send (no local consumer). 

781 _BATCH_SUB_DISPATCH = { 

782 MetaStepType.FWD_RECV: ("fwd_recv_specs", "fwd"), 

783 MetaStepType.BWD_RECV: ("bwd_recv_specs", "bwd"), 

784 MetaStepType.FWD_SEND: ("fwd_send_specs", None), 

785 MetaStepType.BWD_SEND: ("bwd_send_specs", None), 

786 } 

787 

788 def _exec_batch_send_recv(self, step) -> None: 

789 """Execute a coalesced P2P run: one ``batch_isend_irecv`` per peer. 

790 

791 Builds each sub-step's specs (same meta/bookkeeping side effects as the 

792 per-step ``recv_fwd`` / ``send_fwd`` / ...), groups every op by peer 

793 global rank, and issues one batch per peer so a same-peer send+recv runs 

794 duplex. Handle routing mirrors the per-step path: under 

795 ``overlap_p2p`` a batch carrying a recv is cached for ``wait_*_recv`` 

796 (its send rides along), a send-only batch defers to ``_send_handles``; 

797 without ``overlap_p2p`` every batch waits inline. 

798 """ 

799 # (op_type, tensor, peer, route) per op; route = (kind, stage, micro) for 

800 # a recv, else None. 

801 tagged = [] 

802 for sub in step.sub_steps: 

803 builder_name, kind = self._BATCH_SUB_DISPATCH[sub.type] 

804 stage = self._stage_dict[sub.stage_index] 

805 specs = getattr(stage, builder_name)(sub.micro_index) 

806 route = (kind, sub.stage_index, sub.micro_index) if kind is not None else None 

807 for op_type, tensor, peer in specs: 

808 tagged.append((op_type, tensor, peer, route)) 

809 

810 by_peer = {} 

811 for item in tagged: 

812 by_peer.setdefault(item[2], []).append(item) 

813 

814 for items in by_peer.values(): 

815 ops = [platform.p2p_op(op_type, tensor, peer) for op_type, tensor, peer, _ in items] 

816 handle = platform.batch_isend_irecv(ops) 

817 if handle is None: 

818 continue 

819 if not self._overlap_p2p: 

820 self._wait_p2p([handle]) 

821 continue 

822 recv_routes = [route for *_, route in items if route is not None] 

823 if recv_routes: 

824 for kind, si, mi in recv_routes: 

825 cache = self.fwd_handle_cache if kind == "fwd" else self.bwd_handle_cache 

826 cache[(si, mi)] = [handle] 

827 else: 

828 self._send_handles.append([handle]) 

829 

830 def _assert_in_unshard_if_needed(self, stage, check_step): 

831 if not isinstance(stage.submodule, HSDPModule): 

832 return 

833 submodule_hsdp_scheduler = stage.submodule.hsdp_scheduler 

834 scheduler_state = submodule_hsdp_scheduler.hsdp_state 

835 if scheduler_state.is_shard: 

836 raise RuntimeError( 

837 f"Executing MetaStep: {check_step}, expected HSDPModule parameters in unsharded " 

838 f"state, but got sharded parameters." 

839 ) 

840 

841 def _exec_step(self, cur_step, arg_mbs, kwarg_mbs, losses): 

842 """Execute one built-in step (non-custom, non-composite). 

843 

844 Each comm step dispatches to a single P2P primitive; each compute step 

845 first waits its cached recv (a no-op under ``overlap_p2p=False``) and 

846 then runs. 

847 """ 

848 stage = self._stage_dict[cur_step.stage_index] 

849 micro_index = cur_step.micro_index 

850 step_type = cur_step.type 

851 

852 if step_type in ( 

853 MetaStepType.SWAP_SET_GROUP, 

854 MetaStepType.SWAP_LAUNCH_OFFLOAD, 

855 MetaStepType.SWAP_WAIT_OFFLOAD, 

856 MetaStepType.SWAP_LAUNCH_LOAD, 

857 MetaStepType.SWAP_WAIT_LOAD, 

858 ): 

859 self._exec_pipeline_swap_step(cur_step, arg_mbs, kwarg_mbs) 

860 

861 elif step_type == MetaStepType.FWD_RECV: 

862 self.recv_fwd(stage, micro_index) 

863 

864 elif step_type == MetaStepType.FWD: 

865 self._assert_in_unshard_if_needed(stage, cur_step) 

866 self.wait_fwd_recv(stage.stage_index, micro_index) 

867 out = stage.forward_one_chunk(micro_index, arg_mbs[micro_index], kwarg_mbs[micro_index]) 

868 self.update_losses(stage, out, losses) 

869 

870 elif step_type == MetaStepType.FWD_SEND: 

871 self.send_fwd(stage, micro_index) 

872 

873 elif step_type == MetaStepType.BWD_RECV: 

874 self.recv_bwd(stage, micro_index) 

875 

876 elif step_type == MetaStepType.BWD_INPUT: 

877 self._assert_in_unshard_if_needed(stage, cur_step) 

878 self.wait_bwd_recv(stage.stage_index, micro_index) 

879 stage.backward_input_one_chunk(micro_index) 

880 

881 elif step_type == MetaStepType.BWD_WEIGHT: 

882 self._assert_in_unshard_if_needed(stage, cur_step) 

883 self.wait_bwd_recv(stage.stage_index, micro_index) 

884 stage.backward_weight_one_chunk(micro_index) 

885 

886 elif step_type == MetaStepType.BWD: 

887 self._assert_in_unshard_if_needed(stage, cur_step) 

888 self.wait_bwd_recv(stage.stage_index, micro_index) 

889 stage.backward_one_chunk(micro_index) 

890 

891 elif step_type == MetaStepType.BWD_SEND: 

892 self.send_bwd(stage, micro_index) 

893 

894 else: 

895 # FSDP control steps dispatch via the handler table; any other type 

896 # is a no-op here (composite/custom types are handled upstream). 

897 fsdp_handler = _FSDP_STEP_HANDLERS.get(step_type) 

898 if fsdp_handler is not None: 

899 fsdp_handler(stage) 

900 

901 def _exec_pipeline_swap_step(self, cur_step, arg_mbs, kwarg_mbs): 

902 """Execute a pipeline activation-swap control step.""" 

903 from hyper_parallel.core.pipeline_parallel.pipeline_swap import ( # pylint: disable=C0415 

904 swap_launch_load, 

905 swap_launch_offload, 

906 swap_set_group, 

907 swap_wait_load, 

908 swap_wait_offload, 

909 ) 

910 

911 if cur_step.type == MetaStepType.SWAP_SET_GROUP: 

912 swap_set_group(cur_step) 

913 elif cur_step.type == MetaStepType.SWAP_LAUNCH_OFFLOAD: 

914 swap_launch_offload(cur_step, self, arg_mbs, kwarg_mbs) 

915 elif cur_step.type == MetaStepType.SWAP_WAIT_OFFLOAD: 

916 swap_wait_offload(cur_step) 

917 elif cur_step.type == MetaStepType.SWAP_LAUNCH_LOAD: 

918 swap_launch_load(cur_step) 

919 elif cur_step.type == MetaStepType.SWAP_WAIT_LOAD: 

920 swap_wait_load(cur_step) 

921 

922 def run_microbatches(self, arg_mbs: list, kwarg_mbs: list, losses: list) -> None: 

923 """Execute the schedule step by step. 

924 

925 Steps whose :attr:`MetaStep.type` has a registered custom function 

926 are delegated to that function with a :class:`PipelineContext`. 

927 Composite ``OVERLAP_F_B`` / ``OVERLAP_B_F`` steps without a 

928 registered handler fall back to executing their ``sub_steps`` 

929 sequentially via :meth:`_exec_step` — correct but without 

930 comm/compute overlap. All other steps are executed by 

931 :meth:`_exec_step`. 

932 

933 Logs one ``DEBUG`` line per non-bubble step showing the rank's 

934 progress: ``rank=<r> step=<i>/<n> <MetaStep>``. Enable with 

935 ``logging.getLogger('hyper_parallel.core.pipeline_parallel.scheduler') 

936 .setLevel(logging.DEBUG)`` to trace per-rank schedule advancement 

937 (handy when diagnosing deadlocks or callback ordering issues). 

938 """ 

939 real_stage_index = self.stages[0].stage_index % self.real_stage_num 

940 self._send_handles = [] 

941 self._boundary_issued = set() 

942 self._pending_boundary = {} 

943 ctx = None # lazily created 

944 

945 ordered = self.exec_order[real_stage_index] 

946 total_steps = len(ordered) 

947 logger.debug( 

948 "run_microbatches start: rank=%d total_steps=%d micro_batch_num=%d", 

949 real_stage_index, total_steps, self.micro_batch_num, 

950 ) 

951 

952 for step_idx, cur_step in enumerate(ordered): 

953 if cur_step is None: 

954 continue 

955 

956 logger.debug( 

957 "rank=%d step=%d/%d %s", 

958 real_stage_index, step_idx, total_steps, cur_step, 

959 ) 

960 

961 # Arm the fwd-boundary hook: when this step carries boundary_p2p, 

962 # the stage's after-forward hook fires exec_boundary_p2p the moment 

963 # the overlap's forward chunk completes (works for any callback — 

964 # no callback cooperation needed). 

965 armed_key = self._arm_boundary(cur_step) 

966 

967 # Check for registered custom function 

968 custom_fn = self._custom_fn_map.get(cur_step.type) 

969 if custom_fn is not None: 

970 if ctx is None: 

971 ctx = PipelineContext(self, arg_mbs, kwarg_mbs, losses) 

972 custom_fn(cur_step, ctx) 

973 # Safety net: if the forward hook never fired (custom fwd path), 

974 # issue the boundary ops now (gap-time order) instead of 

975 # dropping them. Idempotent. 

976 self._finish_boundary(cur_step, armed_key) 

977 continue 

978 

979 # Coalesced P2P block: group sub-steps by peer, issue one 

980 # batch_isend_irecv per peer (same-peer send+recv -> duplex). 

981 if cur_step.type == MetaStepType.BATCH_SEND_RECV: 

982 self._exec_batch_send_recv(cur_step) 

983 continue 

984 

985 # Default for composite OVERLAP steps: run sub_steps sequentially. 

986 # P2P send/recv around these steps are already laid out in two 

987 # virtual slots by ``add_send_recv``, so sequential execution is 

988 # semantically equivalent to non-overlapped 1F1B. 

989 if (cur_step.type in (MetaStepType.OVERLAP_F_B, MetaStepType.OVERLAP_B_F) 

990 and cur_step.sub_steps): 

991 for sub in cur_step.sub_steps: 

992 self._exec_step(sub, arg_mbs, kwarg_mbs, losses) 

993 self._finish_boundary(cur_step, armed_key) 

994 continue 

995 

996 self._exec_step(cur_step, arg_mbs, kwarg_mbs, losses) 

997 

998 logger.debug( 

999 "run_microbatches end: rank=%d pending_send_handles=%d", 

1000 real_stage_index, len(self._send_handles), 

1001 ) 

1002 self.sync_shared_parameters_grad() 

1003 while self._send_handles: 

1004 self._wait_p2p(self._send_handles.pop()) 

1005 

1006 

1007class _OverlapPhantom: 

1008 """Internal marker used by :func:`add_send_recv` to expand an 

1009 ``OVERLAP_F_B`` or ``OVERLAP_B_F`` step into two virtual time slots. 

1010 

1011 An overlap step composes two sub-steps (``B + F`` or ``F + B``) that 

1012 execute concurrently on the GPU but occupy **two** logical time slots 

1013 in the column-scan sender timeline — the sender can only finish 

1014 emitting the second sub-step's output after the first sub-step has 

1015 completed. Treating an overlap step as a single slot places the RECV 

1016 triggered by the second sub-step too early on the receiver. 

1017 

1018 Each overlap step is expanded into two phantoms: 

1019 * ``is_first_half=True`` — represents the first sub-step's emission 

1020 slot; the original overlap step is emitted into the output 

1021 schedule here (only once). 

1022 * ``is_first_half=False`` — represents the second sub-step's emission 

1023 slot; only its send/recv comms are inserted. 

1024 """ 

1025 

1026 __slots__ = ('obf_step', 'sub_step', 'is_first_half') 

1027 

1028 def __init__(self, obf_step, sub_step, is_first_half: bool): 

1029 self.obf_step = obf_step 

1030 self.sub_step = sub_step 

1031 self.is_first_half = is_first_half 

1032 

1033 

1034def _expand_overlap_slots(scheduler, real_stage_num): 

1035 """Expand OVERLAP steps in a per-rank schedule into 2 virtual time slots. 

1036 

1037 Returns a new ``{rank: [MetaStep | _OverlapPhantom | None, ...]}`` dict 

1038 where each OVERLAP step is replaced by a pair of phantoms. Non-OVERLAP 

1039 entries pass through unchanged. 

1040 """ 

1041 expanded = {} 

1042 for rank in range(real_stage_num): 

1043 order = scheduler[rank] 

1044 exp = [] 

1045 for op in order: 

1046 if (op is not None 

1047 and op.type in (MetaStepType.OVERLAP_F_B, MetaStepType.OVERLAP_B_F) 

1048 and op.sub_steps): 

1049 exp.append(_OverlapPhantom(op, op.sub_steps[0], is_first_half=True)) 

1050 exp.append(_OverlapPhantom(op, op.sub_steps[1], is_first_half=False)) 

1051 else: 

1052 exp.append(op) 

1053 expanded[rank] = exp 

1054 return expanded 

1055 

1056 

1057def _process_rank_items(real_stage_num, current_items, insert_step_comms, new_schedule): 

1058 """Run ``insert_step_comms`` for each rank's current item, even ranks first. 

1059 

1060 Even-before-odd ordering avoids P2P deadlocks between adjacent ranks. 

1061 """ 

1062 for rank in range(0, real_stage_num, 2): 

1063 item = current_items.get(rank) 

1064 if item is not None: 

1065 sub = item.sub_step if isinstance(item, _OverlapPhantom) else item 

1066 insert_step_comms(sub, rank, new_schedule) 

1067 for rank in range(1, real_stage_num, 2): 

1068 item = current_items.get(rank) 

1069 if item is not None: 

1070 sub = item.sub_step if isinstance(item, _OverlapPhantom) else item 

1071 insert_step_comms(sub, rank, new_schedule) 

1072 

1073 

1074def _column_scan_insert_comms(expanded, real_stage_num, insert_step_comms): 

1075 """Column-scan over an OVERLAP-expanded schedule to insert SEND/RECV. 

1076 

1077 Processes ``expanded`` one time slot at a time. Emits the original 

1078 overlap step into ``new_schedule`` only once (at the first-half 

1079 phantom). Delegates comm insertion to ``insert_step_comms`` for each 

1080 plain step or phantom's underlying sub-step. 

1081 

1082 Even ranks are processed before odd ranks at each time step to avoid 

1083 P2P deadlocks between adjacent ranks. 

1084 

1085 Args: 

1086 expanded: Result of :func:`_expand_overlap_slots`. 

1087 real_stage_num: Number of physical ranks. 

1088 insert_step_comms: Callable ``(step, rank, new_schedule) -> None`` 

1089 that inserts SEND/RECV for a single FWD/BWD step. 

1090 

1091 Returns: 

1092 ``{rank: [MetaStep, ...]}`` final schedule. 

1093 """ 

1094 max_length = max(len(order) for order in expanded.values()) 

1095 new_schedule = {rank: [] for rank in range(real_stage_num)} 

1096 

1097 for time_step in range(max_length): 

1098 current_items = {} 

1099 for rank in range(real_stage_num): 

1100 if time_step < len(expanded[rank]): 

1101 item = expanded[rank][time_step] 

1102 current_items[rank] = item 

1103 if item is None: 

1104 # Preserve bubble slots to keep per-rank time-step 

1105 # indexing aligned with the column scan. The runtime 

1106 # loop skips ``None`` entries, so this is execution- 

1107 # semantics-neutral. 

1108 new_schedule[rank].append(None) 

1109 continue 

1110 if isinstance(item, _OverlapPhantom): 

1111 # Emit the overlap step only once, at the first-half slot. 

1112 if item.is_first_half: 

1113 new_schedule[rank].append(item.obf_step) 

1114 else: 

1115 new_schedule[rank].append(item) 

1116 else: 

1117 current_items[rank] = None 

1118 

1119 _process_rank_items( 

1120 real_stage_num, current_items, insert_step_comms, new_schedule, 

1121 ) 

1122 

1123 return new_schedule 

1124 

1125 

1126_P2P_STEP_TYPES = frozenset({ 

1127 MetaStepType.FWD_SEND, MetaStepType.FWD_RECV, 

1128 MetaStepType.BWD_SEND, MetaStepType.BWD_RECV, 

1129}) 

1130 

1131 

1132def coalesce_p2p(exec_order): 

1133 """Coalesce maximal contiguous runs of >=2 P2P steps into BATCH_SEND_RECV. 

1134 

1135 A *run* is a maximal sequence of consecutive ``FWD_SEND`` / ``FWD_RECV`` / 

1136 ``BWD_SEND`` / ``BWD_RECV`` steps with no compute / overlap / bubble (``None``) 

1137 step between them — so no recv in the run is consumed before the batch is 

1138 issued, and all sends' data is already produced. Each such run is replaced 

1139 by a single :class:`MetaStep` of type ``BATCH_SEND_RECV`` carrying the run as 

1140 ``sub_steps`` (order preserved, so per-direction FIFO is kept); the runtime 

1141 groups those sub-steps by peer and issues one ``batch_isend_irecv`` per peer 

1142 (same-peer send+recv -> duplex). Runs of length 1 are left untouched (the 

1143 per-op batched path still batches them, so every transfer is still 

1144 batch-vs-batch). Pure ``exec_order -> exec_order`` transform. 

1145 

1146 Args: 

1147 exec_order: ``{rank: [MetaStep | None, ...]}``. 

1148 

1149 Returns: 

1150 A new ``{rank: [...]}`` with contiguous P2P runs coalesced. 

1151 """ 

1152 def _flush(run, new): 

1153 if len(run) >= 2: 

1154 new.append(MetaStep(None, MetaStepType.BATCH_SEND_RECV, None, sub_steps=tuple(run))) 

1155 else: 

1156 new.extend(run) 

1157 

1158 out = {} 

1159 for rank, order in exec_order.items(): 

1160 new = [] 

1161 run = [] 

1162 for step in order: 

1163 if step is not None and step.type in _P2P_STEP_TYPES: 

1164 run.append(step) 

1165 continue 

1166 _flush(run, new) 

1167 run = [] 

1168 new.append(step) 

1169 _flush(run, new) 

1170 out[rank] = new 

1171 return out 

1172 

1173 

1174_RECV_STEP_TYPES = frozenset({MetaStepType.FWD_RECV, MetaStepType.BWD_RECV}) 

1175 

1176 

1177def attach_fwd_boundary_p2p(exec_order): 

1178 """Hang each overlap gap's boundary-safe P2P on the OVERLAP_B_F step. 

1179 

1180 For every ``OVERLAP_B_F`` step, the contiguous P2P run right after it is 

1181 split by data readiness at the overlap's fwd/bwd boundary (forward is the 

1182 short side; backward, ~2x FLOPs, is the long pole): 

1183 

1184 * the forward's own ``FWD_SEND`` — its payload exists the moment the 

1185 forward sub-step finishes, no need to wait out the backward; 

1186 * every ``FWD_RECV`` / ``BWD_RECV`` — no data dependency at all; 

1187 

1188 are removed from the gap and attached to the OVERLAP step as 

1189 ``boundary_p2p`` (order: ``F_SEND`` first, then the recvs in original 

1190 order), to be issued by :meth:`PipelineScheduleRuntime.exec_boundary_p2p` 

1191 at the boundary, while the backward is still running. ``BWD_SEND`` (its 

1192 grad is produced by that backward) and any send not produced by this 

1193 overlap's forward stay in the gap. 

1194 

1195 Pairing shape (the reason this composition is safe where naive 

1196 hoist+coalesce hung): with every op issued as a per-op solo batch, each 

1197 pair's per-pair batch sequence per slot is ``[F_SEND, B_RECV]`` on the 

1198 prev end and ``[F_RECV, B_SEND]`` on the next end — complementary at every 

1199 position and equal in count, so it matches under both per-direction FIFO 

1200 and per-pair shape-mirroring semantics. Per-direction FIFO data order is 

1201 preserved (each direction's ops keep their relative order; they all shift 

1202 by the same amount). Pure ``exec_order -> exec_order`` transform. 

1203 

1204 Args: 

1205 exec_order: ``{rank: [MetaStep | None, ...]}``. 

1206 

1207 Returns: 

1208 A new ``{rank: [...]}`` with boundary P2P attached to OVERLAP steps. 

1209 """ 

1210 out = {} 

1211 for rank, order in exec_order.items(): 

1212 new = [] 

1213 i = 0 

1214 while i < len(order): 

1215 step = order[i] 

1216 if (step is None or step.type != MetaStepType.OVERLAP_B_F 

1217 or not step.sub_steps): 

1218 new.append(step) 

1219 i += 1 

1220 continue 

1221 run, j = _p2p_run_after(order, i + 1) 

1222 boundary, leftover = _split_boundary_run(step, run) 

1223 if not boundary: 

1224 new.append(step) 

1225 i += 1 

1226 continue 

1227 new.append(MetaStep(step.micro_index, step.type, step.stage_index, 

1228 sub_steps=step.sub_steps, boundary_p2p=boundary)) 

1229 new.extend(leftover) 

1230 i = j 

1231 out[rank] = new 

1232 return out 

1233 

1234 

1235def _p2p_run_after(order, start): 

1236 """Collect the contiguous P2P run starting at ``start``. 

1237 

1238 Returns ``(run, end)`` where ``end`` is the index of the first step past 

1239 the run (a compute step, ``None`` bubble, or end of order). 

1240 """ 

1241 run = [] 

1242 j = start 

1243 while j < len(order) and order[j] is not None and order[j].type in _P2P_STEP_TYPES: 

1244 run.append(order[j]) 

1245 j += 1 

1246 return run, j 

1247 

1248 

1249def _split_boundary_run(step, run): 

1250 """Split an overlap's trailing P2P run by fwd/bwd-boundary data readiness. 

1251 

1252 Returns ``(boundary, leftover)``: ``boundary`` holds the overlap's own 

1253 forward ``FWD_SEND`` (payload ready at the boundary) first, then every 

1254 recv (no data dependency), keeping original order; ``leftover`` keeps the 

1255 sends produced by the still-running backward, in place. 

1256 """ 

1257 fwd_sub = next((s for s in step.sub_steps 

1258 if s.type == MetaStepType.FWD), None) 

1259 

1260 def _is_own_fwd_send(s): 

1261 return (fwd_sub is not None 

1262 and s.type == MetaStepType.FWD_SEND 

1263 and s.stage_index == fwd_sub.stage_index 

1264 and s.micro_index == fwd_sub.micro_index) 

1265 

1266 boundary = ([s for s in run if _is_own_fwd_send(s)] 

1267 + [s for s in run if s.type in _RECV_STEP_TYPES]) 

1268 taken = {id(s) for s in boundary} 

1269 leftover = [s for s in run if id(s) not in taken] 

1270 return tuple(boundary), leftover 

1271 

1272 

1273def split_overlap_dxdw(exec_order: dict) -> dict: 

1274 """Split each OVERLAP_B_F backward into dx (in the pair) + dw (after the gap). 

1275 

1276 Rewrites ``(BWD, FWD)`` sub_steps to ``(BWD_INPUT, FWD)`` and inserts the 

1277 matching ``BWD_WEIGHT`` after the contiguous P2P run that follows the 

1278 overlap. The overlap then joins at ``max(dx, fwd)`` instead of 

1279 ``max(dx + dw, fwd)``, so the gap's ``BWD_SEND`` (dx already wrote its 

1280 grad to ``bwd_cache``) and the next slot's recvs are issued a dw earlier; 

1281 under ``overlap_p2p`` they are async and dw computes while they fly. 

1282 

1283 Comm placement is untouched: the pass runs after ``add_send_recv`` and 

1284 only moves local compute, so the cross-rank matching order is identical, 

1285 and the P2P run stays contiguous (dw lands after it, not inside) so 

1286 ``coalesce_p2p`` / ``attach_fwd_boundary_p2p`` see the same gap shape. 

1287 

1288 First-stage (``stage_index == 0``) backwards stay unified: their dx is a 

1289 no-op (no input grad to compute or send), so splitting would only move 

1290 the whole backward out of the overlap and lose its fwd overlap. 

1291 

1292 Args: 

1293 exec_order: ``{rank: [MetaStep | None, ...]}``. 

1294 

1295 Returns: 

1296 A new ``{rank: [...]}`` with overlap backwards split into dx/dw. 

1297 """ 

1298 out = {} 

1299 for rank, order in exec_order.items(): 

1300 new = [] 

1301 i = 0 

1302 n = len(order) 

1303 while i < n: 

1304 step = order[i] 

1305 i += 1 

1306 if (step is None or step.type != MetaStepType.OVERLAP_B_F 

1307 or not step.sub_steps): 

1308 new.append(step) 

1309 continue 

1310 bwd_sub, fwd_sub = step.sub_steps 

1311 if bwd_sub.type != MetaStepType.BWD or bwd_sub.stage_index == 0: 

1312 new.append(step) 

1313 continue 

1314 dx = MetaStep(bwd_sub.micro_index, MetaStepType.BWD_INPUT, bwd_sub.stage_index) 

1315 new.append(MetaStep(step.micro_index, step.type, step.stage_index, 

1316 sub_steps=(dx, fwd_sub))) 

1317 run, i = _p2p_run_after(order, i) 

1318 new.extend(run) 

1319 new.append(MetaStep(bwd_sub.micro_index, MetaStepType.BWD_WEIGHT, 

1320 bwd_sub.stage_index)) 

1321 out[rank] = new 

1322 return out 

1323 

1324 

1325def add_send_recv(scheduler, stage_num, real_stage_num, style='loop'): 

1326 """Insert P2P send/recv operations into a per-rank compute schedule. 

1327 

1328 For each FWD or BWD step that requires cross-rank communication, a 

1329 ``FWD_SEND`` / ``BWD_SEND`` is appended to the sender's schedule and a 

1330 ``FWD_RECV`` / ``BWD_RECV`` is appended to the receiver's schedule. 

1331 

1332 ``OVERLAP_F_B`` / ``OVERLAP_B_F`` composite steps are expanded into 

1333 **two** virtual time slots during the column scan so that the RECV 

1334 triggered by the **second** sub-step lands in the receiver's schedule 

1335 one slot later — matching the fact that the sender can only finish 

1336 emitting the second sub-step's output after the first completes. 

1337 

1338 Even ranks are processed before odd ranks at each time step to avoid 

1339 P2P deadlocks between adjacent ranks. 

1340 

1341 The resulting per-gap op order (steady state: 

1342 ``[B_RECV, B_SEND, F_RECV, F_SEND]``) is LOAD-BEARING, not cosmetic. 

1343 Each adjacent rank pair shares one comm (HCCL split-by-group) on which 

1344 plain send/recv execute in queue order, and this layout makes one end of 

1345 every pair recv-first while the other is send-first, so the two queue 

1346 heads always match (recv<->send), then the tails match (send<->recv). 

1347 Any later pass that reorders P2P ops relative to EACH OTHER breaks this: 

1348 a (since removed) hoist variant that moved recvs across sends made both 

1349 ends recv-first and deadlocked on hardware (2026-06). 

1350 ``attach_fwd_boundary_p2p`` (the ``"boundary"`` transport) is safe: it keeps 

1351 every per-direction FIFO and runs on the batch transport with per-op solo 

1352 batches, whose per-pair sequences stay complementary. 

1353 

1354 Args: 

1355 scheduler: ``{rank: [MetaStep | None, ...]}`` — compute schedule 

1356 with ``None`` for bubble slots. 

1357 stage_num: Total number of virtual pipeline stages. 

1358 real_stage_num: Number of physical ranks. 

1359 style: Topology mapping — ``'loop'`` or ``'v'``. 

1360 

1361 Returns: 

1362 ``{rank: [MetaStep, ...]}`` — schedule with communication ops inserted. 

1363 """ 

1364 

1365 def stage_to_rank(stage_index: int) -> int: 

1366 """Map a virtual stage index to its physical rank.""" 

1367 if style == 'loop': 

1368 return stage_index % real_stage_num 

1369 if style == 'v': 

1370 if stage_index < real_stage_num: 

1371 return stage_index 

1372 return stage_num - 1 - stage_index 

1373 raise ValueError(f"Argument 'style' must be 'loop' or 'v', but got {style!r}.") 

1374 

1375 def _fwd_peer(stage_index: int): 

1376 """Return the rank that receives this stage's forward output, or None.""" 

1377 if stage_index >= stage_num - 1: 

1378 return None 

1379 peer = stage_to_rank(stage_index + 1) 

1380 return peer if peer != stage_to_rank(stage_index) else None 

1381 

1382 def _bwd_peer(stage_index: int): 

1383 """Return the rank that receives this stage's backward gradient, or None.""" 

1384 if stage_index <= 0: 

1385 return None 

1386 peer = stage_to_rank(stage_index - 1) 

1387 return peer if peer != stage_to_rank(stage_index) else None 

1388 

1389 def _insert_comms_for_step(step, rank, new_schedule): 

1390 """Insert send/recv for a single FWD, BWD, or composite OVERLAP step.""" 

1391 if step is None: 

1392 return 

1393 

1394 if step.type == MetaStepType.FWD: 

1395 peer = _fwd_peer(step.stage_index) 

1396 if peer is not None: 

1397 new_schedule[rank].append( 

1398 MetaStep(step.micro_index, MetaStepType.FWD_SEND, step.stage_index)) 

1399 new_schedule[peer].append( 

1400 MetaStep(step.micro_index, MetaStepType.FWD_RECV, step.stage_index + 1)) 

1401 

1402 elif step.type == MetaStepType.BWD: 

1403 peer = _bwd_peer(step.stage_index) 

1404 if peer is not None: 

1405 new_schedule[rank].append( 

1406 MetaStep(step.micro_index, MetaStepType.BWD_SEND, step.stage_index)) 

1407 new_schedule[peer].append( 

1408 MetaStep(step.micro_index, MetaStepType.BWD_RECV, step.stage_index - 1)) 

1409 

1410 elif step.type in (MetaStepType.OVERLAP_F_B, MetaStepType.OVERLAP_B_F) and step.sub_steps: 

1411 for sub in step.sub_steps: 

1412 _insert_comms_for_step(sub, rank, new_schedule) 

1413 

1414 # --- Main logic: expand OVERLAP steps into 2 virtual slots, then scan --- 

1415 expanded = _expand_overlap_slots(scheduler, real_stage_num) 

1416 return _column_scan_insert_comms(expanded, real_stage_num, _insert_comms_for_step) 

1417 

1418 

1419_ALIGN_PAD = object() 

1420"""Sentinel marking a forced 1F1B-boundary bubble produced during alignment.""" 

1421 

1422 

1423def _step_dep_ready(step, rank, t, done, stage_num, stage_to_rank): 

1424 """Cross-rank data dependency check used by the alignment simulator. 

1425 

1426 A FWD step at stage ``s`` depends on FWD at stage ``s-1`` (on a 

1427 different rank); BWD at stage ``s`` depends on BWD at stage ``s+1``. 

1428 Steps at boundaries or whose producer lives on the same rank are 

1429 always ready. 

1430 """ 

1431 si, mi = step.stage_index, step.micro_index 

1432 if step.type == MetaStepType.FWD: 

1433 if si == 0 or stage_to_rank(si - 1) == rank: 

1434 return True 

1435 key = (MetaStepType.FWD, si - 1, mi) 

1436 return key in done and done[key] < t 

1437 if step.type == MetaStepType.BWD: 

1438 if si == stage_num - 1 or stage_to_rank(si + 1) == rank: 

1439 return True 

1440 key = (MetaStepType.BWD, si + 1, mi) 

1441 return key in done and done[key] < t 

1442 return True 

1443 

1444 

1445def _simulate_aligned_schedule(padded, stage_num, real_stage_num, stage_to_rank): 

1446 """Simulate execution time-step by time-step, inserting bubbles where 

1447 a step is not yet ready (cross-rank dep) or where the cooldown 

1448 rhythm requires it. 

1449 

1450 Args: 

1451 padded: ``{rank: [step | _ALIGN_PAD | None, ...]}`` after 

1452 1F1B-boundary padding. 

1453 stage_num: Total number of virtual pipeline stages. 

1454 real_stage_num: Number of physical ranks. 

1455 stage_to_rank: Topology mapping from stage to rank. 

1456 

1457 Returns: 

1458 ``{rank: [step | None, ...]}`` ready for the column-scan SEND/RECV 

1459 insertion phase. 

1460 """ 

1461 remaining_fwd = { 

1462 rank: sum( 

1463 1 for s in padded[rank] 

1464 if s is not _ALIGN_PAD and s is not None and s.type == MetaStepType.FWD 

1465 ) 

1466 for rank in range(real_stage_num) 

1467 } 

1468 cursors = {r: 0 for r in range(real_stage_num)} 

1469 aligned = {r: [] for r in range(real_stage_num)} 

1470 done = {} 

1471 last_was_cooldown_bwd = {r: False for r in range(real_stage_num)} 

1472 max_t = sum(len(v) for v in padded.values()) + real_stage_num * 20 

1473 

1474 def _emit_bubble(rank): 

1475 aligned[rank].append(None) 

1476 last_was_cooldown_bwd[rank] = False 

1477 

1478 def _emit_step(rank, step, t, in_cooldown): 

1479 aligned[rank].append(step) 

1480 done[(step.type, step.stage_index, step.micro_index)] = t 

1481 cursors[rank] += 1 

1482 if step.type == MetaStepType.FWD: 

1483 remaining_fwd[rank] -= 1 

1484 last_was_cooldown_bwd[rank] = in_cooldown and step.type == MetaStepType.BWD 

1485 

1486 def _step_rank_at(t, rank): 

1487 if cursors[rank] >= len(padded[rank]): 

1488 return 

1489 item = padded[rank][cursors[rank]] 

1490 if item is _ALIGN_PAD: 

1491 _emit_bubble(rank) 

1492 cursors[rank] += 1 

1493 return 

1494 in_cooldown = remaining_fwd[rank] == 0 

1495 # Cooldown rhythm: alternate None / BWD in pure-BWD phase. 

1496 cooldown_skip = ( 

1497 in_cooldown 

1498 and item.type == MetaStepType.BWD 

1499 and last_was_cooldown_bwd[rank] 

1500 ) 

1501 if cooldown_skip: 

1502 _emit_bubble(rank) 

1503 return 

1504 if not _step_dep_ready(item, rank, t, done, stage_num, stage_to_rank): 

1505 _emit_bubble(rank) 

1506 return 

1507 _emit_step(rank, item, t, in_cooldown) 

1508 

1509 for t in range(max_t): 

1510 if all(cursors[r] >= len(padded[r]) for r in range(real_stage_num)): 

1511 break 

1512 for rank in range(real_stage_num): 

1513 _step_rank_at(t, rank) 

1514 return aligned 

1515 

1516 

1517def auto_align_and_add_send_recv(scheduler, stage_num, real_stage_num, style='loop'): 

1518 """Auto-insert bubble alignment and P2P send/recv into a pure-compute schedule. 

1519 

1520 Unlike :func:`add_send_recv` which requires the caller to pre-insert 

1521 ``None`` bubble slots for time-step alignment, this function accepts a 

1522 **pure compute order** (``FWD`` / ``BWD`` only, no ``None`` needed) and 

1523 automatically determines bubble placement via execution simulation. 

1524 

1525 Three constraints are enforced: 

1526 

1527 1. **Data dependency** — a ``FWD(stage_k)`` cannot execute until 

1528 ``FWD(stage_{k-1})`` on its source rank has completed (and 

1529 analogously for ``BWD``). 

1530 2. **1F1B transition alignment** — ``real_stage_num - 1 - rank`` padding 

1531 slots are inserted at the warmup → 1F1B boundary (detected as the 

1532 first ``FWD`` immediately followed by a ``BWD`` in the compute order) 

1533 so that all ranks enter the 1F1B steady state in lockstep. 

1534 3. **Cooldown rhythm** — once a rank exhausts its ``FWD`` ops and enters 

1535 pure-``BWD`` cooldown, consecutive ``BWD`` steps are separated by a 

1536 ``None`` slot, maintaining the column-phase-sync property (no rank 

1537 does ``BWD`` while another does ``FWD`` at the same time step). 

1538 

1539 After alignment, a column-scan pass inserts ``FWD_SEND`` / ``FWD_RECV`` 

1540 and ``BWD_SEND`` / ``BWD_RECV`` with the same prefetch semantics as 

1541 :func:`add_send_recv`. 

1542 

1543 Args: 

1544 scheduler: ``{rank: [MetaStep, ...]}`` — pure compute schedule. 

1545 ``None`` entries are silently stripped before processing. 

1546 stage_num: Total number of virtual pipeline stages. 

1547 real_stage_num: Number of physical ranks. 

1548 style: Topology mapping — ``'loop'`` or ``'v'``. 

1549 

1550 Returns: 

1551 ``{rank: [MetaStep, ...]}`` — fully aligned schedule with bubbles 

1552 and communication ops inserted. 

1553 """ 

1554 

1555 # ---- topology helpers (shared with column-scan phase) ---- 

1556 

1557 def stage_to_rank(stage_index: int) -> int: 

1558 if style == 'loop': 

1559 return stage_index % real_stage_num 

1560 if style == 'v': 

1561 if stage_index < real_stage_num: 

1562 return stage_index 

1563 return stage_num - 1 - stage_index 

1564 raise ValueError(f"Argument 'style' must be 'loop' or 'v', but got {style!r}.") 

1565 

1566 def _fwd_peer(stage_index: int): 

1567 if stage_index >= stage_num - 1: 

1568 return None 

1569 peer = stage_to_rank(stage_index + 1) 

1570 return peer if peer != stage_to_rank(stage_index) else None 

1571 

1572 def _bwd_peer(stage_index: int): 

1573 if stage_index <= 0: 

1574 return None 

1575 peer = stage_to_rank(stage_index - 1) 

1576 return peer if peer != stage_to_rank(stage_index) else None 

1577 

1578 # ---- Phase 1: strip None, detect 1F1B boundary, insert transition padding ---- 

1579 

1580 def _find_1f1b_boundary(order): 

1581 """Index of the first FWD followed by BWD; ``len(order)`` if absent.""" 

1582 for i in range(len(order) - 1): 

1583 if (order[i].type == MetaStepType.FWD 

1584 and order[i + 1].type == MetaStepType.BWD): 

1585 return i 

1586 return len(order) 

1587 

1588 padded = {} 

1589 for rank in range(real_stage_num): 

1590 order = [s for s in scheduler[rank] if s is not None] 

1591 boundary = _find_1f1b_boundary(order) 

1592 pad_count = real_stage_num - 1 - rank 

1593 padded[rank] = order[:boundary] + [_ALIGN_PAD] * pad_count + order[boundary:] 

1594 

1595 # ---- Phase 2: simulate execution with data deps + cooldown rhythm ---- 

1596 

1597 aligned = _simulate_aligned_schedule(padded, stage_num, real_stage_num, stage_to_rank) 

1598 

1599 # ---- Phase 3: column-scan SEND/RECV insertion (same as add_send_recv) ---- 

1600 

1601 def _insert_comms_for_step(step, rank, new_schedule): 

1602 if step is None: 

1603 return 

1604 if step.type == MetaStepType.FWD: 

1605 peer = _fwd_peer(step.stage_index) 

1606 if peer is not None: 

1607 new_schedule[rank].append( 

1608 MetaStep(step.micro_index, MetaStepType.FWD_SEND, step.stage_index)) 

1609 new_schedule[peer].append( 

1610 MetaStep(step.micro_index, MetaStepType.FWD_RECV, step.stage_index + 1)) 

1611 elif step.type == MetaStepType.BWD: 

1612 peer = _bwd_peer(step.stage_index) 

1613 if peer is not None: 

1614 new_schedule[rank].append( 

1615 MetaStep(step.micro_index, MetaStepType.BWD_SEND, step.stage_index)) 

1616 new_schedule[peer].append( 

1617 MetaStep(step.micro_index, MetaStepType.BWD_RECV, step.stage_index - 1)) 

1618 elif step.type in (MetaStepType.OVERLAP_F_B, MetaStepType.OVERLAP_B_F) and step.sub_steps: 

1619 for sub in step.sub_steps: 

1620 _insert_comms_for_step(sub, rank, new_schedule) 

1621 

1622 # Expand OVERLAP steps into 2 virtual slots before the column scan so 

1623 # the RECV triggered by an overlap's second sub-step lands one slot 

1624 # later on the receiver — matching the fact that the sender can only 

1625 # finish emitting the second sub-step after the first completes. 

1626 expanded = _expand_overlap_slots(aligned, real_stage_num) 

1627 return _column_scan_insert_comms(expanded, real_stage_num, _insert_comms_for_step) 

1628 

1629 

1630class ScheduleGPipe(PipelineScheduleRuntime): 

1631 """ 

1632 The Gpipe schedule. 

1633 It first executes all forward micro batches and then execute all backward micro batches. 

1634 """ 

1635 def __init__(self, 

1636 stages, 

1637 micro_batch_num, 

1638 args_batch_dim=None, 

1639 kwargs_batch_dim=None, 

1640 output_concat_dim=None, 

1641 swap=False): 

1642 super().__init__(stages, 

1643 micro_batch_num, 

1644 args_batch_dim=args_batch_dim, 

1645 kwargs_batch_dim=kwargs_batch_dim, 

1646 output_concat_dim=output_concat_dim, 

1647 swap=swap) 

1648 self.build_exec_order() 

1649 

1650 def _build_stage_to_rank_index(self) -> None: 

1651 self._stage_to_rank_index = generate_stage_to_rank_mapping( 

1652 self.real_stage_num, self._stage_num, style='loop' 

1653 ) 

1654 

1655 def construct_exec_order(self): 

1656 """construct_exec_order of Gpipe.""" 

1657 for stage_index in range(self.real_stage_num): 

1658 order_list = [] 

1659 for mb_index in range(self.micro_batch_num): 

1660 if stage_index != 0: 

1661 order_list.append(MetaStep(mb_index, MetaStepType.FWD_RECV, stage_index)) 

1662 order_list.append(MetaStep(mb_index, MetaStepType.FWD, stage_index)) 

1663 if stage_index != self.real_stage_num - 1: 

1664 order_list.append(MetaStep(mb_index, MetaStepType.FWD_SEND, stage_index)) 

1665 for mb_index in range(self.micro_batch_num): 

1666 if stage_index != self.real_stage_num - 1: 

1667 order_list.append(MetaStep(mb_index, MetaStepType.BWD_RECV, stage_index)) 

1668 order_list.append(MetaStep(mb_index, MetaStepType.BWD, stage_index)) 

1669 if stage_index != 0: 

1670 order_list.append(MetaStep(mb_index, MetaStepType.BWD_SEND, stage_index)) 

1671 self.exec_order[stage_index] = order_list 

1672 

1673 

1674class Schedule1F1B(PipelineScheduleRuntime): 

1675 """ 

1676 The 1F1B schedule. 

1677 It will perform one forward and one backward on the micro batches in steady state. 

1678 """ 

1679 def __init__(self, 

1680 stages, 

1681 micro_batch_num, 

1682 args_batch_dim=None, 

1683 kwargs_batch_dim=None, 

1684 output_concat_dim=None, 

1685 swap=False): 

1686 super().__init__(stages, 

1687 micro_batch_num, 

1688 args_batch_dim=args_batch_dim, 

1689 kwargs_batch_dim=kwargs_batch_dim, 

1690 output_concat_dim=output_concat_dim, 

1691 swap=swap) 

1692 self.build_exec_order() 

1693 

1694 def _build_stage_to_rank_index(self) -> None: 

1695 self._stage_to_rank_index = generate_stage_to_rank_mapping( 

1696 self.real_stage_num, self._stage_num, style='loop' 

1697 ) 

1698 

1699 def construct_exec_order(self): 

1700 """construct_exec_order of 1F1B.""" 

1701 for stage_index in range(self.real_stage_num): 

1702 order_list = [] 

1703 fwd_index = 0 

1704 bwd_index = 0 

1705 # warmup phase 

1706 warmup_micro_batches = min(self.real_stage_num - stage_index, self.micro_batch_num) 

1707 for _ in range(warmup_micro_batches): 

1708 if stage_index != 0: 

1709 order_list.append(MetaStep(fwd_index, MetaStepType.FWD_RECV, stage_index)) 

1710 if stage_index % 2 == 0: 

1711 order_list.append(MetaStep(fwd_index, MetaStepType.FWD, stage_index)) 

1712 if fwd_index != warmup_micro_batches - 1: 

1713 order_list.append(MetaStep(fwd_index, MetaStepType.FWD_SEND, stage_index)) 

1714 else: 

1715 if fwd_index > 0: 

1716 order_list.append(MetaStep(fwd_index - 1, MetaStepType.FWD_SEND, stage_index)) 

1717 order_list.append(MetaStep(fwd_index, MetaStepType.FWD, stage_index)) 

1718 fwd_index += 1 

1719 

1720 # if warmup phase cannot filled up, then we need to execute fwd send in advance 

1721 if self.real_stage_num - stage_index > self.micro_batch_num: 

1722 order_list.append(MetaStep(fwd_index - 1, MetaStepType.FWD_SEND, stage_index)) 

1723 fwd_index += 1 

1724 # steady phase 

1725 steady_micro_batches = self.micro_batch_num - warmup_micro_batches 

1726 for _ in range(steady_micro_batches): 

1727 if stage_index != self.real_stage_num - 1: 

1728 order_list.append(MetaStep(bwd_index, MetaStepType.BWD_RECV, stage_index)) 

1729 order_list.append(MetaStep(fwd_index - 1, MetaStepType.FWD_SEND, stage_index)) 

1730 order_list.append(MetaStep(bwd_index, MetaStepType.BWD, stage_index)) 

1731 

1732 if stage_index != 0: 

1733 order_list.append(MetaStep(bwd_index, MetaStepType.BWD_SEND, stage_index)) 

1734 order_list.append(MetaStep(fwd_index, MetaStepType.FWD_RECV, stage_index)) 

1735 order_list.append(MetaStep(fwd_index, MetaStepType.FWD, stage_index)) 

1736 fwd_index += 1 

1737 bwd_index += 1 

1738 

1739 # cooldown phase 

1740 cooldown_micro_batches = warmup_micro_batches 

1741 for _ in range(cooldown_micro_batches): 

1742 if stage_index != self.real_stage_num - 1: 

1743 order_list.append(MetaStep(bwd_index, MetaStepType.BWD_RECV, stage_index)) 

1744 if bwd_index == self.micro_batch_num - warmup_micro_batches and fwd_index <= self.micro_batch_num: 

1745 order_list.append(MetaStep(fwd_index - 1, MetaStepType.FWD_SEND, stage_index)) 

1746 order_list.append(MetaStep(bwd_index, MetaStepType.BWD, stage_index)) 

1747 

1748 if stage_index != 0: 

1749 order_list.append(MetaStep(bwd_index, MetaStepType.BWD_SEND, stage_index)) 

1750 bwd_index += 1 

1751 self.exec_order[stage_index] = order_list 

1752 

1753 

1754class ScheduleInterleaved1F1B(PipelineScheduleRuntime): 

1755 """The Interleaved 1F1B schedule. 

1756 

1757 Supports multiple stages per rank. In steady state, performs one 

1758 forward followed by one backward on each micro-batch. Handles the 

1759 cases where ``micro_batch_num`` is less than, equal to, or greater 

1760 than the stage count, including non-evenly-divisible micro counts. 

1761 

1762 Two orthogonal overlap modes can be enabled via constructor flags: 

1763 

1764 * ``overlap_p2p=True``: defer P2P recv ``handle.wait()`` until the 

1765 consuming FWD/BWD step (or the OVERLAP_B_F callback when 

1766 ``overlap_b_f=True``), letting recv overlap with prior compute. 

1767 * ``overlap_b_f=True``: in the 1F1B steady state, pair consecutive 

1768 ``(B_i, F_{i+1})`` steps into ``OVERLAP_B_F`` composite steps so 

1769 a registered callback can drive comm/compute overlap (typically 

1770 via :class:`CommComputeOverlap` for MoE EP A2A). Users register 

1771 the callback through :meth:`register_custom_function`. 

1772 * ``enable_dxdw_split=True`` (requires ``overlap_b_f``): each 

1773 steady-state pair becomes ``(BWD_INPUT_i, F_{i+1})`` and the 

1774 ``BWD_WEIGHT_i`` runs as its own step after the pair's P2P gap 

1775 (see :func:`split_overlap_dxdw`), so the input-grad send is 

1776 issued once dx and the paired forward finish instead of waiting 

1777 out the full backward. 

1778 

1779 The two overlap flags are independent and can be combined. 

1780 

1781 Example: 

1782 >>> # Plain interleaved 1F1B 

1783 >>> sched = ScheduleInterleaved1F1B(stages, 8) 

1784 >>> # With B/F overlap (dual-pipe-style comm/compute overlap) 

1785 >>> sched = ScheduleInterleaved1F1B(stages, 8, overlap_b_f=True) 

1786 >>> sched.register_custom_function(MetaStepType.OVERLAP_B_F, callback) 

1787 """ 

1788 def __init__(self, 

1789 stages, 

1790 micro_batch_num, 

1791 args_batch_dim=None, 

1792 kwargs_batch_dim=None, 

1793 output_concat_dim=None, 

1794 overlap_p2p=False, 

1795 overlap_b_f=False, 

1796 swap=False, 

1797 enable_dxdw_split=False, 

1798 p2p_transport="auto"): 

1799 super().__init__(stages, 

1800 micro_batch_num, 

1801 args_batch_dim=args_batch_dim, 

1802 kwargs_batch_dim=kwargs_batch_dim, 

1803 output_concat_dim=output_concat_dim, 

1804 overlap_p2p=overlap_p2p, 

1805 swap=swap, 

1806 p2p_transport=p2p_transport) 

1807 # _overlap_b_f selects between plain F/B emission and OVERLAP_B_F 

1808 # pairing in the 1F1B steady-state phase. Must be set before 

1809 # ``construct_stage_exec_order`` is called below. 

1810 self._overlap_b_f = overlap_b_f 

1811 # dx/dw split: ``construct_exec_order`` rewrites each steady-state 

1812 # OVERLAP_B_F pair to ``(BWD_INPUT, FWD)`` and re-emits the matching 

1813 # BWD_WEIGHT after the pair's P2P gap (``split_overlap_dxdw``), so the 

1814 # input-grad send leaves at ``max(dx, fwd)`` and dw overlaps with the 

1815 # in-flight P2P instead of delaying it. 

1816 self._enable_dxdw_split = enable_dxdw_split 

1817 if enable_dxdw_split and not overlap_b_f: 

1818 raise ValueError( 

1819 "enable_dxdw_split=True requires overlap_b_f=True; the split " 

1820 "is only applied to BWD sub-steps inside OVERLAP_B_F composite steps." 

1821 ) 

1822 if enable_dxdw_split and swap: 

1823 raise ValueError( 

1824 "enable_dxdw_split=True is incompatible with swap=True: " 

1825 "pipeline-swap injection anchors on unified BWD steps and " 

1826 "does not recognize the split BWD_INPUT/BWD_WEIGHT steps." 

1827 ) 

1828 

1829 self._init_round_layout() 

1830 self.build_exec_order() 

1831 

1832 def _init_round_layout(self): 

1833 """Compute per-round micro-batch counts used by stage-order emission. 

1834 

1835 Populates ``n_rounds``, ``n_microbatch_per_round`` and its prefix-sum 

1836 ``n_microbatch_per_round_accu`` from ``micro_batch_num``, 

1837 ``real_stage_num`` and ``n_local_stages``. Factored out of 

1838 ``__init__`` so the pure schedule-construction path (used by offline 

1839 unit tests) can be exercised without instantiating stages. 

1840 """ 

1841 self.n_rounds = max(1, self.micro_batch_num // self.real_stage_num) 

1842 if self.micro_batch_num < self.real_stage_num: 

1843 base = self.micro_batch_num - self.real_stage_num 

1844 remainder = 0 

1845 else: 

1846 n_extra_microbatch = self.micro_batch_num % self.real_stage_num 

1847 base = n_extra_microbatch // self.n_rounds 

1848 remainder = n_extra_microbatch % self.n_rounds 

1849 self.n_microbatch_per_round = \ 

1850 [self.real_stage_num + base + 1 if i < remainder else 

1851 self.real_stage_num + base for i in range(self.n_rounds)] 

1852 self.n_microbatch_per_round_accu = \ 

1853 [x * self.n_local_stages for x in itertools.accumulate(self.n_microbatch_per_round)] 

1854 self.n_microbatch_per_round_accu.insert(0, 0) 

1855 

1856 def construct_exec_order(self): 

1857 for stage_index in range(self.real_stage_num): 

1858 self.exec_order[stage_index] = self.construct_stage_exec_order(stage_index) 

1859 self.exec_order = add_send_recv(self.exec_order, self._stage_num, self.real_stage_num, style='loop') 

1860 if self.enable_dxdw_split: 

1861 self.exec_order = split_overlap_dxdw(self.exec_order) 

1862 

1863 def _build_stage_to_rank_index(self) -> None: 

1864 self._stage_to_rank_index = generate_stage_to_rank_mapping( 

1865 self.real_stage_num, self._stage_num, style='loop' 

1866 ) 

1867 

1868 def warmup_ops(self, stage_index): 

1869 """warmup phase.""" 

1870 warmup_ops_last_stage = (self.n_local_stages - 1) * self.n_microbatch_per_round[0] 

1871 warmup_ops = warmup_ops_last_stage + 2 * (self.real_stage_num - 1 - stage_index) 

1872 return min(warmup_ops, self.micro_batch_num * self.n_local_stages) 

1873 

1874 def forward_stage_index(self, op_index, stage_index): 

1875 """obtain forward stage_index based on op_index.""" 

1876 accu_index = bisect.bisect_right(self.n_microbatch_per_round_accu, op_index) - 1 

1877 local_index = (op_index - self.n_microbatch_per_round_accu[accu_index]) // \ 

1878 self.n_microbatch_per_round[accu_index] 

1879 return (local_index * self.real_stage_num) + stage_index 

1880 

1881 def backward_stage_index(self, op_index, stage_index): 

1882 """obtain backward stage_index based on op_index.""" 

1883 accu_index = bisect.bisect_right(self.n_microbatch_per_round_accu, op_index) - 1 

1884 local_index = (op_index - self.n_microbatch_per_round_accu[accu_index]) // \ 

1885 self.n_microbatch_per_round[accu_index] 

1886 local_index = self.n_local_stages - 1 - local_index 

1887 return (local_index * self.real_stage_num) + stage_index 

1888 

1889 def _short_micro(self) -> bool: 

1890 """True when ``micro_batch_num < real_stage_num`` (extra-bubble regime).""" 

1891 return self.micro_batch_num < self.real_stage_num 

1892 

1893 def _trailing_bubble(self) -> int: 

1894 """Bubble count appended after a BWD with ``micro == micro_batch_num - 1`` 

1895 in the short-micro regime. 

1896 """ 

1897 return self.real_stage_num - self.micro_batch_num 

1898 

1899 def _emit_warmup_ops(self, stage_index, warmup_ops, fwd_stage_micro_index): 

1900 """Emit pure-FWD warmup ops with optional short-micro bubble padding.""" 

1901 ops = [] 

1902 short = self._short_micro() 

1903 last_micro = self.micro_batch_num - 1 

1904 last_stage = self.real_stage_num - 1 

1905 bubble = self._trailing_bubble() 

1906 for op_idx in range(warmup_ops): 

1907 fwd_stage_idx = self.forward_stage_index(op_idx, stage_index) 

1908 fwd_micro_idx = fwd_stage_micro_index[fwd_stage_idx] 

1909 ops.append(MetaStep(fwd_micro_idx, MetaStepType.FWD, fwd_stage_idx)) 

1910 need_pad = ( 

1911 short 

1912 and fwd_micro_idx == last_micro 

1913 and (op_idx != warmup_ops - 1 or stage_index == last_stage) 

1914 ) 

1915 if need_pad: 

1916 ops.extend([None] * bubble) 

1917 fwd_stage_micro_index[fwd_stage_idx] += 1 

1918 return ops 

1919 

1920 def _emit_cooldown_ops(self, stage_index, warmup_ops, fwd_bwd_ops, total_ops, 

1921 bwd_stage_micro_index): 

1922 """Emit pure-BWD cooldown ops (each preceded by a bubble) with 

1923 optional short-micro trailing padding. 

1924 """ 

1925 ops = [] 

1926 short = self._short_micro() 

1927 last_micro = self.micro_batch_num - 1 

1928 # Double the bubble at each chunk's last-micro BWD: one ``bubble`` covers 

1929 # the missing ``rs - micro`` micros, the second offsets the next chunk 

1930 # by 2 slots so the wrap-around grad (rank 0 stage ``rs`` -> rank 

1931 # last_stage stage ``rs - 1``) lands AFTER its producer in column-scan 

1932 # time. Matches the +2 cooldown-rhythm offset that non-short Interleaved 

1933 # 1F1B naturally has from extra 1F1B ops on rank last_stage. 

1934 bubble = 2 * self._trailing_bubble() 

1935 for op_idx in range(warmup_ops + fwd_bwd_ops, total_ops): 

1936 ops.append(None) 

1937 bwd_stage_idx = self.backward_stage_index(op_idx - warmup_ops, stage_index) 

1938 bwd_micro_idx = bwd_stage_micro_index[bwd_stage_idx] 

1939 ops.append(MetaStep(bwd_micro_idx, MetaStepType.BWD, bwd_stage_idx)) 

1940 if short and bwd_micro_idx == last_micro: 

1941 ops.extend([None] * bubble) 

1942 bwd_stage_micro_index[bwd_stage_idx] += 1 

1943 return ops 

1944 

1945 def _emit_1f1b_ops(self, stage_index, warmup_ops, fwd_bwd_ops, 

1946 fwd_stage_micro_index, bwd_stage_micro_index): 

1947 """Emit interleaved (FWD, BWD) pairs for the 1F1B steady-state phase.""" 

1948 ops = [] 

1949 short = self._short_micro() 

1950 last_micro = self.micro_batch_num - 1 

1951 last_stage = self.real_stage_num - 1 

1952 # Double the bubble at the 1F1B->cooldown chunk boundary on rank 

1953 # last_stage; see :meth:`_emit_cooldown_ops` for the alignment rationale. 

1954 bubble = 2 * self._trailing_bubble() 

1955 for op_idx in range(warmup_ops, warmup_ops + fwd_bwd_ops): 

1956 fwd_stage_idx = self.forward_stage_index(op_idx, stage_index) 

1957 fwd_micro_idx = fwd_stage_micro_index[fwd_stage_idx] 

1958 ops.append(MetaStep(fwd_micro_idx, MetaStepType.FWD, fwd_stage_idx)) 

1959 fwd_stage_micro_index[fwd_stage_idx] += 1 

1960 bwd_stage_idx = self.backward_stage_index(op_idx - warmup_ops, stage_index) 

1961 bwd_micro_idx = bwd_stage_micro_index[bwd_stage_idx] 

1962 ops.append(MetaStep(bwd_micro_idx, MetaStepType.BWD, bwd_stage_idx)) 

1963 need_pad = ( 

1964 short 

1965 and bwd_micro_idx == last_micro 

1966 and stage_index == last_stage 

1967 ) 

1968 if need_pad: 

1969 ops.extend([None] * bubble) 

1970 bwd_stage_micro_index[bwd_stage_idx] += 1 

1971 return ops 

1972 

1973 @staticmethod 

1974 def _collect_fwd_bwd_steps(emit_fwd, emit_bwd, fwd_bwd_ops, warmup_ops): 

1975 """Walk the 1F1B range collecting parallel ``fwd_steps`` / ``bwd_steps``. 

1976 

1977 ``emit_fwd(op_idx)`` and ``emit_bwd(op_idx)`` build a single 

1978 :class:`MetaStep` and advance their respective per-stage micro 

1979 counters as a side effect. 

1980 """ 

1981 fwd_steps = [] 

1982 bwd_steps = [] 

1983 for op_idx in range(warmup_ops, warmup_ops + fwd_bwd_ops): 

1984 fwd_steps.append(emit_fwd(op_idx)) 

1985 bwd_steps.append(emit_bwd(op_idx)) 

1986 return fwd_steps, bwd_steps 

1987 

1988 @staticmethod 

1989 def _pair_into_overlap_b_f(fwd_steps, bwd_steps): 

1990 """Build ``F₁, [B_i, F_{i+1}], B_n`` ordering with OVERLAP_B_F pairs. 

1991 

1992 ``sub_steps`` carry the ``(bwd, fwd)`` tuple — callbacks access 

1993 them via ``step.sub_steps`` to recover per-direction stage / 

1994 micro info. 

1995 """ 

1996 ops = [] 

1997 if fwd_steps: 

1998 ops.append(fwd_steps[0]) # F₁ runs alone 

1999 for i in range(len(bwd_steps) - 1): 

2000 ops.append(MetaStep( 

2001 None, MetaStepType.OVERLAP_B_F, None, 

2002 sub_steps=(bwd_steps[i], fwd_steps[i + 1]), 

2003 )) 

2004 if bwd_steps: 

2005 ops.append(bwd_steps[-1]) # B_n runs alone 

2006 return ops 

2007 

2008 def _emit_1f1b_overlap_ops(self, stage_index, warmup_ops, fwd_bwd_ops, 

2009 fwd_stage_micro_index, bwd_stage_micro_index): 

2010 """Emit ``F₁, [B_i, F_{i+1}], B_n`` for the 1F1B phase under 

2011 ``overlap_b_f=True``. Each ``[B_i, F_{i+1}]`` becomes an 

2012 ``OVERLAP_B_F`` composite step; a registered callback drives the 

2013 actual concurrent execution. Short-micro extra-bubble padding 

2014 on the last rank is appended after ``B_n``. 

2015 """ 

2016 def emit_fwd(op_idx): 

2017 fwd_si = self.forward_stage_index(op_idx, stage_index) 

2018 fwd_mi = fwd_stage_micro_index[fwd_si] 

2019 fwd_stage_micro_index[fwd_si] += 1 

2020 return MetaStep(fwd_mi, MetaStepType.FWD, fwd_si) 

2021 

2022 def emit_bwd(op_idx): 

2023 bwd_si = self.backward_stage_index(op_idx - warmup_ops, stage_index) 

2024 bwd_mi = bwd_stage_micro_index[bwd_si] 

2025 bwd_stage_micro_index[bwd_si] += 1 

2026 return MetaStep(bwd_mi, MetaStepType.BWD, bwd_si) 

2027 

2028 fwd_steps, bwd_steps = self._collect_fwd_bwd_steps( 

2029 emit_fwd, emit_bwd, fwd_bwd_ops, warmup_ops, 

2030 ) 

2031 ops = self._pair_into_overlap_b_f(fwd_steps, bwd_steps) 

2032 

2033 last_stage = self.real_stage_num - 1 

2034 if self._short_micro() and stage_index == last_stage and bwd_steps: 

2035 if bwd_steps[-1].micro_index == self.micro_batch_num - 1: 

2036 # Double the bubble at the 1F1B->cooldown chunk boundary; 

2037 # see :meth:`_emit_cooldown_ops` for the alignment rationale. 

2038 ops.extend([None] * (2 * self._trailing_bubble())) 

2039 return ops 

2040 

2041 def construct_stage_exec_order(self, stage_index): 

2042 """Construct the execution order for ``stage_index``. 

2043 

2044 Builds: warmup → bubbles → 1F1B steady state → cooldown. The 

2045 1F1B segment switches between :meth:`_emit_1f1b_ops` (plain) and 

2046 :meth:`_emit_1f1b_overlap_ops` (OVERLAP_B_F pairing) based on 

2047 the ``overlap_b_f`` constructor flag. 

2048 """ 

2049 warmup_ops = self.warmup_ops(stage_index) 

2050 fwd_bwd_ops = self.n_local_stages * self.micro_batch_num - warmup_ops 

2051 total_ops = 2 * warmup_ops + fwd_bwd_ops 

2052 order_list = [None for _ in range(stage_index)] 

2053 fwd_stage_micro_index = defaultdict(int) 

2054 bwd_stage_micro_index = defaultdict(int) 

2055 order_list.extend(self._emit_warmup_ops(stage_index, warmup_ops, fwd_stage_micro_index)) 

2056 bubbles_before_1f1b = max( 

2057 0, 

2058 2 * (self.real_stage_num - stage_index - 1) - self.micro_batch_num, 

2059 ) 

2060 order_list.extend([None] * bubbles_before_1f1b) 

2061 order_list.extend([None] * (self.real_stage_num - 1 - stage_index)) 

2062 if self._overlap_b_f: 

2063 order_list.extend(self._emit_1f1b_overlap_ops( 

2064 stage_index, warmup_ops, fwd_bwd_ops, 

2065 fwd_stage_micro_index, bwd_stage_micro_index, 

2066 )) 

2067 else: 

2068 order_list.extend(self._emit_1f1b_ops( 

2069 stage_index, warmup_ops, fwd_bwd_ops, 

2070 fwd_stage_micro_index, bwd_stage_micro_index, 

2071 )) 

2072 order_list.extend(self._emit_cooldown_ops( 

2073 stage_index, warmup_ops, fwd_bwd_ops, total_ops, bwd_stage_micro_index, 

2074 )) 

2075 return order_list 

2076 

2077 

2078def detect_cycle_in_graph(ranks_map): 

2079 """ 

2080 Detects a cycle in the directed graph constructed from ranks_map. 

2081 

2082 Args: 

2083 ranks_map: A dictionary where keys are rank names and values are lists of nodes. 

2084 

2085 Returns: 

2086 tuple: (cycle_path, cycle_ranks) where cycle_path is a list of nodes forming the cycle and cycle_ranks 

2087 is a list of rank transitions corresponding to the cycle path. 

2088 """ 

2089 graph = defaultdict(list) 

2090 rank_edges = {} 

2091 

2092 for rank, nodes in ranks_map.items(): 

2093 for i in range(len(nodes) - 1): 

2094 u, v = nodes[i], nodes[i + 1] 

2095 graph[u].append(v) 

2096 rank_edges[(u, v)] = rank 

2097 

2098 visited = set() 

2099 path = [] 

2100 node_indices = {} 

2101 cycle_path = [] 

2102 cycle_ranks = [] 

2103 

2104 stack = [] 

2105 for node in list(graph.keys()): 

2106 if node not in visited: 

2107 stack.append((node, False)) 

2108 while stack: 

2109 current_node, is_processed = stack.pop() 

2110 

2111 if is_processed: 

2112 path.pop() 

2113 del node_indices[current_node] 

2114 continue 

2115 

2116 if current_node in node_indices: 

2117 cycle_start = node_indices[current_node] 

2118 cycle_path = path[cycle_start:] + [current_node] 

2119 for i in range(cycle_start, len(path)): 

2120 u = path[i] 

2121 v = path[i + 1] if i + 1 < len(path) else current_node 

2122 cycle_ranks.append(f"{rank_edges[(u, v)]} {u} -> {v}") 

2123 return cycle_path, cycle_ranks 

2124 

2125 if current_node in visited: 

2126 continue 

2127 

2128 visited.add(current_node) 

2129 node_indices[current_node] = len(path) 

2130 path.append(current_node) 

2131 

2132 stack.append((current_node, True)) 

2133 for neighbor in reversed(graph[current_node]): 

2134 stack.append((neighbor, False)) 

2135 

2136 return None, None 

2137 

2138 

2139def output_cycle_results(cycle_path, cycle_ranks): 

2140 """ 

2141 Helper function to output cycle detection results. 

2142 

2143 Args: 

2144 cycle_path (list): List of nodes forming a cycle, if any. 

2145 cycle_ranks (list): List of ranks involved in the cycle. 

2146 

2147 Returns: 

2148 None: Outputs results to the console. 

2149 """ 

2150 if cycle_path: 

2151 logger.error("Cycle detected:") 

2152 path_str = " -> ".join(str(node) for node in cycle_path) 

2153 logger.error("%s -> %s", path_str, cycle_path[0]) # Close the cycle 

2154 logger.error("Involving ranks:") 

2155 for rank in cycle_ranks: 

2156 logger.error(rank) 

2157 else: 

2158 logger.warning("Cycle Check succeeded. There is no cycle in the graph.") 

2159 

2160 

2161def parse_and_validate(data: dict, all_rank: bool = True): 

2162 """ 

2163 Parse and validate execution orders in a directed graph structure. 

2164 

2165 This function checks the integrity and consistency of a given dataset, ensuring all required 

2166 keys are present and correctly referenced. It also validates the structure of the input data 

2167 and parses string values to extract meaningful components. 

2168 

2169 Args: 

2170 data (dict): A dictionary where keys are string identifiers and values are lists of strings. 

2171 Each value represents a dependency or reference to other keys. 

2172 all_rank (bool): If True, checks that all elements referenced in the data are present as keys 

2173 in the dictionary. If False, only checks intersections. 

2174 

2175 Returns: 

2176 None: Log error messages to the console if validation fails, otherwise completes silently. 

2177 

2178 Raises: 

2179 ValueError: Raised indirectly if `parse_elements` encounters malformed input strings. 

2180 TypeError: Raised indirectly if data contains unexpected types. 

2181 """ 

2182 

2183 def parse_elements(value: str, max_groups: int = 2) -> set: 

2184 """Extract unique elements inside the first one or two parentheses from a string.""" 

2185 

2186 groups = re.findall(r'\((\d+)\)', value) 

2187 limited_groups = groups[:max_groups] # Limit to the first `max_groups` matches 

2188 

2189 return {item.strip() for item in limited_groups} 

2190 

2191 if not isinstance(data, dict): 

2192 logger.error("Input must be a dictionary with string keys and lists of strings as values.") 

2193 return 

2194 

2195 key_to_values = {key: set(values) for key, values in data.items() if 

2196 isinstance(values, list) and all(isinstance(v, str) for v in values)} 

2197 

2198 for key, values in data.items(): 

2199 if not isinstance(values, list) or not all(isinstance(v, str) for v in values): 

2200 logger.error("Values for key '%s' must be a list of strings.", key) 

2201 continue 

2202 

2203 for value in values: 

2204 try: 

2205 elements = parse_elements(value) 

2206 except (ValueError, TypeError, AttributeError) as e: 

2207 logger.error("Unable to parse elements from value '%s' in key '%s'. Error: %s", value, key, e) 

2208 continue 

2209 

2210 # Check for missing keys if all_rank is True 

2211 if all_rank: 

2212 missing_keys = elements - key_to_values.keys() 

2213 if missing_keys: 

2214 logger.error("The following keys are missing for value '%s': %s", value, missing_keys) 

2215 continue 

2216 

2217 # Check if the value is present in the referenced keys 

2218 for element in elements & key_to_values.keys() if not all_rank else elements: 

2219 if value not in key_to_values[element]: 

2220 logger.error("Key '%s' is missing the value '%s'.", element, value) 

2221 

2222 

2223def generate_operations(order_list: dict[int, list[MetaStep]], 

2224 chunk_num: int, 

2225 com_type: str = 'loop') -> dict[str, list[str]]: 

2226 """ 

2227 Generate formatted operations dictionary from pipeline execution order. 

2228 

2229 Args: 

2230 order_list (dict): Dictionary where keys are rank IDs and values are MetaStep execution sequences 

2231 chunk_num (int): Number of chunks (virtual pipeline stages) 

2232 com_type (str): Stage-to-rank mapping type ('loop' for cyclic, 'v' for V-shaped) 

2233 

2234 Returns: 

2235 Dictionary where keys are rank IDs (as strings) and values are lists of formatted operation strings 

2236 """ 

2237 

2238 def stage_to_rank(stage_index, style, stage_num, real_stage_num): 

2239 """Map stage index to rank""" 

2240 if style == 'loop': 

2241 return stage_index % real_stage_num 

2242 if style == 'v': 

2243 if stage_index < real_stage_num: 

2244 return stage_index 

2245 return stage_num - 1 - stage_index 

2246 raise ValueError("Invalid style") 

2247 

2248 def find_send_target(stage_idx, op_type): 

2249 """Find target stage for SEND operation""" 

2250 if op_type == MetaStepType.FWD_SEND: 

2251 return forward_comm.get(stage_idx) 

2252 return backward_comm.get(stage_idx) 

2253 

2254 def find_recv_source(stage_idx, op_type): 

2255 """Find source stage for RECV operation""" 

2256 if op_type == MetaStepType.FWD_RECV: 

2257 # Reverse lookup in forward_comm 

2258 for src, dst in forward_comm.items(): 

2259 if dst == stage_idx: 

2260 return src 

2261 else: 

2262 # Reverse lookup in backward_comm 

2263 for src, dst in backward_comm.items(): 

2264 if dst == stage_idx: 

2265 return src 

2266 return None 

2267 

2268 real_stage = len(order_list) 

2269 total_stages = real_stage * chunk_num 

2270 

2271 # Build communication rules 

2272 forward_comm = {} 

2273 backward_comm = {} 

2274 

2275 for i in range(total_stages): 

2276 if i + 1 < total_stages: 

2277 forward_comm[i] = i + 1 

2278 if i - 1 >= 0: 

2279 backward_comm[i] = i - 1 

2280 

2281 formatted_operations = defaultdict(list) 

2282 

2283 for rank, steps in order_list.items(): 

2284 operation_counter = defaultdict(int) 

2285 

2286 for step in steps: 

2287 if step.type in [MetaStepType.FWD_SEND, MetaStepType.BWD_SEND]: 

2288 target_stage = find_send_target(step.stage_index, step.type) 

2289 if target_stage is not None: 

2290 target_rank = stage_to_rank(target_stage, com_type, total_stages, real_stage) 

2291 comm_pair = (rank, target_rank, step.micro_index) 

2292 operation_counter[comm_pair] += 1 

2293 count = operation_counter[comm_pair] 

2294 formatted_op = f"Send_Receive_({rank})->({target_rank})_micro{step.micro_index}_{count}th" 

2295 formatted_operations[str(rank)].append(formatted_op) 

2296 

2297 elif step.type in [MetaStepType.FWD_RECV, MetaStepType.BWD_RECV]: 

2298 source_stage = find_recv_source(step.stage_index, step.type) 

2299 if source_stage is not None: 

2300 source_rank = stage_to_rank(source_stage, com_type, total_stages, real_stage) 

2301 comm_pair = (source_rank, rank, step.micro_index) 

2302 operation_counter[comm_pair] += 1 

2303 count = operation_counter[comm_pair] 

2304 formatted_op = f"Send_Receive_({source_rank})->({rank})_micro{step.micro_index}_{count}th" 

2305 formatted_operations[str(rank)].append(formatted_op) 

2306 

2307 # Convert defaultdict to dict 

2308 return dict(formatted_operations) 

2309 

2310 

2311def validate_pipeline_execution(order_list: dict[int, list[MetaStep]], 

2312 chunk_num: int, 

2313 com_type: str = 'loop') -> dict[str, any]: 

2314 """ 

2315 Comprehensive validation function for pipeline parallel execution order. 

2316 

2317 This function validates the execution order of pipeline parallelism by: 

2318 1. Checking SEND/RECV communication pair matching 

2319 2. Detecting duplicate operations 

2320 3. Detecting cycles in communication graphs 

2321 4. Verifying computation-SEND matching 

2322 

2323 Args: 

2324 order_list: Dictionary where keys are rank IDs and values are MetaStep execution sequences 

2325 chunk_num: Number of chunks (virtual pipeline stages) 

2326 com_type: Stage-to-rank mapping type ('loop' for cyclic, 'v' for V-shaped) 

2327 

2328 Returns: 

2329 Dictionary containing validation results with the following keys: 

2330 - validation: Communication pair validation results 

2331 - cycle_detection: Cycle detection results 

2332 - computation_send_matching: Computation-SEND matching validation results 

2333 - has_errors: Boolean indicating if any errors were found 

2334 - error_messages: List of all error messages found 

2335 - formatted_operations: Generated formatted operations 

2336 """ 

2337 

2338 # Generate operations 

2339 formatted_operations = generate_operations(order_list, chunk_num, com_type) 

2340 

2341 parse_and_validate(formatted_operations, True) 

2342 

2343 # Detect cycles 

2344 cycle_path, cycle_ranks = detect_cycle_in_graph(formatted_operations) 

2345 

2346 # Output results 

2347 output_cycle_results(cycle_path, cycle_ranks) 

2348 

2349 result = { 

2350 'formatted_operations': formatted_operations, 

2351 'cycle_path': cycle_path, 

2352 'cycle_ranks': cycle_ranks, 

2353 'has_cycle': bool(cycle_path) 

2354 } 

2355 return result 

2356 

2357 

2358_COMPUTE_META_STEP_TYPES = frozenset({ 

2359 MetaStepType.FWD, 

2360 MetaStepType.BWD, 

2361 MetaStepType.BWD_INPUT, 

2362 MetaStepType.BWD_WEIGHT, 

2363}) 

2364 

2365 

2366def _next_active_stage_indices(actions, start_index, max_active_stages, managed_stage_indices): 

2367 """Find the next distinct managed stages that will execute compute work. 

2368 

2369 Send/recv and previously injected FSDP control steps are skipped so that the 

2370 lookahead window only counts real compute, otherwise communication-only 

2371 actions would consume the budget and shrink the effective prefetch depth. 

2372 """ 

2373 stage_indices = [] 

2374 seen = set() 

2375 for action in actions[start_index:]: 

2376 for leaf_step in iter_leaf_meta_steps(action): 

2377 if leaf_step.type not in _COMPUTE_META_STEP_TYPES: 

2378 continue 

2379 if leaf_step.stage_index not in managed_stage_indices or leaf_step.stage_index in seen: 

2380 continue 

2381 seen.add(leaf_step.stage_index) 

2382 stage_indices.append(leaf_step.stage_index) 

2383 if len(stage_indices) == max_active_stages: 

2384 return stage_indices 

2385 return stage_indices 

2386 

2387 

2388def add_fsdp_unshard_reshard(actions, managed_stage_indices, max_active_stages=3): 

2389 """Insert FSDP unshard/reshard actions for locally managed stages.""" 

2390 if not managed_stage_indices: 

2391 return actions 

2392 

2393 fsdp_actions = [] 

2394 active_stages = [] 

2395 for index, action in enumerate(actions): 

2396 next_stage_indices = _next_active_stage_indices( 

2397 actions, index, max_active_stages, managed_stage_indices 

2398 ) 

2399 evicted_stages = [stage_index for stage_index in active_stages if stage_index not in next_stage_indices] 

2400 fetched_stages = [stage_index for stage_index in next_stage_indices if stage_index not in active_stages] 

2401 for stage_index in evicted_stages: 

2402 fsdp_actions.append(MetaStep(None, MetaStepType.FSDP_RESHARD, stage_index)) 

2403 active_stages.remove(stage_index) 

2404 for stage_index in fetched_stages: 

2405 fsdp_actions.append(MetaStep(None, MetaStepType.FSDP_UNSHARD, stage_index)) 

2406 active_stages.append(stage_index) 

2407 fsdp_actions.append(action) 

2408 

2409 while active_stages: 

2410 fsdp_actions.append(MetaStep(None, MetaStepType.FSDP_RESHARD, active_stages.pop(0))) 

2411 return fsdp_actions 

2412 

2413 

2414def add_fsdp_reduce_grad(actions, managed_stage_indices, micro_batch_num): 

2415 """Insert FSDP reduce-grad actions after the last backward-like action of each stage.""" 

2416 if not managed_stage_indices: 

2417 return actions 

2418 

2419 fsdp_actions = [] 

2420 for action in actions: 

2421 fsdp_actions.append(action) 

2422 reduced_stage_indices = [] 

2423 for leaf_step in iter_leaf_meta_steps(action): 

2424 if leaf_step.stage_index not in managed_stage_indices: 

2425 continue 

2426 if leaf_step.type not in (MetaStepType.BWD, MetaStepType.BWD_WEIGHT): 

2427 continue 

2428 if leaf_step.micro_index != micro_batch_num - 1: 

2429 continue 

2430 if leaf_step.stage_index not in reduced_stage_indices: 

2431 reduced_stage_indices.append(leaf_step.stage_index) 

2432 for stage_index in reduced_stage_indices: 

2433 fsdp_actions.append(MetaStep(None, MetaStepType.FSDP_REDUCE_GRAD, stage_index)) 

2434 return fsdp_actions