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

1153 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-08-04 05:18 +0800

1# Copyright 2026 Huawei Technologies Co., Ltd 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14# ============================================================================ 

15"""pipeline schedule""" 

16from abc import ABC, abstractmethod 

17from contextlib import nullcontext 

18from enum import Enum, auto 

19from collections import defaultdict 

20import itertools 

21import bisect 

22import logging 

23import re 

24from typing import Any 

25 

26import hyper_parallel 

27from hyper_parallel.platform import get_platform 

28from hyper_parallel.core.fully_shard.api import HSDPModule 

29from hyper_parallel.core.pipeline_parallel.pipeline_swap import ( 

30 PipelineSwapSession, 

31 inject_pipeline_swap_steps, 

32 swap_launch_load, 

33 swap_launch_offload, 

34 swap_wait_load, 

35 swap_wait_offload, 

36) 

37from hyper_parallel.core.pipeline_parallel.utils import BatchDimSpec 

38platform = get_platform() 

39logger = logging.getLogger(__name__) 

40 

41 

42class MetaStepType(Enum): 

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

44 FWD = auto() 

45 BWD = auto() 

46 BWD_INPUT = auto() 

47 BWD_WEIGHT = auto() 

48 FWD_RECV = auto() 

49 FWD_SEND = auto() 

50 BWD_RECV = auto() 

51 BWD_SEND = auto() 

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

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

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

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

56 BATCH_SEND_RECV = auto() 

57 OVERLAP_F_B = auto() 

58 OVERLAP_B_F = auto() 

59 FSDP_UNSHARD = auto() 

60 FSDP_RESHARD = auto() 

61 FSDP_REDUCE_GRAD = auto() 

62 SWAP_LAUNCH_OFFLOAD = auto() 

63 SWAP_WAIT_OFFLOAD = auto() 

64 SWAP_LAUNCH_LOAD = auto() 

65 SWAP_WAIT_LOAD = auto() 

66 

67 

68class MetaStep: 

69 """ 

70 Meta step of PipelineSchedule. 

71 An execution list composed of MetaStep can be constructed 

72 and fed into the PipelineSchedule for execution. 

73 

74 Args: 

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

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

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

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

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

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

81 stage. 

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

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

84 ``OVERLAP_B_F``. 

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

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

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

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

89 ``attach_fwd_boundary_p2p``. Issued via 

90 :meth:`PipelineScheduleRuntime.exec_boundary_p2p`. 

91 """ 

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

93 boundary_p2p=None): 

94 self._type = meta_type 

95 self._micro_index = micro_index 

96 self._stage_index = stage_index 

97 self._sub_steps = sub_steps 

98 self._boundary_p2p = boundary_p2p 

99 

100 @property 

101 def micro_index(self): 

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

103 return self._micro_index 

104 

105 @property 

106 def stage_index(self): 

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

108 return self._stage_index 

109 

110 @property 

111 def type(self): 

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

113 return self._type 

114 

115 @property 

116 def sub_steps(self): 

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

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

119 return self._sub_steps 

120 

121 @property 

122 def boundary_p2p(self): 

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

124 return self._boundary_p2p 

125 

126 def __eq__(self, value): 

127 if not isinstance(value, MetaStep): 

128 return NotImplemented 

129 return (self.type == value.type 

130 and self.micro_index == value.micro_index 

131 and self.stage_index == value.stage_index 

132 and self.sub_steps == value.sub_steps) 

133 

134 def __ne__(self, value): 

135 if not isinstance(value, MetaStep): 

136 return NotImplemented 

137 return not self.__eq__(value) 

138 

139 def __hash__(self): 

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

141 

142 def __str__(self): 

143 if self.sub_steps: 

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

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

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

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

148 

149 def __repr__(self): 

150 return self.__str__() 

151 

152 @staticmethod 

153 def from_str(step_str): 

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

155 pass 

156 

157 

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

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

160 if style == 'loop': 

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

162 if style == 'v': 

163 if stage_num % real_stage_num != 0: 

164 raise ValueError( 

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

166 ) 

167 mapping = {} 

168 rank_index = 0 

169 for stage_index in range(stage_num): 

170 mapping[stage_index] = rank_index 

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

172 continue 

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

174 rank_index += 1 

175 else: 

176 rank_index -= 1 

177 return mapping 

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

179 

180 

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

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

183 stage_to_rank = generate_stage_to_rank_mapping(real_stage_num, stage_num, style) 

184 rank_to_stages = defaultdict(list) 

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

186 rank_to_stages[rank].append(stage_index) 

187 

188 for stages in rank_to_stages.values(): 

189 stages.sort() 

190 return dict(rank_to_stages) 

191 

192 

193def iter_leaf_meta_steps(step): 

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

195 

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

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

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

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

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

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

202 """ 

203 if step is None: 

204 return 

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

206 for sub_step in step.sub_steps: 

207 

208 yield from iter_leaf_meta_steps(sub_step) 

209 return 

210 yield step 

211 

212 

213class PipelineContext: 

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

215 :meth:`PipelineScheduleRuntime.register_custom_function`). 

216 

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

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

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

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

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

222 

223 Attributes: 

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

225 arg_mbs: Per-micro-batch positional args. 

226 kwarg_mbs: Per-micro-batch keyword args. 

227 losses: Mutable list collecting per-step losses. 

228 """ 

229 

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

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

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

233 self.schedule = schedule 

234 self.arg_mbs = arg_mbs 

235 self.kwarg_mbs = kwarg_mbs 

236 self.losses = losses 

237 

238 

239def _exec_fsdp_unshard(stage): 

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

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

242 if isinstance(module, HSDPModule): 

243 module.unshard() 

244 

245 

246def _exec_fsdp_reshard(stage): 

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

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

249 if isinstance(module, HSDPModule): 

250 module.reshard() 

251 

252 

253def _exec_fsdp_reduce_grad(stage): 

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

255 stage.execute_reduce_grad() 

256 

257 

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

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

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

261_FSDP_STEP_HANDLERS = { 

262 MetaStepType.FSDP_UNSHARD: _exec_fsdp_unshard, 

263 MetaStepType.FSDP_RESHARD: _exec_fsdp_reshard, 

264 MetaStepType.FSDP_REDUCE_GRAD: _exec_fsdp_reduce_grad, 

265} 

266 

267 

268class PipelineScheduleRuntime(ABC): 

269 """ 

270 Base class for pipeline schedule. 

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

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

273 

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

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

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

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

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

279 

280 Args: 

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

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

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

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

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

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

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

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

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

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

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

292 and ``ScheduleInterleaved1F1B``. Default ``False``. 

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

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

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

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

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

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

299 (escape hatch for transports or topologies where batching 

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

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

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

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

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

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

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

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

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

309 (EI0005). On backends that require full-group participation before 

310 the first batched P2P call, batch-backed modes prepare the PP group 

311 once at their first run boundary. 

312 """ 

313 

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

315 

316 def __init__(self, 

317 stages, 

318 micro_batch_num, 

319 args_batch_dim=None, 

320 kwargs_batch_dim=None, 

321 output_concat_dim=None, 

322 overlap_p2p=False, 

323 swap=False, 

324 p2p_transport="auto"): 

325 if p2p_transport not in self._P2P_TRANSPORTS: 

326 raise ValueError( 

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

328 f"{p2p_transport!r}" 

329 ) 

330 self.stages = self._check_stages(stages) 

331 self.micro_batch_num = micro_batch_num 

332 self._args_batch_dim = self._normalize_args_batch_dim(args_batch_dim) 

333 self._kwargs_batch_dim = self._normalize_kwargs_batch_dim(kwargs_batch_dim) 

334 self._output_concat_dim = output_concat_dim 

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

336 self._args_batch_dim, self._kwargs_batch_dim) 

337 self.n_local_stages = len(self.stages) 

338 self._stage_dict = self.convert_stages_dict() 

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

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

341 self._stage_to_rank_index = None 

342 self._overlap_p2p = overlap_p2p 

343 self.exec_order = {} 

344 self._init_stages() 

345 self._build_stage_to_rank_index() 

346 self.fwd_handle_cache = {} 

347 self.bwd_handle_cache = {} 

348 self._custom_fn_map = {} 

349 self._pp_swap_enabled = swap 

350 self._swap_keys = frozenset() 

351 self._swap_session = None 

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

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

354 self._send_handles = [] 

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

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

357 # 

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

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

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

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

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

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

