Skip to content

Unify expert parallel token dispatcher API - #3970

Merged
wwwjn merged 28 commits into
mainfrom
unify-ep-token-dispatcher
Aug 11, 2026
Merged

Unify expert parallel token dispatcher API#3970
wwwjn merged 28 commits into
mainfrom
unify-ep-token-dispatcher

Conversation

@wwwjn

@wwwjn wwwjn commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR unifies the token-dispatch API across EP backends and separates:

  1. The maximum token capacity used to allocate persistent communication storage.
  2. The current token capacity used by one dispatch operation.

Persistent communication buffers are initialized before execution and no longer grow inside
dispatch().

API design

Dispatcher configuration carries the lifetime maximum:

num_max_tokens_per_rank: int

Dispatch receives the current logical per-rank capacity:

  dispatch(
      x_TD,
      topk_scores_TK,
      topk_expert_ids_TK,
      num_local_tokens_per_expert_E,
      *,
      num_tokens_per_rank: int,
  )

The intended relationship is:

  x_TD.shape[0]
      <= num_tokens_per_rank
      <= num_max_tokens_per_rank

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:

  num_token_shards = (
      context_parallel_degree * tensor_parallel_degree
  )

  num_max_tokens_per_rank = (
      local_batch_size
      * ceil_div(seq_len, num_token_shards)
  )

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:

  num_max_tokens_per_rank = ceil_div(
      max_num_batched_tokens,
      context_parallel_degree * tensor_parallel_degree,
  )

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:

  --debug.seed=42
  --debug.deterministic

The runs produced bitwise-identical loss and grad_norm at every compared step.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Jul 22, 2026
@wwwjn
wwwjn marked this pull request as draft July 22, 2026 16:29
@wwwjn wwwjn changed the title Unify expert parallel token dispatcher API [WIP] Unify expert parallel token dispatcher API Jul 22, 2026
Comment thread torchtitan/models/common/token_dispatcher.py Outdated
@wwwjn
wwwjn marked this pull request as ready for review July 22, 2026 18:54
@wwwjn wwwjn changed the title [WIP] Unify expert parallel token dispatcher API Unify expert parallel token dispatcher API Jul 22, 2026
Comment thread torchtitan/models/common/token_dispatcher.py Outdated
Comment on lines +35 to +41
def wire_meshes(
self,
*,
ep_mesh: DeviceMesh | None,
tp_mesh: DeviceMesh | None,
) -> None:
...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's try to deprecate this, maybe blocked by deprecate non-spmd_types (default / full_dtensor) backend. If so, can wait for @pianpwk

Comment thread torchtitan/models/common/token_dispatcher.py Outdated
Comment thread torchtitan/models/common/decoder.py Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@IvanKobzarev to review code change in this file

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@IvanKobzarev if there's no review, we'll proceed with the change

@tianyu-l
tianyu-l requested a review from acisseJZhong July 22, 2026 22:53
Comment thread torchtitan/models/common/token_dispatcher.py Outdated
Comment thread torchtitan/models/common/token_dispatcher.py Outdated
Comment thread torchtitan/models/common/token_dispatcher.py
Comment thread torchtitan/models/common/token_dispatcher.py Outdated
Comment thread torchtitan/models/common/token_dispatcher.py Outdated
Comment thread torchtitan/models/common/token_dispatcher.py Outdated
Comment thread torchtitan/distributed/deepep/deepep.py
@wwwjn wwwjn changed the title Unify expert parallel token dispatcher API [WIP] Unify expert parallel token dispatcher API Jul 29, 2026
@wwwjn
wwwjn marked this pull request as draft July 29, 2026 22:15
Comment thread torchtitan/distributed/deepep/hybridep.py Outdated
top_k=dispatcher.top_k,
non_blocking_capacity_factor=dispatcher.non_blocking_capacity_factor,
pad_multiple=pad_multiple,
hidden_dim=dispatcher.hidden_dim,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Including all fields when constructing a new HybridEPTokenDispatcher.Config, copy all the fields over

Comment thread torchtitan/distributed/deepep/hybridep.py Outdated
@wwwjn
wwwjn marked this pull request as ready for review July 30, 2026 21:38
@wwwjn wwwjn changed the title [WIP] Unify expert parallel token dispatcher API Unify expert parallel token dispatcher API Jul 30, 2026
@wwwjn wwwjn added the ciflow/h100.8 Trigger H100.8 CI label Jul 30, 2026
Comment thread torchtitan/distributed/deepep/deepep.py Outdated
Comment thread torchtitan/distributed/deepep/hybridep.py Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@IvanKobzarev if there's no review, we'll proceed with the change

Comment thread torchtitan/models/common/moe.py Outdated
Comment thread torchtitan/models/common/token_dispatcher.py Outdated
Comment thread torchtitan/models/common/token_dispatcher.py Outdated
dispatcher_cfgs.append(token_dispatcher_cfg)

required_num_max_tokens_per_rank = None
training = getattr(config, "training", None)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when would it be None?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread torchtitan/experiments/rl/models/vllm_wrapper.py Outdated
Comment thread torchtitan/experiments/rl/models/vllm_wrapper.py
Comment thread torchtitan/experiments/rl/models/vllm_wrapper.py Outdated
@wwwjn
wwwjn force-pushed the unify-ep-token-dispatcher branch from 5f79ec5 to db61e92 Compare August 11, 2026 14:36
@wwwjn
wwwjn merged commit 4a93ee4 into main Aug 11, 2026
24 of 25 checks passed
wwwjn added a commit that referenced this pull request Aug 11, 2026
…#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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/h100.8 Trigger H100.8 CI ciflow/rl ciflow/8gpu CLA Signed This label is managed by the Meta Open Source bot.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants