Unify expert parallel token dispatcher API - #3970
Conversation
| def wire_meshes( | ||
| self, | ||
| *, | ||
| ep_mesh: DeviceMesh | None, | ||
| tp_mesh: DeviceMesh | None, | ||
| ) -> None: | ||
| ... |
There was a problem hiding this comment.
Let's try to deprecate this, maybe blocked by deprecate non-spmd_types (default / full_dtensor) backend. If so, can wait for @pianpwk
There was a problem hiding this comment.
@IvanKobzarev to review code change in this file
There was a problem hiding this comment.
@IvanKobzarev if there's no review, we'll proceed with the change
| top_k=dispatcher.top_k, | ||
| non_blocking_capacity_factor=dispatcher.non_blocking_capacity_factor, | ||
| pad_multiple=pad_multiple, | ||
| hidden_dim=dispatcher.hidden_dim, |
There was a problem hiding this comment.
Including all fields when constructing a new HybridEPTokenDispatcher.Config, copy all the fields over
There was a problem hiding this comment.
@IvanKobzarev if there's no review, we'll proceed with the change
| dispatcher_cfgs.append(token_dispatcher_cfg) | ||
|
|
||
| required_num_max_tokens_per_rank = None | ||
| training = getattr(config, "training", None) |
There was a problem hiding this comment.
No, both pretraining and inference caller will send training config. This checking is for safty guard purpose, and following the same pattern in maybe_update_minimal_async_ep_config
5f79ec5 to
db61e92
Compare
…#4089) ## Summary Enable MinimalAsyncEP to compose with context parallelism, tensor parallelism, and pipeline parallelism. This PR does not add backend-specific CP, TP, or PP dispatch algorithms. The required behavior comes from the common MoE and mesh contracts: - #3996 makes `RoutedExperts` operate on a local sequence shard and return that local layout. - #4080 physically pads the sequence before routing, so every rank dispatches the same number of materialized token rows. - #3970 centralizes lifetime communication-buffer capacity inference for persistent EP backends. - The sparse mesh places PP outside EP, so every pipeline stage owns an independent EP process group. MinimalAsyncEP therefore continues to dispatch the local `x_TD` rows over its EP group and returns a combined tensor with the same local token shape. The common MoE sharding path handles CP and TP, while the pipeline runtime invokes the stage-local MinimalAsyncEP instances. ## Buffer capacity MinimalAsyncEP now uses the shared lifetime capacity derivation: ```python num_token_shards = context_parallel_degree * tensor_parallel_degree if seq_len % num_token_shards != 0: raise ValueError(...) num_max_tokens_per_rank = local_batch_size * seq_len // num_token_shards ``` CP and TP reduce the local token capacity because they shard the sequence dimension before MoE. The shared configuration requires `seq_len` to divide evenly across `CP * TP`; users must pad the configured sequence length when it does not. PP does not enter this calculation because it partitions model layers, not the token axis. This replaces the backend-local `local_batch_size * seq_len` calculation, which overallocated once CP or TP was enabled. The shared setup also fills `hidden_dim`, preserves a user-specified capacity when it is large enough, and rejects an undersized capacity. MinimalAsyncEP keeps only its backend-specific dtype setup. ## Changes - Remove the MinimalAsyncEP configuration restrictions for CP, TP, and PP. - Include MinimalAsyncEP in shared EP buffer-capacity configuration. - Add an H100 integration configuration for `DP-shard=2, CP=2, TP=2, EP=8`. - Keep the test on `spmd_types` until the H100 suite migrates from the default DTensor backend. ## Testing ### Tensor parallelism Four H100s, 10 training steps: ```text DP-shard=2, TP=2, EP=4 spmd_backend=spmd_types compile disabled full activation checkpointing ``` Loss decreased from `8.06603` to `3.86079`; final `grad_norm` was `1.6618`. ### Context parallelism Four H100s, 10 training steps: ```text DP-shard=2, CP=2, EP=4 spmd_backend=spmd_types compile disabled full activation checkpointing ``` Loss decreased from `8.00828` to `3.87139`; final `grad_norm` was `1.5916`. ### Pipeline parallelism Four H100s with PyTorch `2.14.0.dev20260811+cu130`, two optimizer steps: ```text DP-shard=2, PP=2, EP=2 1F1B with 8 microbatches spmd_backend=spmd_types compile disabled full activation checkpointing ``` The run completed MinimalAsyncEP dispatch/combine, pipeline forward/backward, and PP-wide gradient-norm reduction. Loss decreased from `8.32359` to `6.38615`; `grad_norm` changed from `3.6094` to `4.4162`. ### Numerical comparison against AllToAll The standard AllToAll dispatcher and MinimalAsyncEP were run for 10 steps with the same `DP-shard=2, PP=2, EP=2` topology, fused expert kernels, data, seed 42, and deterministic mode. Full-precision TensorBoard metrics show a maximum relative loss difference of `4.55e-4` and a maximum relative grad-norm difference of `2.60e-3`. MinimalAsyncEP is not expected to be bitwise identical to AllToAll: AllToAll rounds each score-weighted expert row to BF16 and then performs deterministic scatter-add in expert order, while MinimalAsyncEP accumulates the top-k rows in FP32 slot order and rounds once. Two diagnostics verify that this difference is numerical rather than token dropping: - A temporary assertion reduced the active receive-row count across every stage-local EP group and required it to equal `EP_size * local_tokens * top_k`. It passed every forward and activation-recompute dispatch across all 10 steps. - Replacing only MinimalAsyncEP's top-k combine arithmetic with the AllToAll arithmetic produced the exact same step-1 loss: `8.1351776123046875` for both backends. Native MinimalAsyncEP produced `8.1351785659790039`. The exact eight-GPU `DP-shard=2, CP=2, TP=2, EP=8` CI configuration was not run locally because the host has seven available H100s. CP and TP were validated independently as listed above. ## Limitations - MinimalAsyncEP requires the `spmd_types` backend. The default DTensor path currently hits the same `Shard(1) -> Partial(sum)` limitation tracked by #4110. - MinimalAsyncEP requires full recomputation: full activation checkpointing for eager training or full memory policy for GraphTrainer.
Summary
This PR unifies the token-dispatch API across EP backends and separates:
Persistent communication buffers are initialized before execution and no longer grow inside
dispatch().API design
Dispatcher configuration carries the lifetime maximum:
Dispatch receives the current logical per-rank capacity:
The intended relationship is:
x_TD.shape[0] is the materialized token count. The dispatch argument includes sequence-dimension
padding and may therefore be larger.
During mesh wiring, backends with persistent communication storage allocate it using
num_max_tokens_per_rank. Dispatch then creates or uses a current logical layout without
reallocating or growing that storage.
This provides stable buffer addresses for CUDA graph capture while allowing different eager calls
or graph buckets to use different current token capacities.
Backend behavior
DeepEP: preallocates one ElasticBuffer. The current capacity is passed as DeepEP's per-rank
layout stride.
HybridEP: preallocates its communication buffer. Non-blocking dispatch uses the current capacity
to size the fused-permute output.
MinimalAsyncEP: preallocates symmetric-memory storage. It currently derives dispatch shapes from
x_TD.shape[0] because TP/SP is unsupported.
AllToAll/TorchAO: have no dispatcher-owned persistent communication buffer and continue using
exact collective tensor sizes.
Capacity derivation
Pretraining
The maximum is derived from the configured training shape:
Ceiling division prevents underallocation when the sequence length is not divisible by the token-
sharding degree.
vLLM inference
The inference bound comes from vLLM's per-step scheduler limit:
vLLM filters its finalized CUDA graph capture sizes so they do not exceed max_num_batched_tokens.
Decode-only workers should configure max_num_batched_tokens from their maximum decode workload
rather than retaining a large prefill-oriented default.
If the user explicitly configures num_max_tokens_per_rank, setup validates that it is positive and
large enough for the derived runtime requirement.
Correctness
The change only refactors dispatcher APIs and communication-buffer lifetime. It does not change MoE
routing or expert computation.
With TP enabled, the baseline and this PR were run for 10 training steps using:
The runs produced bitwise-identical loss and grad_norm at every compared step.