364 # shave EP a2a overlap). 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

383 # 

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

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

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

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

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

389 self._p2p_transport = p2p_transport 

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

391 self._p2p_mode = None 

392 self._batch_p2p = False 

393 self._batch_p2p_group = None 

394 self._batch_p2p_group_initialized = False 

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

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

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

398 self._boundary_issued = set() 

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

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

401 self._pending_boundary = {} 

402 

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

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

405 

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

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

408 of the built-in logic. 

409 

410 Args: 

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

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

413 

414 When pipeline activation swap is enabled, callbacks that 

415 execute FWD leaves must call ``ctx.schedule.execute_fwd_leaf`` 

416 so swap collection runs in the matching chunk context. 

417 

418 Example: 

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

420 ... fwd_step, bwd_step = step.sub_steps 

421 ... # custom parallel execution logic 

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

423 """ 

424 self._custom_fn_map[step_type] = fn 

425 

426 def _inject_local_fsdp_actions(self): 

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

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

429 managed_stage_indices = { 

430 stage.stage_index 

431 for stage in self.stages 

432 if any( 

433 isinstance(module, HSDPModule) 

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

435 ) 

436 } 

437 if not managed_stage_indices: 

438 return 

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

440 raise RuntimeError( 

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

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

443 ) 

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

445 self.exec_order[current_rank] = add_fsdp_reduce_grad( 

446 rank_actions, 

447 managed_stage_indices, 

448 self.micro_batch_num, 

449 ) 

450 

451 def _inject_local_pp_swap_actions(self): 

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

453 self._swap_keys = frozenset() 

454 if not self._pp_swap_enabled: 

455 return 

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

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

458 self._swap_keys = frozenset( 

459 (step.stage_index, step.micro_index) 

460 for step in self.exec_order[current_rank] 

461 if step is not None and step.type == MetaStepType.SWAP_LAUNCH_OFFLOAD 

462 ) 

463 

464 @abstractmethod 

465 def _build_stage_to_rank_index(self) -> None: 

466 """ 

467 Build attribute of _stage_to_rank_index. 

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

469 """ 

470 

471 @abstractmethod 

472 def construct_exec_order(self) -> None: 

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

474 

475 def build_exec_order(self) -> None: 

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

477 

478 Meta-Step Ordering Contract 

479 -------------------------- 

480 The per-rank execution schedule is assembled in layers: 

481 

482 1. ``construct_exec_order()`` 

483 Pure compute (FWD/BWD) + P2P communication (SEND/RECV). 

484 

485 2. ``_inject_local_fsdp_actions()`` 

486 FSDP parameter management: UNSHARD before compute, RESHARD after, 

487 REDUCE_GRAD after the last backward of each stage. This layer must 

488 run *before* swap injection so that swap can see the already-placed 

489 FSDP steps and optimise its placement relative to them: 

490 

491 * After FWD: ``SWAP_LAUNCH_OFFLOAD`` before ``FSDP_RESHARD`` 

492 (start D2H early, freeing parameter memory is fast). 

493 * Before BWD: if a direct lookahead ``FSDP_UNSHARD`` exists, 

494 ``SWAP_LAUNCH_LOAD`` precedes it so H2D overlaps all-gather. 

495 If parameters are already unsharded, load is delayed until the 

496 BWD container to avoid extending activation residency. 

497 

498 3. ``coalesce_p2p()`` or ``attach_fwd_boundary_p2p()`` 

499 P2P batching — runs before swap injection so P2P containers are 

500 finalized first. 

501 

502 4. ``_inject_local_pp_swap_actions()`` 

503 Activation swap: leaf-local collection around ``FWD``, followed by 

504 ``LAUNCH_OFFLOAD → WAIT_OFFLOAD → LAUNCH_LOAD → WAIT_LOAD``. 

505 ``WAIT_LOAD`` executes on the scheduler thread immediately before 

506 the backward consumer container. 

507 When ``enable_dxdw_split`` is active, ``BWD_INPUT`` is used as 

508 the backward anchor because it is the activation consumer; 

509 ``BWD_WEIGHT`` receives no swap control steps. 

510 

511 Swap steps are inserted only into before/after slots around 

512 top-level containers and never inside ``BATCH_SEND_RECV.sub_steps``. 

513 This preserves symmetric, paired fusion-block members and avoids 

514 deadlocks caused by splitting a fused P2P block. 

515 

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

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

518 and ``"plain"`` everywhere else. The matching P2P order-rewrite pass 

519 runs after FSDP injection and before swap injection, so swap sees the 

520 finalized top-level P2P containers. 

521 """ 

522 mode = self._p2p_transport 

523 if mode == "auto": 

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

525 self._p2p_mode = mode 

526 self._batch_p2p = mode != "plain" 

527 self._batch_p2p_group = self._resolve_batch_p2p_group() if self._batch_p2p else None 

528 self.construct_exec_order() 

529 # FSDP must inject before PP-swap so that swap can optimise 

530 # placement relative to FSDP steps (LAUNCH_OFFLOAD before 

531 # RESHARD after FWD; LAUNCH_LOAD before UNSHARD for BWD). 

532 self._inject_local_fsdp_actions() 

533 if mode == "boundary": 

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

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

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

537 # NO coalesce_p2p (see __init__). 

538 self.exec_order = attach_fwd_boundary_p2p(self.exec_order) 

539 elif mode == "batch": 

540 # Coalesce contiguous P2P runs into BATCH_SEND_RECV so the runtime 

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

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

543 # __init__. 

544 self.exec_order = coalesce_p2p(self.exec_order) 

545 self._inject_local_pp_swap_actions() 

546 

547 def convert_stages_dict(self): 

548 """convert stages to dict.""" 

549 stage_dict = {} 

550 for stage in self.stages: 

551 stage_dict[stage.stage_index] = stage 

552 return stage_dict 

553 

554 def split_microbatches(self, args, kwargs): 

555 """split_microbatches.""" 

556 if args or kwargs: 

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

558 return args_split, kwargs_split 

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

560 

561 @staticmethod 

562 def _to_spec(elem): 

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

564 

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

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

567 are not silently read as dims 1/0. 

568 """ 

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

570 return elem 

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

572 return BatchDimSpec(elem) 

573 raise TypeError( 

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

575 

576 @staticmethod 

577 def _normalize_args_batch_dim(args_batch_dim): 

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

579 

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

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

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

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

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

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

586 unchanged. 

587 """ 

588 if args_batch_dim is None: 

589 return None 

590 if isinstance(args_batch_dim, BatchDimSpec) or \ 

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

592 args_batch_dim = (args_batch_dim,) 

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

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

