Parallelism mechanics
This page is the runtime side of parallelism: when a batch hits ASTRA-Sim, what collectives fire, where, and how multi-instance DP groups synchronize. The cluster-config angle (which fields turn each of these on) is on Examples → Cluster config explained.
What the simulator can model
| Style | What's parallelized | Collective | Where it fires |
|---|---|---|---|
| TP (tensor) | Linear weights split along head dim | ALLREDUCE | After o_proj and down_proj |
| PP (pipeline) | Decoder layers split across GPU groups | (point-to-point in inflight queue) | At stage boundaries |
| EP (expert) | MoE experts split across ranks | ALLTOALL | Around the MoE block |
| DP+EP | EP across multiple instances | ALLTOALL | Same, but across instance boundaries with wave-sync |
TP and EP can share the same GPUs. DP requires a dp_group
identifier on the cluster config — for a dense model that is plain data
parallelism, and for MoE it also spreads experts across the group.
TP, ALLREDUCE on every dense layer
When tp_size > 1, the trace generator attaches an ALLREDUCE
COMM_COLL_NODE after each TP-aware dense linear:
o_proj(attention output projection)down_proj(MLP output projection)
These are the two layers where each TP rank holds a different head slice of the output and needs to sum across ranks.
The comm_size on each ALLREDUCE is the full output tensor size
(not per-rank, ASTRA-Sim divides internally based on
nodes_in_ring).
qkv_proj, gate_up_proj, etc. don't need ALLREDUCE because they
split the input along the head dim, those layers' output is
already correctly sharded for the next layer. TP's collective cost
is bound by o_proj + down_proj, two ALLREDUCEs per decoder block.
PP, pipeline stages and inflight
When pp_size > 1, the scheduler keeps an inflight list capped at
pp_size entries. When the pipeline is full, schedule() returns
None and waits for ASTRA-Sim to drain a stage, the same
back-pressure pattern as Megatron-style 1F1B.
The trace header is stamped with model_parallel_NPU_group: {pp_size}
plus pp_stage_boundaries, the layer-row indices at which each stage
after the first begins. trace_generator.py computes them from the
transformer-block starts it just wrote, using the same partitioning
rule as vLLM's get_pp_indices: blocks split evenly, with any
remainder going to the stages before the last one, since the last
stage also carries final_layernorm / lm_head / sampler. Chakra's
llm_converter.py reads the boundaries and emits one .et per NPU. At
each stage boundary it pairs a COMM_SEND_NODE on the upstream NPU with
a matching COMM_RECV_NODE on the downstream one, sized by the boundary
activation tensor.
Stages are cut only on transformer-block boundaries. That is the
one place where the upstream layer's output_size and the downstream
layer's input_size are the same tensor — the hidden state, since a
block runs layernorm → … → down_proj/moe. Inside a block they
differ (qkv_proj emits Q+K+V, rotary_emb declares only Q+K), and
ASTRA-Sim's analytical backend keys its send/recv callback tracker on
(tag, src, dst, chunk_size, chunk_id) — so a size disagreement never
matches and the downstream NPU waits forever instead of raising. Cutting
the raw line count evenly used to land boundaries mid-block, which is
what made only some pp_size values hang.
--enable-sub-batch-interleaving is rejected with pp_size > 1: an
interleaved trace leaves both sub-batches mid-block at every group edge,
so a stage has no single hidden state to hand on.
Inter-stage P2P latency (link bandwidth, hop count, contention) is
therefore part of the reported iteration time, and pipeline overlap
between in-flight batches falls out from each NPU's independent .et
schedule.
EP, ALLTOALL around the MoE block
For MoE models, trace_generator wraps the MoE block with two
ALLTOALL collectives:
... → MoE dispatch ALLTOALL → expert compute → MoE combine ALLTOALL → ...
The dispatch ALLTOALL routes each token to its assigned expert's rank. The combine ALLTOALL gathers expert outputs back to the originating ranks. Both are scoped to the EP dimension.
Each EP rank gets a per-rank latency from
profiler/perf/<hw>/<model>/<variant>/tp1/moe.csv keyed on its
local token count (after dispatch) and the activated experts
per token. Ranks execute in parallel and synchronize at the ALLTOALL
barrier, slower ranks gate the others.
Token routing decisions come from gate_function.py. See
MoE expert routing for the policies.
DP+EP, wave synchronization
This is where the simulator gets clever. When two or more instances
share a dp_group, they form a single coordinated wave. Two
synchronization mechanisms work together:
1. Python-side dp_pending barrier
In __main__.py, dp_pending holds one queue per DP-group member
of batches waiting for their wave. Trace generation is deferred until
every member has at least one batch queued; the wave then takes the
oldest from each, so a wave always pairs the members' j-th batches —
the same pairing production serving gets, where DP rank A's j-th
forward joins the same collective as rank B's j-th. The queue matters
at pp_size > 1, where a member can have up to pp_size batches
outstanding at once. When a wave assembles:
- The simulator takes
max_total_lenacross the group and pads every member's batch up to it, matching CUDA-graph DP padding in production serving. - The MoE collective size is anchored to that same
max_total_len— notmax x dp_group_size. That calibrates the AllGather/ReduceScatter bandwidth model against the samelink_bwthat already matches AllReduce. - All members generate their traces with the same
comm_size, even if their per-instancetotal_lendiffers.
If one DP member has no pending requests, the scheduler synthesizes a dummy batch (1 decode token) so the wave still runs. When all of one member's real requests have finished but the others haven't, the dummy batches keep flowing until the whole group is done.
A wave's graphs cannot be emitted at schedule time — the padded
max_total_len is not known until the barrier assembles — so each
member's graph is handed to the NPU that opened its round on that NPU's
next poll, ahead of anything the scheduler would otherwise start. That
keeps each NPU running its batches in the order they were opened, which
is what the completion bookkeeping assumes.
2. ASTRA-Sim ALLTOALL barrier
All DP-group instances' .et files share the same workload folder
(dp_<group>_batch<bid>/llm.et) and use matching stream IDs on
the ALLTOALL collectives. ASTRA-Sim's runtime sees the matching IDs
and blocks until both NPUs reach the collective, naturally
implementing the wave-sync at the network layer.
So both halves of the sync, Python deferral on submission, ASTRA-Sim blocking on the collective, together produce a deterministic wave-synchronous schedule.
Multi-dimensional ASTRA-Sim topology and involved_dim
config_builder generates a multi-dimensional ASTRA-Sim network when DP
groups are present, innermost dimension first:
npus_count: [tp_size, dp_group_size], or
[tp_size, pp_size, dp_group_size] when pp_size > 1. This mirrors
vLLM's rank layout, all_ranks.reshape(-1, dp, pp, pcp, tp); the
pp_size dimension is omitted when it is 1, so DP+TP configs keep their
2-D topology. Collectives are scoped per dimension via the
involved_dim BoolList on each COMM_COLL_NODE:
- TP-ALLREDUCE: the TP dim only —
[True, False], or[True, False, False]with PP. - EP: the DP dim, plus the TP dim when EP spans past one instance's
GPUs —
[False, True]/[True, True], or[False, False, True]/[True, False, True]with PP. The PP dim is never involved: vLLM's EP group isall_ranks.transpose(1, 2).reshape(-1, dp*pcp*tp), whose transpose pins the pipeline stage, so experts are sharded across the DP x TP ranks of one stage.
The involved_dim is encoded in the trace's comm_type field with
a :dim0,dim1 suffix:
ALLREDUCE:1,0 # TP only
ALLTOALL:0,1 # EP across DP only
The Chakra converter parses this via _parse_comm_type and writes
the BoolList into the .et file. ASTRA-Sim's Workload::issue_comm
reads it and dispatches the collective only on the involved dims.
The system.json collective implementations need one entry per
topology dim, config_builder generates this automatically:
"all-to-all-implementation": ["ring", "ring"] for 2D.
Communication sizes (ASTRA-Sim semantics)
Every comm_size in the trace is the total data size, not
per-NPU. ASTRA-Sim divides internally by the number of nodes in the
ring (msg_size = data_size / nodes_in_ring).
So:
- ALLREDUCE on
o_proj: pass the full output tensor size (total_len * hidden_size * fp_size). - ALLTOALL for MoE: pass the full activation tensor size
(
total_len * hidden_size * fp_size).
If you see surprisingly fast collectives in your trace logs, check that you're not accidentally passing per-rank sizes, that's a common mistake when extending the trace generator.
When to use which
A rough decision tree (the configuration angle is on Examples → Cluster config explained):
- Single GPU fits the model: TP=1. Done.
- Need more GPUs for memory: start with TP. ALLREDUCE cost grows
with
tp_size, so going past 4-8 is rarely worth it. - Multiple replicas for throughput: add
num_instances(nodp_group). Independent instances behind a router. - MoE model, single instance: add
ep_size = tp_size. Same GPUs, EP-ALLTOALL replaces TP-ALLREDUCE on the MoE block. - MoE, want to scale experts past one instance's GPUs: DP+EP
with
dp_groupset. EP spans instances via wave-sync. - Dense model, want data-parallel replicas:
dp_groupset and noep_size. The replicas are wave-synchronized but share no experts.
Gotchas
ep_size > tp_sizerequiresdp_group. Otherwise the cluster config builder rejects the spec. EP needs the DP dimension of the topology to scale beyond a single instance's GPU count.- Dummy batches are real ASTRA-Sim work. A DP group with one idle instance still pays the ALLTOALL cost on the dummy batch. This is what production looks like, wave-sync is wave-sync.
comm_sizeis synchronized to the max. Even if one DP member's batch is much smaller, the ALLTOALL message size matches the largest member's. This is correct (matches production padding) but worth knowing.- PP models inter-stage forwarding via send/recv, not via
micro-batch splitting inside an iteration. Activation shipment
between stages goes through ASTRA-Sim send/recv (so link bandwidth
and contention show up in the result), but a single iteration is
not chunked into multiple micro-batches — the overlap benefit
comes from running up to
pp_sizeconsecutive iterations simultaneously. There's also no knob to pick a pipeline schedule (1F1B, interleaved, etc.).
What's next
- MoE expert routing: how tokens get distributed across EP ranks before the dispatch ALLTOALL.
- Examples → DP+EP MoE - a worked-out config that exercises this whole machinery.