MPipe Transpose — Design
1. Motivation
In standard (interleaved) 1F1B pipeline parallelism, the warmup phase leaves
downstream ranks idle while stage 0 sequentially computes the forward of every
micro-batch. MPipe Transpose recovers part of that bubble by transposing the
forward of a model's first T layers — the preprocess block.
Notation:
PP— number of pipeline stages (physical ranks,real_stage_num).MB— number of micro-batches (micro_batch_num).L(s)— number of layers assigned to stages.T— number of transposed (preprocess) layers, taken from stage 0's first chunk,T < (#layers in stage 0's first chunk).NT = min(PP, MB)— number of transposed micro-batches.
Instead of stage 0 running the preprocess forward serially for all micro-batches,
the preprocess parameters are broadcast to every rank. For the first NT
micro-batches, rank i computes the preprocess forward of micro-batch i in
parallel during what would otherwise be its warmup idle time, then ships the
result back to stage 0. The rest of the schedule is ordinary 1F1B.
Training-step flow
- Stage 0 broadcasts the parameters of the
Tpreprocess layers to every rank. - Every rank reads its own micro-batch from the dataset (dataloader replicated per rank, each reading different data).
- Micro-batch
i(fori < NT) has its preprocess forward computed on ranki. - Each transposed rank
i > 0sends to stage 0:- (a) the preprocess output — so stage 0 can run its body forward
(layers
T..L(0)), and - (b) the preprocess input — so the preprocess backward can be recomputed centrally on stage 0.
- (a) the preprocess output — so stage 0 can run its body forward
(layers
- Warmup → 6. Steady → 7. Cooldown proceed as ordinary interleaved 1F1B on the body model.
2. Key design decisions
| ID | Decision |
|---|---|
| D1 | Backward = recompute on stage 0. A live PyTorch autograd graph cannot cross ranks, so "backward on stage 0" requires stage 0 to recompute preprocess(input_i) from the shipped input, then backprop the gradient that the body backward deposited on the detached input buffer. MPIPE_GRAPH_SEND/RECV therefore ships the input (not a literal graph). Pairs naturally with a recompute-wrapped preprocess block. |
| D2 | Preprocess is a separate replicated module, scheduled via dedicated MPIPE_* steps. Stage 0's PipelineStage wraps only body0 (chunk 0 minus the first T layers); the inherited interleaved-1F1B logic runs unchanged on the body model. |
| D3 | Option A — the caller passes preprocess_module on every rank (standalone, identical architecture). On rank 0 it holds the trained parameters; on other ranks it is a structural copy overwritten each step by the broadcast. Required because ranks 1..PP-1 do not otherwise hold stage 0's layers. |
| D4 | Per-rank micro read — every rank reads its own micro-batch i; this is why the preprocess input must be shipped to stage 0 (MPIPE_GRAPH_SEND). |
| D5 | Torch only, using native Torch tensor, autograd, and distributed APIs. |
Micro-batch categories
Every preprocess forward is detached: its output reaches body0 as a plain
buffer, and the preprocess gradient comes from an explicit backward rather than
from body0's graph. Which rank owns which micro-batch depends on
pp_mpipe_transpose_overflow:
"full"(default) — round-robin: micromis owned by rankm % NT, so theMB > NToverflow is spread evenly and every micro is transposed somewhere."min"— onlyi ∈ [0, NT)are transposed; rank 0 absorbs the overflowi ∈ [NT, MB), running their preprocess inline (still detached). This trades a longer rank-0 phase for less inter-rank transport.
The preprocess backward is either the centralized MPIPE_TRANSPOSE_BWD on
stage 0 (recomputing from the shipped input), or, under
pp_mpipe_owner_backward, run by the owning rank after stage 0 ships
dL/dfeatures back — see _build_owner_backward_suffix.
3. New MetaStepType members
| Type | Where | Action |
|---|---|---|
MPIPE_PARAM_BROADCAST |
all ranks | broadcast preprocess params 0→all |
MPIPE_TRANSPOSE_FWD |
the owning rank; rank 0 inline for "min" overflow |
run preprocess forward, detached, retaining the input for the backward |
MPIPE_FWD_SEND / MPIPE_FWD_RECV |
i→0 |
ship preprocess output into stage 0's body input slot |
MPIPE_GRAPH_SEND / MPIPE_GRAPH_RECV |
i→0 |
ship preprocess input for recompute backward (deferrable) |
MPIPE_TRANSPOSE_BWD |
rank 0, micro<NT |
recompute preprocess forward from input, then autograd.backward |
Why a dedicated MPIPE_FWD_SEND rather than reusing FWD_SEND? The output
transport i→0 cannot reuse the existing pair: (1) the receiver is stage 0, the
first stage, whose forward_one_chunk reads args (not args_recv_info), and
the source rank varies per micro-batch while src_stage is fixed; (2) it is
forward-only — the transposed backward is recomputed on stage 0, so no
gradient is sent back to rank i, whereas exec_fwd_send_ops wires up a
backward grad-recv path. Keeping it separate from MPIPE_GRAPH_SEND is also what
makes the T=0 case expressible (see below).
T = 0 — transpose only the dataload
T = 0 is valid and degenerate: there is no preprocess block, so each rank
loads its micro-batch and ships the raw input to stage 0, moving data loading
off stage 0's critical path. The schedule keeps MPIPE_TRANSPOSE_FWD (= the
dataload) and MPIPE_FWD_SEND / MPIPE_FWD_RECV (ship the input), and omits
MPIPE_PARAM_BROADCAST, MPIPE_GRAPH_SEND / MPIPE_GRAPH_RECV, and
MPIPE_TRANSPOSE_BWD (nothing to broadcast, no params to backprop). Non-transposed
micro-batches are read locally by stage 0 as usual.
4. Schedule construction
ScheduleMPipeTranspose subclasses ScheduleInterleaved1F1B and reuses its
per-stage warmup/steady/cooldown construction and add_send_recv for the body.
construct_exec_order():
- Build the body order via
super().construct_exec_order()(body stages, with P2P comms inserted). - Patch stage 0's (rank 0) body order:
- before each
FWD(stage 0, micro ≥ NT): insert inlineMPIPE_TRANSPOSE_FWD(micro); - after each
BWD(stage 0, micro < NT): insertMPIPE_TRANSPOSE_BWD(micro).
- before each
- Prepend a transpose-phase prefix to every rank:
MPIPE_PARAM_BROADCAST;- if
rank < NT:MPIPE_TRANSPOSE_FWD(rank), and forrank > 0MPIPE_FWD_SEND(rank→0)+MPIPE_GRAPH_SEND(rank→0); - if
rank == 0:MPIPE_FWD_RECV(i)fori∈[1,NT), thenMPIPE_GRAPH_RECV(i).
Invariant: stripping all MPIPE_* steps from any rank's order recovers the plain
interleaved-1F1B body exactly — MPipe only layers steps around an unchanged
body schedule.
Worked examples
- PP=4, n_local=1, MB=4 (
NT=4): every rank computes one preprocess forward; ranks 1–3 send output+input to stage 0; stage 0 receives 3 outputs and 3 inputs and appends oneMPIPE_TRANSPOSE_BWDafter eachBWD(0, i). - PP=4, n_local=2, MB=8 (
NT=4): additionally inserts inlineMPIPE_TRANSPOSE_FWD(4..7)beforeFWD(0, 4..7)with automatic backward. - PP=4, n_local=1, MB=2 (
NT=2): only ranks 0,1 transposed; ranks 2,3 only join the broadcast.
5. Files
core/pipeline_parallel/scheduler.py— core scheduling and data-loading steps;core/pipeline_parallel/mpipe/schedule.py—ScheduleMPipeTranspose, including ordering, executor registration, and overflow / owner-backward layouts.core/pipeline_parallel/mpipe/executor.py—MPipeTransposeExecutorimplements native Torch broadcast, P2P transport, preprocess autograd, stage-0 recompute backward, and owner-backward gradient reduction.core/pipeline_parallel/_stage.pyand_microbatch.py— Torch stage autograd and micro-batch splitting._p2p.pyowns pipeline communicator initialization.core/pipeline_parallel/__init__.py+hyper_parallel/__init__.py— exportScheduleMPipeTranspose.- Per-rank dataload convention: every rank passes the batch; the owner of each micro-batch runs its transposed forward.
- Tests:
tests/ut/core/pipeline_parallel/test_mpipe_transpose.py(ordering),test_mpipe_transpose_exec.py(single-process recompute equivalence). Distributed correctness:tests/torch/pipeline_parallel/_test_mpipe_transpose.py(gloo on CPU, hccl on Ascend NPU), launched bytest_mpipe_transpose_dist.pyviaparallel_run/TorchCase.
Backward execution
Pipeline parallel supports PyTorch only. With owner-backward disabled, stage 0
recomputes trainable preprocess outputs and backpropagates their received feature
gradients. With owner-backward enabled, each owner retains its preprocess graph,
receives feature gradients, and runs native torch.autograd.backward; the tower's
local parameter gradients are then summed over the PP group. Gradient accumulation
reduces only the current run's contribution before adding the previous snapshot.
6. Performance
Measured on Ascend: with T = 0 (dataload-only) MPipe step time ≈ 1F1B, but
T > 0 is much slower. The gap is the inherent per-step cost, not the
transport plumbing:
- Dominant cost: the per-step parameter broadcast.
MPIPE_PARAM_BROADCASTre-broadcasts the preprocess weights to every PP rank each step (they change after each optimizer step). For an LLM the preprocess includesembed_tokens, which is param-heavy / compute-light — the worst thing to transpose — and the embedding broadcast is unavoidable if any layer is transposed (rankimust embed before running the layers). - Secondary: recompute — transposed micro-batches compute the preprocess
forward twice (rank
ifor the shipped output, stage 0 for the recompute backward). - Transport is negligible — confirmed by
T = 0 ≈ 1F1B(it still shipsinput_idsand runs the same transport).
MPipe Transpose only wins when bubble_saved > broadcast_cost + recompute_cost,
i.e. a large warmup bubble (high PP, low MB) and a param-light, compute-heavy
preprocess. A trainable LLM embedding/decoder block is the opposite (param-heavy,
broadcast every step), so MPipe is comm-bound and loses to 1F1B for text.
The frozen VL visual tower is the case where MPipe wins: it is compute-heavy (the image encode) but frozen, so there is no per-step broadcast and no recompute — only the image features are shipped. This is exactly the param-light-cost / compute-heavy regime MPipe is designed for, and why the VL integration transposes the visual tower rather than text layers.