595 raise TypeError( 

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

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

598 

599 @staticmethod 

600 def _normalize_kwargs_batch_dim(kwargs_batch_dim): 

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

602 

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

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

605 per-key indexing is unchanged. 

606 """ 

607 if kwargs_batch_dim is None: 

608 return None 

609 if not isinstance(kwargs_batch_dim, dict): 

610 raise TypeError( 

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

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

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

614 

615 def _check_stages(self, stages): 

616 """check stages type.""" 

617 if isinstance(stages, hyper_parallel.PipelineStage): 

618 return [stages] 

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

620 for stage in stages: 

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

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

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

624 return stages 

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

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

627 

628 def _init_stages(self): 

629 """init stages.""" 

630 for stage in self.stages: 

631 stage.init(self.n_local_stages) 

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

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

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

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

636 

637 def _on_forward_chunk_done(self, stage_index, micro_index): 

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

639 

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

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

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

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

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

645 key and no-op. 

646 """ 

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

648 if step is not None: 

649 self.exec_boundary_p2p(step) 

650 

651 def run(self, *args: Any, **kwargs: Any) -> list: 

652 """schedule run.""" 

653 losses = [] 

654 try: 

655 if self._pp_swap_enabled: 

656 self._swap_session = PipelineSwapSession(self._swap_keys) 

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

658 self.run_microbatches(split_args, split_kwargs, losses) 

659 finally: 

660 self._drain_inflight_p2p() 

661 if self._swap_session is not None: 

662 self._swap_session.close() 

663 self._swap_session = None 

664 return losses 

665 

666 def sync_shared_parameters_grad(self): 

667 """sync_shared_parameters_grad.""" 

668 for stage in self.stages: 

669 stage.sync_shared_parameters_grad() 

670 

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

672 """update_losses.""" 

673 if stage.is_last_stage: 

674 losses.append(loss) 

675 

676 @property 

677 def enable_dxdw_split(self) -> bool: 

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

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

680 

681 def _wait_p2p(self, handles): 

682 for handle in handles: 

683 if handle is not None: 

684 handle.wait() 

685 

686 def _resolve_batch_p2p_group(self): 

687 """Return the common PP group required by every local stage.""" 

688 first_stage = self.stages[0] 

689 group = first_stage.pp_group 

690 for stage in self.stages[1:]: 

691 if stage.pp_group is group or stage.pp_group == group: 

692 continue 

693 raise ValueError( 

694 "Batch P2P requires all local stages to use the same PP process group, " 

695 f"but stages {first_stage.stage_index} and {stage.stage_index} use different groups." 

696 ) 

697 return group 

698 

699 def _ensure_batch_p2p_group_initialized(self) -> None: 

700 """Run backend-specific PP-group preparation once before batched P2P.""" 

701 if (not self._batch_p2p or self._batch_p2p_group_initialized 

702 or self.real_stage_num <= 1): 

703 return 

704 platform.prepare_batch_p2p_group(self._batch_p2p_group) 

705 self._batch_p2p_group_initialized = True 

706 

707 def _drain_inflight_p2p(self): 

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

709 

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

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

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

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

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

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

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

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

718 every cached recv was consumed. 

719 """ 

720 while self._send_handles: 

721 self._wait_p2p(self._send_handles.pop()) 

722 while self.fwd_handle_cache: 

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

724 while self.bwd_handle_cache: 

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

726 

727 def _batched_issue(self, specs): 

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

729 

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

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

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

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

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

735 """ 

736 if not specs: 

737 return [] 

738 ops = [ 

739 platform.p2p_op(op_type, tensor, peer, group=self._batch_p2p_group) 

740 for op_type, tensor, peer in specs 

741 ] 

742 handle = platform.batch_isend_irecv(ops) 

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

744 

745 # --- P2P step primitives ------------------------------------------------ 

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

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

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

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

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

751 # op waits inline. 

752 

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

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

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

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

757 if self._overlap_p2p: 

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

759 else: 

760 self._wait_p2p(handles) 

761 

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

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

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

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

766 if self._overlap_p2p: 

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

768 else: 

769 self._wait_p2p(handles) 

770 

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

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

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

774 if handles: 

775 self._wait_p2p(handles) 

776 

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

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

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

780 if handles: 

781 self._wait_p2p(handles) 

782 

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

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

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

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

787 if self._overlap_p2p: 

788 # Append the whole handle group: run_microbatches drains _send_handles 

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

790 self._send_handles.append(handles) 

791 else: 

792 self._wait_p2p(handles) 

793 

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

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

796 

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

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

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

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

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

802 """ 

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

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

805 if self._overlap_p2p: 

806 self._send_handles.append(handles) 

807 else: 

808 self._wait_p2p(handles) 

809 

810 def _arm_boundary(self, step): 

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

812 

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

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

815 what the hook receives when that forward chunk completes. 

816 """ 

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

818 return None 

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

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

821 if fwd_sub is None: 

822 return None 

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

824 self._pending_boundary[key] = step 

825 return key 

826 

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

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

829 self.exec_boundary_p2p(step) 

830 if armed_key is not None: 

831 self._pending_boundary.pop(armed_key, None) 

832 

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

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

835 

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

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

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

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

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

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

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

843 order instead of dropping the ops. 

844 

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

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

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

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

849 ``boundary_p2p``. 

850 """ 

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

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

853 return 

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

855 for sub in ops: 

856 stage = self._stage_dict[sub.stage_index] 

857 if sub.type == MetaStepType.FWD_SEND: 

858 self.send_fwd(stage, sub.micro_index) 

859 elif sub.type == MetaStepType.FWD_RECV: 

860 self.recv_fwd(stage, sub.micro_index) 

861 elif sub.type == MetaStepType.BWD_RECV: 

862 self.recv_bwd(stage, sub.micro_index) 

863 elif sub.type == MetaStepType.BWD_SEND: 

864 # attach_fwd_boundary_p2p never hoists BWD_SEND (its grad is 

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

866 self.send_bwd(stage, sub.micro_index) 

867 

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

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

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

871 _BATCH_SUB_DISPATCH = { 

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

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

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

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

876 } 

877 

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

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

880 

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

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

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

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

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

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

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

888 """ 

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

890 # for a recv, else None. 

891 tagged = [] 

892 for sub in step.sub_steps: 

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

894 stage = self._stage_dict[sub.stage_index] 

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

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

897 for op_type, tensor, peer in specs: 

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

899 

900 by_peer = {} 

901 for item in tagged: 

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

903 

904 for items in by_peer.values(): 

905 ops = [ 

906 platform.p2p_op(op_type, tensor, peer, group=self._batch_p2p_group) 

907 for op_type, tensor, peer, _ in items 

908 ] 

909 handle = platform.batch_isend_irecv(ops) 

910 if handle is None: 

911 continue 

912 if not self._overlap_p2p: 

913 self._wait_p2p([handle]) 

914 continue 

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

916 if recv_routes: 

917 for kind, si, mi in recv_routes: 

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

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

920 else: 

921 self._send_handles.append([handle]) 

922 

923 def _assert_in_unshard_if_needed(self, stage, check_step): 

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

925 return 

926 submodule_hsdp_scheduler = stage.submodule.hsdp_scheduler 

927 scheduler_state = submodule_hsdp_scheduler.hsdp_state 

928 if scheduler_state.is_shard: 

929 raise RuntimeError( 

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

931 f"state, but got sharded parameters." 

932 ) 

933 

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

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

936 

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

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

939 then runs. 

940 """ 

941 stage = self._stage_dict[cur_step.stage_index] 

942 micro_index = cur_step.micro_index 

943 step_type = cur_step.type 

944 

945 if step_type in ( 

946 MetaStepType.SWAP_LAUNCH_OFFLOAD, 

947 MetaStepType.SWAP_WAIT_OFFLOAD, 

948 MetaStepType.SWAP_LAUNCH_LOAD, 

949 MetaStepType.SWAP_WAIT_LOAD, 

950 ): 

951 self._exec_pipeline_swap_step(cur_step, arg_mbs, kwarg_mbs) 

952 

953 elif step_type == MetaStepType.FWD_RECV: 

954 self.recv_fwd(stage, micro_index) 

955 

956 elif step_type == MetaStepType.FWD: 

957 self.execute_fwd_leaf(cur_step, arg_mbs, kwarg_mbs, losses) 

958 

959 elif step_type == MetaStepType.FWD_SEND: 

960 self.send_fwd(stage, micro_index) 

961 

962 elif step_type == MetaStepType.BWD_RECV: 

963 self.recv_bwd(stage, micro_index) 

964 

965 elif step_type == MetaStepType.BWD_INPUT: 

966 self._assert_in_unshard_if_needed(stage, cur_step) 

967 self.wait_bwd_recv(stage.stage_index, micro_index) 

968 stage.backward_input_one_chunk(micro_index) 

969 

970 elif step_type == MetaStepType.BWD_WEIGHT: 

971 self._assert_in_unshard_if_needed(stage, cur_step) 

972 self.wait_bwd_recv(stage.stage_index, micro_index) 

973 stage.backward_weight_one_chunk(micro_index) 

974 

975 elif step_type == MetaStepType.BWD: 

976 self._assert_in_unshard_if_needed(stage, cur_step) 

977 self.wait_bwd_recv(stage.stage_index, micro_index) 

978 stage.backward_one_chunk(micro_index) 

979 

980 elif step_type == MetaStepType.BWD_SEND: 

981 self.send_bwd(stage, micro_index) 

982 

983 else: 

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

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

986 fsdp_handler = _FSDP_STEP_HANDLERS.get(step_type) 

987 if fsdp_handler is not None: 

988 fsdp_handler(stage) 

989 

990 def execute_fwd_leaf(self, step: MetaStep, arg_mbs: list, kwarg_mbs: list, losses: list) -> None: 

991 """Execute a forward leaf within its run-scoped swap group context.""" 

992 stage = self._stage_dict[step.stage_index] 

993 micro_index = step.micro_index 

994 self._assert_in_unshard_if_needed(stage, step) 

995 self.wait_fwd_recv(stage.stage_index, micro_index) 

996 swap_managed = self._swap_session is not None and self._swap_session.manages(step) 

997 group_context = self._swap_session.group_context(step) if swap_managed else nullcontext() 

998 with group_context: 

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

1000 if swap_managed: 

1001 # Boundary transport may pop the stage output cache from the 

1002 # after-forward hook before this leaf returns. Protect the 

1003 # local output while it is still directly available. 

1004 self._swap_session.protect_aliases(step, out) 

1005 self.update_losses(stage, out, losses) 

1006 

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

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

1009 if self._swap_session is None: 

1010 raise RuntimeError("Pipeline swap step executed without an active run session.") 

1011 if cur_step.type == MetaStepType.SWAP_LAUNCH_OFFLOAD: 

1012 swap_launch_offload(cur_step, self, arg_mbs, kwarg_mbs, self._swap_session) 

1013 elif cur_step.type == MetaStepType.SWAP_WAIT_OFFLOAD: 

1014 swap_wait_offload(cur_step, self._swap_session) 

1015 elif cur_step.type == MetaStepType.SWAP_LAUNCH_LOAD: 

1016 swap_launch_load(cur_step, self._swap_session) 

1017 elif cur_step.type == MetaStepType.SWAP_WAIT_LOAD: 

1018 swap_wait_load(cur_step, self._swap_session) 

1019 

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

1021 """Execute the schedule step by step. 

1022 

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

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

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

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

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

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

1029 :meth:`_exec_step`. 

1030 

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

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

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

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

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

1036 """ 

1037 self._ensure_batch_p2p_group_initialized() 

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

1039 self._send_handles = [] 

1040 self._boundary_issued = set() 

1041 self._pending_boundary = {} 

1042 ctx = None # lazily created 

1043 

1044 ordered = self.exec_order[real_stage_index] 

1045 total_steps = len(ordered) 

1046 logger.debug( 

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

1048 real_stage_index, total_steps, self.micro_batch_num, 

1049 ) 

1050 

1051 for step_idx, cur_step in enumerate(ordered): 

1052 if cur_step is None: 

1053 continue 

1054 

1055 logger.debug( 

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

1057 real_stage_index, step_idx, total_steps, cur_step, 

1058 ) 

1059 

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

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

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

1063 # no callback cooperation needed). 

1064 armed_key = self._arm_boundary(cur_step) 

1065 

1066 # Check for registered custom function 

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

1068 if custom_fn is not None: 

1069 if ctx is None: 

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

1071 custom_fn(cur_step, ctx) 

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

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

1074 # dropping them. Idempotent. 

1075 self._finish_boundary(cur_step, armed_key) 

1076 continue 

1077 

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

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

1080 if cur_step.type == MetaStepType.BATCH_SEND_RECV: 

1081 self._exec_batch_send_recv(cur_step) 

1082 continue 

1083 

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

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

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

1087 # semantically equivalent to non-overlapped 1F1B. 

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

1089 and cur_step.sub_steps): 

1090 for sub in cur_step.sub_steps: 

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

1092 self._finish_boundary(cur_step, armed_key) 

1093 continue 

1094 

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

1096 

1097 logger.debug( 

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

1099 real_stage_index, len(self._send_handles), 

1100 ) 

1101 self.sync_shared_parameters_grad() 

1102 while self._send_handles: 

1103 self._wait_p2p(self._send_handles.pop()) 

1104 

1105 

1106class _OverlapPhantom: 

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

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

1109 

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

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

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

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

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

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

1116 

1117 Each overlap step is expanded into two phantoms: 

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

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

1120 schedule here (only once). 

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

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

1123 """ 

1124 

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

1126 

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

1128 self.obf_step = obf_step 

1129 self.sub_step = sub_step 

1130 self.is_first_half = is_first_half 

1131 

1132 

1133def _expand_overlap_slots(scheduler, real_stage_num): 

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

1135 

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

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

1138 entries pass through unchanged. 

1139 """ 

1140 expanded = {} 

1141 for rank in range(real_stage_num): 

1142 order = scheduler[rank] 

1143 exp = [] 

1144 for op in order: 

1145 if (op is not None 

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

1147 and op.sub_steps): 

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

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

1150 else: 

1151 exp.append(op) 

1152 expanded[rank] = exp 

1153 return expanded 

1154 

1155 

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

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

1158 

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

1160 """ 

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

1162 item = current_items.get(rank) 

1163 if item is not None: 

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

1165 insert_step_comms(sub, rank, new_schedule) 

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

1167 item = current_items.get(rank) 

1168 if item is not None: 

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

1170 insert_step_comms(sub, rank, new_schedule) 

1171 

1172 

1173def _column_scan_insert_comms(expanded, real_stage_num, insert_step_comms): 

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

1175 

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

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

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

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

1180 

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

1182 P2P deadlocks between adjacent ranks. 

1183 

1184 Args: 

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

1186 real_stage_num: Number of physical ranks. 

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

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

1189 

1190 Returns: 

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

1192 """ 

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

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

1195 

1196 for time_step in range(max_length): 

1197 current_items = {} 

1198 for rank in range(real_stage_num): 

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

1200 item = expanded[rank][time_step] 

1201 current_items[rank] = item 

1202 if item is None: 

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

1204 # indexing aligned with the column scan. The runtime 

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

1206 # semantics-neutral. 

1207 new_schedule[rank].append(None) 

1208 continue 

1209 if isinstance(item, _OverlapPhantom): 

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

1211 if item.is_first_half: 

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

1213 else: 

1214 new_schedule[rank].append(item) 

1215 else: 

1216 current_items[rank] = None 

1217 

1218 _process_rank_items( 

1219 real_stage_num, current_items, insert_step_comms, new_schedule, 

1220 ) 

1221 

1222 return new_schedule 

1223 

1224 

1225_P2P_STEP_TYPES = frozenset({ 

1226 MetaStepType.FWD_SEND, MetaStepType.FWD_RECV, 

1227 MetaStepType.BWD_SEND, MetaStepType.BWD_RECV, 

1228}) 

1229 

1230 

1231def coalesce_p2p(exec_order): 

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

1233 

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

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

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

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

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

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

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

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

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

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

1244 

1245 Args: 

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

1247 

1248 Returns: 

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

1250 """ 

1251 def _flush(run, new): 

1252 if len(run) >= 2: 

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

1254 else: 

1255 new.extend(run) 

1256 

1257 out = {} 

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

1259 new = [] 

1260 run = [] 

1261 for step in order: 

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

1263 run.append(step) 

1264 continue 

1265 _flush(run, new) 

1266 run = [] 

1267 new.append(step) 

1268 _flush(run, new) 

1269 out[rank] = new 

1270 return out 

1271 

1272 

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

1274 

1275 

1276def attach_fwd_boundary_p2p(exec_order): 

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

1278 

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

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

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

1282 

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

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

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

1286 

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

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

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

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

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

1292 overlap's forward stay in the gap. 

1293 

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

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

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

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

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

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

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

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

1302 

1303 Args: 

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

1305 

1306 Returns: 

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

1308 """ 

1309 out = {} 

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

1311 new = [] 

1312 i = 0 

1313 while i < len(order): 

1314 step = order[i] 

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

1316 or not step.sub_steps): 

1317 new.append(step) 

1318 i += 1 

1319 continue 

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

1321 boundary, leftover = _split_boundary_run(step, run) 

1322 if not boundary: 

1323 new.append(step) 

1324 i += 1 

1325 continue 

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

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

1328 new.extend(leftover) 

1329 i = j 

1330 out[rank] = new 

1331 return out 

1332 

1333 

1334def _p2p_run_after(order, start): 

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

1336 

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

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

1339 """ 

1340 run = [] 

1341 j = start 

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

1343 run.append(order[j]) 

1344 j += 1 

1345 return run, j 

1346 

1347 

1348def _split_boundary_run(step, run): 

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

1350 

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

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

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

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

1355 """ 

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

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

1358 

1359 def _is_own_fwd_send(s): 

1360 return (fwd_sub is not None 

1361 and s.type == MetaStepType.FWD_SEND 

1362 and s.stage_index == fwd_sub.stage_index 

1363 and s.micro_index == fwd_sub.micro_index) 

1364 

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

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

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

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

1369 return tuple(boundary), leftover 

1370 

1371 

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

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

1374 

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

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

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

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

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

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

1381 

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

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

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

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

1386 

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

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

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

1390 

1391 Args: 

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

1393 

1394 Returns: 

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

1396 """ 

1397 out = {} 

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

1399 new = [] 

1400 i = 0 

1401 n = len(order) 

1402 while i < n: 

1403 step = order[i] 

1404 i += 1 

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

1406 or not step.sub_steps): 

1407 new.append(step) 

1408 continue 

1409 bwd_sub, fwd_sub = step.sub_steps 

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

1411 new.append(step) 

1412 continue 

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

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

1415 sub_steps=(dx, fwd_sub))) 

1416 run, i = _p2p_run_after(order, i) 

1417 new.extend(run) 

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

1419 bwd_sub.stage_index)) 

1420 out[rank] = new 

1421 return out 

1422 

1423 

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

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

1426 

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

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

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

1430 

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

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

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

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

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

1436 

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

1438 P2P deadlocks between adjacent ranks. 

1439 

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

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

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

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

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

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

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

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

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

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

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

1451 batches, whose per-pair sequences stay complementary. 

1452 

1453 Args: 

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

1455 with ``None`` for bubble slots. 

1456 stage_num: Total number of virtual pipeline stages. 

1457 real_stage_num: Number of physical ranks. 

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

1459 

1460 Returns: 

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

1462 """ 

1463 

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

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

1466 if style == 'loop': 

1467 return stage_index % real_stage_num 

1468 if style == 'v': 

1469 if stage_index < real_stage_num: 

1470 return stage_index 

1471 return stage_num - 1 - stage_index 

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

1473 

1474 def _fwd_peer(stage_index: int): 

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

1476 if stage_index >= stage_num - 1: 

1477 return None 

1478 peer = stage_to_rank(stage_index + 1) 

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

1480 

1481 def _bwd_peer(stage_index: int): 

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

1483 if stage_index <= 0: 

1484 return None 

1485 peer = stage_to_rank(stage_index - 1) 

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

1487 

1488 def _insert_comms_for_step(step, rank, new_schedule): 

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

1490 if step is None: 

1491 return 

1492 

1493 if step.type == MetaStepType.FWD: 

1494 peer = _fwd_peer(step.stage_index) 

1495 if peer is not None: 

1496 new_schedule[rank].append( 

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

1498 new_schedule[peer].append( 

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

1500 

1501 elif step.type == MetaStepType.BWD: 

1502 peer = _bwd_peer(step.stage_index) 

1503 if peer is not None: 

1504 new_schedule[rank].append( 

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

1506 new_schedule[peer].append( 

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

1508 

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

1510 for sub in step.sub_steps: 

1511 _insert_comms_for_step(sub, rank, new_schedule) 

1512 

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

1514 expanded = _expand_overlap_slots(scheduler, real_stage_num) 

1515 return _column_scan_insert_comms(expanded, real_stage_num, _insert_comms_for_step) 

1516 

1517 

1518_ALIGN_PAD = object() 

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

1520 

1521 

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

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

1524 

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

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

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

1528 always ready. 

1529 """ 

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

1531 if step.type == MetaStepType.FWD: 

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

1533 return True 

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

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

1536 if step.type == MetaStepType.BWD: 

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

1538 return True 

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

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

1541 return True 

1542 

1543 

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

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

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

1547 rhythm requires it. 

1548 

1549 Args: 

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

1551 1F1B-boundary padding. 

1552 stage_num: Total number of virtual pipeline stages. 

1553 real_stage_num: Number of physical ranks. 

1554 stage_to_rank: Topology mapping from stage to rank. 

1555 

1556 Returns: 

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

1558 insertion phase. 

1559 """ 

1560 remaining_fwd = { 

1561 rank: sum( 

1562 1 for s in padded[rank] 

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

1564 ) 

1565 for rank in range(real_stage_num) 

1566 } 

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

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

1569 done = {} 

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

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

1572 

1573 def _emit_bubble(rank): 

1574 aligned[rank].append(None) 

1575 last_was_cooldown_bwd[rank] = False 

1576 

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

1578 aligned[rank].append(step) 

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

1580 cursors[rank] += 1 

1581 if step.type == MetaStepType.FWD: 

1582 remaining_fwd[rank] -= 1 

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

1584 

1585 def _step_rank_at(t, rank): 

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

1587 return 

1588 item = padded[rank][cursors[rank]] 

1589 if item is _ALIGN_PAD: 

1590 _emit_bubble(rank) 

1591 cursors[rank] += 1 

1592 return 

1593 in_cooldown = remaining_fwd[rank] == 0 

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

1595 cooldown_skip = ( 

1596 in_cooldown 

1597 and item.type == MetaStepType.BWD 

1598 and last_was_cooldown_bwd[rank] 

1599 ) 

1600 if cooldown_skip: 

1601 _emit_bubble(rank) 

1602 return 

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

1604 _emit_bubble(rank) 

1605 return 

1606 _emit_step(rank, item, t, in_cooldown) 

1607 

1608 for t in range(max_t): 

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

1610 break 

1611 for rank in range(real_stage_num): 

1612 _step_rank_at(t, rank) 

1613 return aligned 

1614 

1615 

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

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

1618 

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

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

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

1622 automatically determines bubble placement via execution simulation. 

1623 

1624 Three constraints are enforced: 

1625 

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

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

1628 analogously for ``BWD``). 

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

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

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

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

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

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

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

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

1637 

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

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

1640 :func:`add_send_recv`. 

1641 

1642 Args: 

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

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

1645 stage_num: Total number of virtual pipeline stages. 

1646 real_stage_num: Number of physical ranks. 

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

1648 

1649 Returns: 

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

1651 and communication ops inserted. 

1652 """ 

1653 

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

1655 

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

1657 if style == 'loop': 

1658 return stage_index % real_stage_num 

1659 if style == 'v': 

1660 if stage_index < real_stage_num: 

1661 return stage_index 

1662 return stage_num - 1 - stage_index 

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

1664 

1665 def _fwd_peer(stage_index: int): 

1666 if stage_index >= stage_num - 1: 

1667 return None 

1668 peer = stage_to_rank(stage_index + 1) 

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

1670 

1671 def _bwd_peer(stage_index: int): 

1672 if stage_index <= 0: 

1673 return None 

1674 peer = stage_to_rank(stage_index - 1) 

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

1676 

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

1678 

1679 def _find_1f1b_boundary(order): 

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

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

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

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

1684 return i 

1685 return len(order) 

1686 

1687 padded = {} 

1688 for rank in range(real_stage_num): 

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

1690 boundary = _find_1f1b_boundary(order) 

1691 pad_count = real_stage_num - 1 - rank 

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

1693 

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

1695 

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

1697 

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

1699 

1700 def _insert_comms_for_step(step, rank, new_schedule): 

1701 if step is None: 

1702 return 

1703 if step.type == MetaStepType.FWD: 

1704 peer = _fwd_peer(step.stage_index) 

1705 if peer is not None: 

1706 new_schedule[rank].append( 

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

1708 new_schedule[peer].append( 

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

1710 elif step.type == MetaStepType.BWD: 

1711 peer = _bwd_peer(step.stage_index) 

1712 if peer is not None: 

1713 new_schedule[rank].append( 

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

1715 new_schedule[peer].append( 

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

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

1718 for sub in step.sub_steps: 

1719 _insert_comms_for_step(sub, rank, new_schedule) 

1720 

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

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

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

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

1725 expanded = _expand_overlap_slots(aligned, real_stage_num) 

1726 return _column_scan_insert_comms(expanded, real_stage_num, _insert_comms_for_step) 

1727 

1728 

1729class ScheduleGPipe(PipelineScheduleRuntime): 

1730 """ 

1731 The Gpipe schedule. 

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

1733 """ 

1734 def __init__(self, 

1735 stages, 

1736 micro_batch_num, 

1737 args_batch_dim=None, 

1738 kwargs_batch_dim=None, 

1739 output_concat_dim=None, 

1740 swap=False): 

1741 super().__init__(stages, 

1742 micro_batch_num, 

1743 args_batch_dim=args_batch_dim, 

1744 kwargs_batch_dim=kwargs_batch_dim, 

1745 output_concat_dim=output_concat_dim, 

1746 swap=swap) 

1747 self.build_exec_order() 

1748 

1749 def _build_stage_to_rank_index(self) -> None: 

1750 self._stage_to_rank_index = generate_stage_to_rank_mapping( 

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

1752 ) 

1753 

1754 def construct_exec_order(self): 

1755 """construct_exec_order of Gpipe.""" 

1756 for stage_index in range(self.real_stage_num): 

1757 order_list = [] 

1758 for mb_index in range(self.micro_batch_num): 

1759 if stage_index != 0: 

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

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

1762 if stage_index != self.real_stage_num - 1: 

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

1764 for mb_index in range(self.micro_batch_num): 

1765 if stage_index != self.real_stage_num - 1: 

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

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

1768 if stage_index != 0: 

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

1770 self.exec_order[stage_index] = order_list 

1771 

1772 

1773class Schedule1F1B(PipelineScheduleRuntime): 

1774 """ 

1775 The 1F1B schedule. 

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

1777 """ 

1778 def __init__(self, 

1779 stages, 

1780 micro_batch_num, 

1781 args_batch_dim=None, 

1782 kwargs_batch_dim=None, 

1783 output_concat_dim=None, 

1784 swap=False): 

1785 super().__init__(stages, 

1786 micro_batch_num, 

1787 args_batch_dim=args_batch_dim, 

1788 kwargs_batch_dim=kwargs_batch_dim, 

1789 output_concat_dim=output_concat_dim, 

1790 swap=swap) 

1791 self.build_exec_order() 

1792 

1793 def _build_stage_to_rank_index(self) -> None: 

1794 self._stage_to_rank_index = generate_stage_to_rank_mapping( 

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

1796 ) 

1797 

1798 def construct_exec_order(self): 

1799 """construct_exec_order of 1F1B.""" 

1800 for stage_index in range(self.real_stage_num): 

1801 order_list = [] 

1802 fwd_index = 0 

1803 bwd_index = 0 

1804 # warmup phase 

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

1806 for _ in range(warmup_micro_batches): 

1807 if stage_index != 0: 

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

1809 if stage_index % 2 == 0: 

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

1811 if fwd_index != warmup_micro_batches - 1: 

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

1813 else: 

1814 if fwd_index > 0: 

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

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

1817 fwd_index += 1 

1818 

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

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

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

1822 fwd_index += 1 

1823 # steady phase 

1824 steady_micro_batches = self.micro_batch_num - warmup_micro_batches 

1825 for _ in range(steady_micro_batches): 

1826 if stage_index != self.real_stage_num - 1: 

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

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

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

1830 

1831 if stage_index != 0: 

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

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

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

1835 fwd_index += 1 

1836 bwd_index += 1 

1837 

1838 # cooldown phase 

1839 cooldown_micro_batches = warmup_micro_batches 

1840 for _ in range(cooldown_micro_batches): 

1841 if stage_index != self.real_stage_num - 1: 

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

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

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

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

1846 

1847 if stage_index != 0: 

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

1849 bwd_index += 1 

1850 self.exec_order[stage_index] = order_list 

1851 

1852 

1853class ScheduleInterleaved1F1B(PipelineScheduleRuntime): 

1854 """The Interleaved 1F1B schedule. 

1855 

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

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

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

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

1860 

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

1862 

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

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

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

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

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

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

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

1870 the callback through :meth:`register_custom_function`. 

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

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

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

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

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

1876 out the full backward. 

1877 

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

1879 

1880 Example: 

1881 >>> # Plain interleaved 1F1B 

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

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

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

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

1886 """ 

1887 def __init__(self, 

1888 stages, 

1889 micro_batch_num, 

1890 args_batch_dim=None, 

1891 kwargs_batch_dim=None, 

1892 output_concat_dim=None, 

1893 overlap_p2p=False, 

1894 overlap_b_f=False, 

1895 swap=False, 

1896 enable_dxdw_split=False, 

1897 p2p_transport="auto"): 

1898 super().__init__(stages, 

1899 micro_batch_num, 

1900 args_batch_dim=args_batch_dim, 

1901 kwargs_batch_dim=kwargs_batch_dim, 

1902 output_concat_dim=output_concat_dim, 

1903 overlap_p2p=overlap_p2p, 

1904 swap=swap, 

1905 p2p_transport=p2p_transport) 

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

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

1908 # ``construct_stage_exec_order`` is called below. 

1909 self._overlap_b_f = overlap_b_f 

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

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

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

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

1914 # in-flight P2P instead of delaying it. 

1915 self._enable_dxdw_split = enable_dxdw_split 

1916 if enable_dxdw_split and not overlap_b_f: 

1917 raise ValueError( 

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

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

1920 ) 

1921 self._init_round_layout() 

1922 self.build_exec_order() 

1923 

1924 def _init_round_layout(self): 

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

1926 

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

1928 ``n_microbatch_per_round_accu`` from ``micro_batch_num``, 

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

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

1931 unit tests) can be exercised without instantiating stages. 

1932 """ 

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

1934 if self.micro_batch_num < self.real_stage_num: 

1935 base = self.micro_batch_num - self.real_stage_num 

1936 remainder = 0 

1937 else: 

1938 n_extra_microbatch = self.micro_batch_num % self.real_stage_num 

1939 base = n_extra_microbatch // self.n_rounds 

1940 remainder = n_extra_microbatch % self.n_rounds 

1941 self.n_microbatch_per_round = \ 

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

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

1944 self.n_microbatch_per_round_accu = \ 

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

1946 self.n_microbatch_per_round_accu.insert(0, 0) 

1947 

1948 def construct_exec_order(self): 

1949 for stage_index in range(self.real_stage_num): 

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

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

1952 if self.enable_dxdw_split: 

1953 self.exec_order = split_overlap_dxdw(self.exec_order) 

1954 

1955 def _build_stage_to_rank_index(self) -> None: 

1956 self._stage_to_rank_index = generate_stage_to_rank_mapping( 

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

1958 ) 

1959 

1960 def warmup_ops(self, stage_index): 

1961 """warmup phase.""" 

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

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

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

1965 

1966 def forward_stage_index(self, op_index, stage_index): 

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

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

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

1970 self.n_microbatch_per_round[accu_index] 

1971 return (local_index * self.real_stage_num) + stage_index 

1972 

1973 def backward_stage_index(self, op_index, stage_index): 

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

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

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

1977 self.n_microbatch_per_round[accu_index] 

1978 local_index = self.n_local_stages - 1 - local_index 

1979 return (local_index * self.real_stage_num) + stage_index 

1980 

1981 def _short_micro(self) -> bool: 

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

1983 return self.micro_batch_num < self.real_stage_num 

1984 

1985 def _trailing_bubble(self) -> int: 

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

1987 in the short-micro regime. 

1988 """ 

1989 return self.real_stage_num - self.micro_batch_num 

1990 

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

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

1993 ops = [] 

1994 short = self._short_micro() 

1995 last_micro = self.micro_batch_num - 1 

1996 last_stage = self.real_stage_num - 1 

1997 bubble = self._trailing_bubble() 

1998 for op_idx in range(warmup_ops): 

1999 fwd_stage_idx = self.forward_stage_index(op_idx, stage_index) 

2000 fwd_micro_idx = fwd_stage_micro_index[fwd_stage_idx] 

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

2002 need_pad = ( 

2003 short 

2004 and fwd_micro_idx == last_micro 

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

2006 ) 

2007 if need_pad: 

2008 ops.extend([None] * bubble) 

2009 fwd_stage_micro_index[fwd_stage_idx] += 1 

2010 return ops 

2011 

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

2013 bwd_stage_micro_index): 

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

2015 optional short-micro trailing padding. 

2016 """ 

2017 ops = [] 

2018 short = self._short_micro() 

2019 last_micro = self.micro_batch_num - 1 

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

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

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

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

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

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

2026 bubble = 2 * self._trailing_bubble() 

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

2028 ops.append(None) 

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

2030 bwd_micro_idx = bwd_stage_micro_index[bwd_stage_idx] 

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

2032 if short and bwd_micro_idx == last_micro: 

2033 ops.extend([None] * bubble) 

2034 bwd_stage_micro_index[bwd_stage_idx] += 1 

2035 return ops 

2036 

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

2038 fwd_stage_micro_index, bwd_stage_micro_index): 

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

2040 ops = [] 

2041 short = self._short_micro() 

2042 last_micro = self.micro_batch_num - 1 

2043 last_stage = self.real_stage_num - 1 

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

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

2046 bubble = 2 * self._trailing_bubble() 

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

2048 fwd_stage_idx = self.forward_stage_index(op_idx, stage_index) 

2049 fwd_micro_idx = fwd_stage_micro_index[fwd_stage_idx] 

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

2051 fwd_stage_micro_index[fwd_stage_idx] += 1 

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

2053 bwd_micro_idx = bwd_stage_micro_index[bwd_stage_idx] 

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

2055 need_pad = ( 

2056 short 

2057 and bwd_micro_idx == last_micro 

2058 and stage_index == last_stage 

2059 ) 

2060 if need_pad: 

2061 ops.extend([None] * bubble) 

2062 bwd_stage_micro_index[bwd_stage_idx] += 1 

2063 return ops 

2064 

2065 @staticmethod 

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

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

2068 

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

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

2071 counters as a side effect. 

2072 """ 

2073 fwd_steps = [] 

2074 bwd_steps = [] 

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

2076 fwd_steps.append(emit_fwd(op_idx)) 

2077 bwd_steps.append(emit_bwd(op_idx)) 

2078 return fwd_steps, bwd_steps 

2079 

2080 @staticmethod 

2081 def _pair_into_overlap_b_f(fwd_steps, bwd_steps): 

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

2083 

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

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

2086 micro info. 

2087 """ 

2088 ops = [] 

2089 if fwd_steps: 

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

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

2092 ops.append(MetaStep( 

2093 None, MetaStepType.OVERLAP_B_F, None, 

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

2095 )) 

2096 if bwd_steps: 

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

2098 return ops 

2099 

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

2101 fwd_stage_micro_index, bwd_stage_micro_index): 

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

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

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

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

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

2107 """ 

2108 def emit_fwd(op_idx): 

2109 fwd_si = self.forward_stage_index(op_idx, stage_index) 

2110 fwd_mi = fwd_stage_micro_index[fwd_si] 

2111 fwd_stage_micro_index[fwd_si] += 1 

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

2113 

2114 def emit_bwd(op_idx): 

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

2116 bwd_mi = bwd_stage_micro_index[bwd_si] 

2117 bwd_stage_micro_index[bwd_si] += 1 

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

2119 

2120 fwd_steps, bwd_steps = self._collect_fwd_bwd_steps( 

2121 emit_fwd, emit_bwd, fwd_bwd_ops, warmup_ops, 

2122 ) 

2123 ops = self._pair_into_overlap_b_f(fwd_steps, bwd_steps) 

2124 

2125 last_stage = self.real_stage_num - 1 

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

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

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

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

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

2131 return ops 

2132 

2133 def construct_stage_exec_order(self, stage_index): 

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

2135 

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

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

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

2139 the ``overlap_b_f`` constructor flag. 

2140 """ 

2141 warmup_ops = self.warmup_ops(stage_index) 

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

2143 total_ops = 2 * warmup_ops + fwd_bwd_ops 

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

2145 fwd_stage_micro_index = defaultdict(int) 

2146 bwd_stage_micro_index = defaultdict(int) 

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

2148 bubbles_before_1f1b = max( 

2149 0, 

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

2151 ) 

2152 order_list.extend([None] * bubbles_before_1f1b) 

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

2154 if self._overlap_b_f: 

2155 order_list.extend(self._emit_1f1b_overlap_ops( 

2156 stage_index, warmup_ops, fwd_bwd_ops, 

2157 fwd_stage_micro_index, bwd_stage_micro_index, 

2158 )) 

2159 else: 

2160 order_list.extend(self._emit_1f1b_ops( 

2161 stage_index, warmup_ops, fwd_bwd_ops, 

2162 fwd_stage_micro_index, bwd_stage_micro_index, 

2163 )) 

2164 order_list.extend(self._emit_cooldown_ops( 

2165 stage_index, warmup_ops, fwd_bwd_ops, total_ops, bwd_stage_micro_index, 

2166 )) 

2167 return order_list 

2168 

2169 

2170def detect_cycle_in_graph(ranks_map): 

2171 """ 

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

2173 

2174 Args: 

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

2176 

2177 Returns: 

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

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

2180 """ 

2181 graph = defaultdict(list) 

2182 rank_edges = {} 

2183 

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

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

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

2187 graph[u].append(v) 

2188 rank_edges[(u, v)] = rank 

2189 

2190 visited = set() 

2191 path = [] 

2192 node_indices = {} 

2193 cycle_path = [] 

2194 cycle_ranks = [] 

2195 

2196 stack = [] 

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

2198 if node not in visited: 

2199 stack.append((node, False)) 

2200 while stack: 

2201 current_node, is_processed = stack.pop() 

2202 

2203 if is_processed: 

2204 path.pop() 

2205 del node_indices[current_node] 

2206 continue 

2207 

2208 if current_node in node_indices: 

2209 cycle_start = node_indices[current_node] 

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

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

2212 u = path[i] 

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

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

2215 return cycle_path, cycle_ranks 

2216 

2217 if current_node in visited: 

2218 continue 

2219 

2220 visited.add(current_node) 

2221 node_indices[current_node] = len(path) 

2222 path.append(current_node) 

2223 

2224 stack.append((current_node, True)) 

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

2226 stack.append((neighbor, False)) 

2227 

2228 return None, None 

2229 

2230 

2231def output_cycle_results(cycle_path, cycle_ranks): 

2232 """ 

2233 Helper function to output cycle detection results. 

2234 

2235 Args: 

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

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

2238 

2239 Returns: 

2240 None: Outputs results to the console. 

2241 """ 

2242 if cycle_path: 

2243 logger.error("Cycle detected:") 

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

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

2246 logger.error("Involving ranks:") 

2247 for rank in cycle_ranks: 

2248 logger.error(rank) 

2249 else: 

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

2251 

2252 

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

2254 """ 

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

2256 

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

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

2259 and parses string values to extract meaningful components. 

2260 

2261 Args: 

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

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

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

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

2266 

2267 Returns: 

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

2269 

2270 Raises: 

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

2272 TypeError: Raised indirectly if data contains unexpected types. 

2273 """ 

2274 

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

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

2277 

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

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

2280 

2281 return {item.strip() for item in limited_groups} 

2282 

2283 if not isinstance(data, dict): 

2284 logger.error("Input must be a dictionary with string keys and lists of strings as values.") 

2285 return 

2286 

2287 key_to_values = {key: set(values) for key, values in data.items() if 

2288 isinstance(values, list) and all(isinstance(v, str) for v in values)} 

2289 

2290 for key, values in data.items(): 

2291 if not isinstance(values, list) or not all(isinstance(v, str) for v in values): 

2292 logger.error("Values for key '%s' must be a list of strings.", key) 

2293 continue 

2294 

2295 for value in values: 

2296 try: 

2297 elements = parse_elements(value) 

2298 except (ValueError, TypeError, AttributeError) as e: 

2299 logger.error("Unable to parse elements from value '%s' in key '%s'. Error: %s", value, key, e) 

2300 continue 

2301 

2302 # Check for missing keys if all_rank is True 

2303 if all_rank: 

2304 missing_keys = elements - key_to_values.keys() 

2305 if missing_keys: 

2306 logger.error("The following keys are missing for value '%s': %s", value, missing_keys) 

2307 continue 

2308 

2309 # Check if the value is present in the referenced keys 

2310 for element in elements & key_to_values.keys() if not all_rank else elements: 

2311 if value not in key_to_values[element]: 

2312 logger.error("Key '%s' is missing the value '%s'.", element, value) 

2313 

2314 

2315def generate_operations(order_list: dict[int, list[MetaStep]], 

2316 chunk_num: int, 

2317 com_type: str = 'loop') -> dict[str, list[str]]: 

2318 """ 

2319 Generate formatted operations dictionary from pipeline execution order. 

2320 

2321 Args: 

2322 order_list (dict): Dictionary where keys are rank IDs and values are MetaStep execution sequences 

2323 chunk_num (int): Number of chunks (virtual pipeline stages) 

2324 com_type (str): Stage-to-rank mapping type ('loop' for cyclic, 'v' for V-shaped) 

2325 

2326 Returns: 

2327 Dictionary where keys are rank IDs (as strings) and values are lists of formatted operation strings 

2328 """ 

2329 

2330 def stage_to_rank(stage_index, style, stage_num, real_stage_num): 

2331 """Map stage index to rank""" 

2332 if style == 'loop': 

2333 return stage_index % real_stage_num 

2334 if style == 'v': 

2335 if stage_index < real_stage_num: 

2336 return stage_index 

2337 return stage_num - 1 - stage_index 

2338 raise ValueError("Invalid style") 

2339 

2340 def find_send_target(stage_idx, op_type): 

2341 """Find target stage for SEND operation""" 

2342 if op_type == MetaStepType.FWD_SEND: 

2343 return forward_comm.get(stage_idx) 

2344 return backward_comm.get(stage_idx) 

2345 

2346 def find_recv_source(stage_idx, op_type): 

2347 """Find source stage for RECV operation""" 

2348 if op_type == MetaStepType.FWD_RECV: 

2349 # Reverse lookup in forward_comm 

2350 for src, dst in forward_comm.items(): 

2351 if dst == stage_idx: 

2352 return src 

2353 else: 

2354 # Reverse lookup in backward_comm 

2355 for src, dst in backward_comm.items(): 

2356 if dst == stage_idx: 

2357 return src 

2358 return None 

2359 

2360 real_stage = len(order_list) 

2361 total_stages = real_stage * chunk_num 

2362 

2363 # Build communication rules 

2364 forward_comm = {} 

2365 backward_comm = {} 

2366 

2367 for i in range(total_stages): 

2368 if i + 1 < total_stages: 

2369 forward_comm[i] = i + 1 

2370 if i - 1 >= 0: 

2371 backward_comm[i] = i - 1 

2372 

2373 formatted_operations = defaultdict(list) 

2374 

2375 for rank, steps in order_list.items(): 

2376 operation_counter = defaultdict(int) 

2377 

2378 for step in steps: 

2379 if step.type in [MetaStepType.FWD_SEND, MetaStepType.BWD_SEND]: 

2380 target_stage = find_send_target(step.stage_index, step.type) 

2381 if target_stage is not None: 

2382 target_rank = stage_to_rank(target_stage, com_type, total_stages, real_stage) 

2383 comm_pair = (rank, target_rank, step.micro_index) 

2384 operation_counter[comm_pair] += 1 

2385 count = operation_counter[comm_pair] 

2386 formatted_op = f"Send_Receive_({rank})->({target_rank})_micro{step.micro_index}_{count}th" 

2387 formatted_operations[str(rank)].append(formatted_op) 

2388 

2389 elif step.type in [MetaStepType.FWD_RECV, MetaStepType.BWD_RECV]: 

2390 source_stage = find_recv_source(step.stage_index, step.type) 

2391 if source_stage is not None: 

2392 source_rank = stage_to_rank(source_stage, com_type, total_stages, real_stage) 

2393 comm_pair = (source_rank, rank, step.micro_index) 

2394 operation_counter[comm_pair] += 1 

2395 count = operation_counter[comm_pair] 

2396 formatted_op = f"Send_Receive_({source_rank})->({rank})_micro{step.micro_index}_{count}th" 

2397 formatted_operations[str(rank)].append(formatted_op) 

2398 

2399 # Convert defaultdict to dict 

2400 return dict(formatted_operations) 

2401 

2402 

2403def validate_pipeline_execution(order_list: dict[int, list[MetaStep]], 

2404 chunk_num: int, 

2405 com_type: str = 'loop') -> dict[str, any]: 

2406 """ 

2407 Comprehensive validation function for pipeline parallel execution order. 

2408 

2409 This function validates the execution order of pipeline parallelism by: 

2410 1. Checking SEND/RECV communication pair matching 

2411 2. Detecting duplicate operations 

2412 3. Detecting cycles in communication graphs 

2413 4. Verifying computation-SEND matching 

2414 

2415 Args: 

2416 order_list: Dictionary where keys are rank IDs and values are MetaStep execution sequences 

2417 chunk_num: Number of chunks (virtual pipeline stages) 

2418 com_type: Stage-to-rank mapping type ('loop' for cyclic, 'v' for V-shaped) 

2419 

2420 Returns: 

2421 Dictionary containing validation results with the following keys: 

2422 - validation: Communication pair validation results 

2423 - cycle_detection: Cycle detection results 

2424 - computation_send_matching: Computation-SEND matching validation results 

2425 - has_errors: Boolean indicating if any errors were found 

2426 - error_messages: List of all error messages found 

2427 - formatted_operations: Generated formatted operations 

2428 """ 

2429 

2430 # Generate operations 

2431 formatted_operations = generate_operations(order_list, chunk_num, com_type) 

2432 

2433 parse_and_validate(formatted_operations, True) 

2434 

2435 # Detect cycles 

2436 cycle_path, cycle_ranks = detect_cycle_in_graph(formatted_operations) 

2437 

2438 # Output results 

2439 output_cycle_results(cycle_path, cycle_ranks) 

2440 

2441 result = { 

2442 'formatted_operations': formatted_operations, 

2443 'cycle_path': cycle_path, 

2444 'cycle_ranks': cycle_ranks, 

2445 'has_cycle': bool(cycle_path) 

2446 } 

2447 return result 

2448 

2449 

2450_COMPUTE_META_STEP_TYPES = frozenset({ 

2451 MetaStepType.FWD, 

2452 MetaStepType.BWD, 

2453 MetaStepType.BWD_INPUT, 

2454 MetaStepType.BWD_WEIGHT, 

2455}) 

2456 

2457 

2458def _next_active_stage_indices(actions, start_index, max_active_stages, managed_stage_indices): 

2459 """Find the next distinct managed stages that will execute compute work. 

2460 

2461 Send/recv and previously injected FSDP control steps are skipped so that the 

2462 lookahead window only counts real compute, otherwise communication-only 

2463 actions would consume the budget and shrink the effective prefetch depth. 

2464 """ 

2465 stage_indices = [] 

2466 seen = set() 

2467 for action in actions[start_index:]: 

2468 for leaf_step in iter_leaf_meta_steps(action): 

2469 if leaf_step.type not in _COMPUTE_META_STEP_TYPES: 

2470 continue 

2471 if leaf_step.stage_index not in managed_stage_indices or leaf_step.stage_index in seen: 

2472 continue 

2473 seen.add(leaf_step.stage_index) 

2474 stage_indices.append(leaf_step.stage_index) 

2475 if len(stage_indices) == max_active_stages: 

2476 return stage_indices 

2477 return stage_indices 

2478 

2479 

2480def add_fsdp_unshard_reshard(actions, managed_stage_indices, max_active_stages=3): 

2481 """Insert FSDP unshard/reshard actions for locally managed stages.""" 

2482 if not managed_stage_indices: 

2483 return actions 

2484 

2485 fsdp_actions = [] 

2486 active_stages = [] 

2487 for index, action in enumerate(actions): 

2488 next_stage_indices = _next_active_stage_indices( 

2489 actions, index, max_active_stages, managed_stage_indices 

2490 ) 

2491 evicted_stages = [stage_index for stage_index in active_stages if stage_index not in next_stage_indices] 

2492 fetched_stages = [stage_index for stage_index in next_stage_indices if stage_index not in active_stages] 

2493 for stage_index in evicted_stages: 

2494 fsdp_actions.append(MetaStep(None, MetaStepType.FSDP_RESHARD, stage_index)) 

2495 active_stages.remove(stage_index) 

2496 for stage_index in fetched_stages: 

2497 fsdp_actions.append(MetaStep(None, MetaStepType.FSDP_UNSHARD, stage_index)) 

2498 active_stages.append(stage_index) 

2499 fsdp_actions.append(action) 

2500 

2501 while active_stages: 

2502 fsdp_actions.append(MetaStep(None, MetaStepType.FSDP_RESHARD, active_stages.pop(0))) 

2503 return fsdp_actions 

2504 

2505 

2506def add_fsdp_reduce_grad(actions, managed_stage_indices, micro_batch_num): 

2507 """Insert FSDP reduce-grad actions after the last backward-like action of each stage.""" 

2508 if not managed_stage_indices: 

2509 return actions 

2510 

2511 fsdp_actions = [] 

2512 for action in actions: 

2513 fsdp_actions.append(action) 

2514 reduced_stage_indices = [] 

2515 for leaf_step in iter_leaf_meta_steps(action): 

2516 if leaf_step.stage_index not in managed_stage_indices: 

2517 continue 

2518 if leaf_step.type not in (MetaStepType.BWD, MetaStepType.BWD_WEIGHT): 

2519 continue 

2520 if leaf_step.micro_index != micro_batch_num - 1: 

2521 continue 

2522 if leaf_step.stage_index not in reduced_stage_indices: 

2523 reduced_stage_indices.append(leaf_step.stage_index) 

2524 for stage_index in reduced_stage_indices: 

2525 fsdp_actions.append(MetaStep(None, MetaStepType.FSDP_REDUCE_GRAD, stage_index)) 

2526 return fsdp_actions