427 lines
15 KiB
Markdown
427 lines
15 KiB
Markdown
# Mooncake EP & Mooncake Backend (PG)
|
|
|
|
## Overview
|
|
|
|
Mooncake provides two closely related components for fault-tolerant MoE
|
|
inference:
|
|
|
|
- **Mooncake Backend (PG)** is a `torch.distributed` ProcessGroup backend. It
|
|
registers the `mooncake` accelerator backend and the `mooncake-cpu` backend,
|
|
implements common collective and point-to-point APIs, tracks active ranks, and
|
|
exposes elastic recovery helpers.
|
|
- **Mooncake EP** is an expert-parallel dispatch/combine runtime for
|
|
latency-sensitive MoE inference. It follows the DeepEP low-latency programming
|
|
model while adding rank activeness awareness and Mooncake transport support.
|
|
|
|
The usual integration pattern is to initialize a Mooncake process group first,
|
|
then construct a Mooncake EP `Buffer` from that group. The process group is used
|
|
both for regular collectives and for exchanging EP bootstrap metadata.
|
|
|
|
For implementation details, see the [Mooncake Backend (PG) design guide](../design/mooncake-backend-pg.md)
|
|
and the [Mooncake EP design guide](../design/mooncake-ep.md).
|
|
|
|
## Installation and build notes
|
|
|
|
Mooncake EP and PG are included in CUDA-enabled Mooncake wheels. When building
|
|
from source, enable the EP/PG extensions with:
|
|
|
|
```bash
|
|
cmake .. -DWITH_EP=ON
|
|
```
|
|
|
|
The extensions are compiled against a specific PyTorch version. At import time,
|
|
`mooncake.pg` and `mooncake.ep` load version-suffixed extension modules that
|
|
match the active `torch.__version__`. If the current PyTorch version does not
|
|
match a built extension, import will fail with a message such as
|
|
`Mooncake PG was not built against torch==...`.
|
|
|
|
## Mooncake Backend (PG) quick start
|
|
|
|
### CUDA backend
|
|
|
|
```python
|
|
import os
|
|
|
|
import torch
|
|
import torch.distributed as dist
|
|
from mooncake import pg
|
|
|
|
|
|
rank = int(os.environ["RANK"])
|
|
world_size = int(os.environ["WORLD_SIZE"])
|
|
local_rank = int(os.environ.get("LOCAL_RANK", rank))
|
|
|
|
torch.cuda.set_device(local_rank)
|
|
device = torch.device("cuda", local_rank)
|
|
|
|
# Backend-level active-rank mask. Use int32 and place it on the backend device.
|
|
active_ranks = torch.ones(world_size, dtype=torch.int32, device=device)
|
|
|
|
dist.init_process_group(
|
|
backend="mooncake",
|
|
rank=rank,
|
|
world_size=world_size,
|
|
pg_options=pg.MooncakeBackendOptions(active_ranks),
|
|
)
|
|
|
|
x = torch.tensor([rank + 1], dtype=torch.int32, device=device)
|
|
dist.all_reduce(x, op=dist.ReduceOp.SUM)
|
|
print(f"rank={rank}, all_reduce={int(x.cpu())}")
|
|
```
|
|
|
|
Run it with the usual PyTorch launcher, for example:
|
|
|
|
```bash
|
|
torchrun --nproc-per-node=2 pg_quickstart.py
|
|
```
|
|
|
|
### CPU backend
|
|
|
|
Use `backend="mooncake-cpu"` and put `active_ranks` on CPU:
|
|
|
|
```python
|
|
active_ranks = torch.ones(world_size, dtype=torch.int32)
|
|
dist.init_process_group(
|
|
backend="mooncake-cpu",
|
|
rank=rank,
|
|
world_size=world_size,
|
|
pg_options=pg.MooncakeBackendOptions(active_ranks),
|
|
)
|
|
```
|
|
|
|
### Selecting network devices
|
|
|
|
To explicitly restrict Mooncake to a list of NIC / HCA devices, call
|
|
`pg.set_device_filter(...)` before `init_process_group()`:
|
|
|
|
```python
|
|
from mooncake import pg
|
|
|
|
pg.set_device_filter(["mlx5_1", "mlx5_2"])
|
|
```
|
|
|
|
For test and benchmark commands, the same setting is commonly passed through
|
|
`MOONCAKE_PGTEST_DEVICE_FILTERS=mlx5_1,mlx5_2`.
|
|
|
|
## Mooncake Backend (PG) API reference
|
|
|
|
### `MooncakeBackendOptions`
|
|
|
|
```python
|
|
pg.MooncakeBackendOptions(active_ranks)
|
|
pg.MooncakeBackendOptions(active_ranks, is_extension)
|
|
pg.MooncakeBackendOptions(active_ranks, is_extension, max_world_size)
|
|
```
|
|
|
|
Arguments:
|
|
|
|
- `active_ranks`: `torch.int32` tensor used as the backend-level rank-health
|
|
mask. For `mooncake`, it must be on the accelerator device; for
|
|
`mooncake-cpu`, it must be on CPU. When `max_world_size` is set, size this
|
|
tensor to `max_world_size`, not the current visible world size.
|
|
- `is_extension`: set to `True` for a replacement or joining process that will
|
|
enter an existing group through `join_group()`.
|
|
- `max_world_size`: optional upper bound for reserved rank slots. It lets
|
|
healthy ranks reserve inactive future ranks while keeping
|
|
`dist.get_world_size()` equal to the current active size.
|
|
|
|
### Utility functions
|
|
|
|
| Function | Purpose | Notes |
|
|
| --- | --- | --- |
|
|
| `pg.set_host_ip(host_ip)` | Override the host IP used by the backend. | Call before `init_process_group()`. |
|
|
| `pg.set_device_filter(filters)` | Restrict NIC/HCA selection. | Call before `init_process_group()`. |
|
|
| `pg.set_transfer_engine(engine)` | Reuse an external `TransferEngine`. | The engine must outlive all process groups. |
|
|
| `pg.get_preferred_hca(backend, location)` | Query topology-preferred HCA for a location. | Useful for topology-aware placement/debugging. |
|
|
| `pg.get_active_ranks(backend)` | Return the backend active-rank tensor. | Used by EP fallback and recovery paths. |
|
|
| `pg.get_num_synced_ranks(backend)` | Return the number of ranks synchronized by the backend. | Diagnostic helper. |
|
|
| `pg.extend_group_size_to(backend, size)` | Reserve additional inactive ranks. | Newly extended ranks do not participate until recovered. |
|
|
| `pg.get_peer_state(backend, ranks)` | Check whether candidate ranks have published peer metadata. | Collective among healthy ranks. |
|
|
| `pg.recover_ranks(backend, ranks)` | Activate ready ranks and publish extension state. | Requires peer metadata to be ready. |
|
|
| `pg.join_group(backend)` | Joiner-side blocking call for extension ranks. | Used after `is_extension=True` initialization. |
|
|
|
|
### Supported distributed operations
|
|
|
|
Mooncake Backend implements the following `torch.distributed` APIs. Support may
|
|
depend on device type, dtype, PyTorch version, and whether the current backend is
|
|
`mooncake` or `mooncake-cpu`; run the PG tests on the target environment before
|
|
production use.
|
|
|
|
| API family | Examples | Notes |
|
|
| --- | --- | --- |
|
|
| Collectives | `all_reduce`, `broadcast`, `all_gather`, `all_gather_into_tensor`, `reduce_scatter_tensor`, `all_to_all`, `barrier`, `reduce`, `gather`, `scatter` | Active ranks participate; inactive ranks are skipped by backend internals. |
|
|
| Async work | `dist.all_reduce(..., async_op=True)` | Wait on the returned work object, then synchronize the device stream as needed. |
|
|
| P2P | `isend`, `irecv`, `batch_isend_irecv` | Single-tensor P2P is routed through the Mooncake P2P shim. |
|
|
|
|
## Elastic recovery protocol
|
|
|
|
Mooncake PG supports a two-sided recovery protocol. Existing healthy ranks poll
|
|
for replacement rank readiness, then activate those ranks. Replacement ranks
|
|
start in extension mode, publish metadata, and wait until healthy ranks recover
|
|
them.
|
|
|
|
### Healthy-rank side
|
|
|
|
```python
|
|
from mooncake import pg
|
|
|
|
active_ranks = torch.tensor([1, 1, 0], dtype=torch.int32, device=device)
|
|
dist.init_process_group(
|
|
backend="mooncake",
|
|
rank=rank,
|
|
world_size=2,
|
|
pg_options=pg.MooncakeBackendOptions(
|
|
active_ranks,
|
|
False, # is_extension
|
|
3, # max_world_size
|
|
),
|
|
)
|
|
|
|
backend = dist.group.WORLD
|
|
join_ranks = [2]
|
|
|
|
while not all(pg.get_peer_state(backend, join_ranks)):
|
|
# Continue serving, back off, or poll according to your scheduler policy.
|
|
pass
|
|
|
|
pg.recover_ranks(backend, join_ranks)
|
|
```
|
|
|
|
### Joining-rank side
|
|
|
|
```python
|
|
from mooncake import pg
|
|
|
|
active_ranks = torch.tensor([1, 1, 1], dtype=torch.int32, device=device)
|
|
dist.init_process_group(
|
|
backend="mooncake",
|
|
rank=2,
|
|
world_size=3,
|
|
pg_options=pg.MooncakeBackendOptions(
|
|
active_ranks,
|
|
True, # is_extension
|
|
3, # max_world_size
|
|
),
|
|
)
|
|
|
|
backend = dist.group.WORLD
|
|
pg.join_group(backend)
|
|
```
|
|
|
|
Important semantics:
|
|
|
|
- `get_peer_state()` is collective among the current healthy ranks. Call it in a
|
|
consistent order across those ranks.
|
|
- New ranks are inactive after `extend_group_size_to()` and become collective
|
|
participants only after `recover_ranks()`.
|
|
- A joining rank initialized with `is_extension=True` starts with local-only
|
|
behavior and blocks in `join_group()` until the corresponding healthy ranks
|
|
publish recovery state.
|
|
- Subgroups must be created in the same order on healthy and joining processes,
|
|
following PyTorch `new_group()` ordering rules.
|
|
|
|
## Mooncake EP quick start
|
|
|
|
Mooncake EP exposes `Buffer` from `mooncake.mooncake_ep_buffer`. Initialize it
|
|
with a Mooncake process group and a workspace size computed from the expected
|
|
dispatch shape.
|
|
|
|
```python
|
|
import torch
|
|
import torch.distributed as dist
|
|
from mooncake import pg
|
|
from mooncake.mooncake_ep_buffer import Buffer
|
|
|
|
|
|
# Assume dist.init_process_group(..., backend="mooncake", ...) has completed.
|
|
group = dist.group.WORLD
|
|
rank = dist.get_rank(group)
|
|
world_size = dist.get_world_size(group)
|
|
|
|
num_tokens = 128
|
|
hidden = 7168
|
|
num_experts = 288
|
|
top_k = 8
|
|
max_tokens_per_rank = 128
|
|
|
|
x = torch.randn(num_tokens, hidden, dtype=torch.bfloat16, device="cuda")
|
|
scores = torch.randn(num_tokens, num_experts, dtype=torch.float32, device="cuda")
|
|
topk_idx = torch.topk(scores, top_k, dim=-1).indices
|
|
topk_weights = torch.softmax(
|
|
torch.randn(num_tokens, top_k, dtype=torch.float32, device="cuda"), dim=-1
|
|
)
|
|
|
|
num_ep_buffer_bytes = Buffer.get_ep_buffer_size_hint(
|
|
max_tokens_per_rank,
|
|
hidden,
|
|
world_size,
|
|
num_experts,
|
|
)
|
|
buffer = Buffer(group, num_ep_buffer_bytes)
|
|
|
|
# EP-level rank-health tensor. Kernels may update it to 0 when timeout_us
|
|
# detects a failed source rank.
|
|
active_ranks = torch.ones(world_size, dtype=torch.int32, device="cuda")
|
|
|
|
recv_x, recv_count, handle, event, hook = buffer.dispatch(
|
|
x,
|
|
topk_idx,
|
|
active_ranks,
|
|
num_max_dispatch_tokens_per_rank=max_tokens_per_rank,
|
|
num_experts=num_experts,
|
|
timeout_us=-1,
|
|
use_fp8=True,
|
|
async_finish=False,
|
|
return_recv_hook=False,
|
|
)
|
|
event.current_stream_wait()
|
|
|
|
# Run local experts on recv_x here. If use_fp8=True, recv_x is a
|
|
# (data, scales) tuple; dequantize or feed it into an FP8-aware expert kernel.
|
|
expert_out = run_local_experts(recv_x, recv_count)
|
|
|
|
combined_x, event, hook = buffer.combine(
|
|
expert_out,
|
|
topk_idx,
|
|
topk_weights,
|
|
active_ranks,
|
|
timeout_us=-1,
|
|
handle=handle,
|
|
)
|
|
event.current_stream_wait()
|
|
```
|
|
|
|
## Mooncake EP API reference
|
|
|
|
### `Buffer.get_ep_buffer_size_hint(...)`
|
|
|
|
```python
|
|
Buffer.get_ep_buffer_size_hint(
|
|
num_max_dispatch_tokens_per_rank: int,
|
|
hidden: int,
|
|
num_ranks: int,
|
|
num_experts: int,
|
|
) -> int
|
|
```
|
|
|
|
Returns the workspace size in bytes for the EP buffer. Use the maximum number of
|
|
tokens a rank may dispatch in one step. Underestimating this value can cause
|
|
buffer overflow or incorrect dispatch results.
|
|
|
|
### `Buffer(group, num_ep_buffer_bytes=0)`
|
|
|
|
Creates the EP runtime for a Mooncake process group. The constructor exchanges
|
|
RDMA and IPC metadata through the group, initializes fast-path transports when
|
|
available, and falls back to the Python implementation if the fast path is not
|
|
usable.
|
|
|
|
### `Buffer.dispatch(...)`
|
|
|
|
```python
|
|
recv_x, recv_count, handle, event, hook = buffer.dispatch(
|
|
x,
|
|
topk_idx,
|
|
active_ranks,
|
|
num_max_dispatch_tokens_per_rank,
|
|
num_experts,
|
|
timeout_us,
|
|
use_fp8=True,
|
|
async_finish=False,
|
|
return_recv_hook=False,
|
|
)
|
|
```
|
|
|
|
Arguments:
|
|
|
|
- `x`: local token hidden states, shape `[num_tokens, hidden]`, typically BF16
|
|
on CUDA.
|
|
- `topk_idx`: selected expert IDs, shape `[num_tokens, top_k]`. Use `-1` to mark
|
|
masked selections.
|
|
- `active_ranks`: EP-level rank-health tensor, shape `[num_ranks]`, dtype
|
|
`torch.int32`. Timeout detection may set failed source ranks to `0`.
|
|
- `num_max_dispatch_tokens_per_rank`: workspace capacity per rank. It should be
|
|
at least the maximum local `num_tokens` across ranks for the current step.
|
|
- `num_experts`: global expert count. It must be divisible by `num_ranks`.
|
|
- `timeout_us`: timeout in microseconds. Use `-1` to disable timeout detection.
|
|
- `use_fp8`: when `True`, dispatch returns FP8 data plus scales.
|
|
- `async_finish`: when `True`, returned tensors are associated with the returned
|
|
event for stream-lifetime management.
|
|
- `return_recv_hook`: when `True`, call the returned `hook()` to complete receive
|
|
synchronization; otherwise use `event.current_stream_wait()`.
|
|
|
|
Returns:
|
|
|
|
- `recv_x`: packed local-expert inputs. If `use_fp8=True`, this is
|
|
`(packed_data, packed_scales)`; otherwise it is a BF16 tensor.
|
|
- `recv_count`: number of tokens received by each local expert.
|
|
- `handle`: opaque metadata required by `combine()` and
|
|
`get_next_combine_buffer()`.
|
|
- `event`: `EventOverlap` helper; call `event.current_stream_wait()` before using
|
|
outputs when no hook is used.
|
|
- `hook`: optional synchronization hook used when `return_recv_hook=True`.
|
|
|
|
### `Buffer.combine(...)`
|
|
|
|
```python
|
|
combined_x, event, hook = buffer.combine(
|
|
x,
|
|
topk_idx,
|
|
topk_weights,
|
|
active_ranks,
|
|
timeout_us,
|
|
handle,
|
|
zero_copy=False,
|
|
async_finish=False,
|
|
return_recv_hook=False,
|
|
out=None,
|
|
)
|
|
```
|
|
|
|
Arguments:
|
|
|
|
- `x`: local expert outputs packed in the layout returned by `dispatch()`.
|
|
- `topk_idx` and `topk_weights`: routing metadata for combining expert outputs
|
|
back to local tokens.
|
|
- `active_ranks`: same EP-level rank-health tensor used by `dispatch()`.
|
|
- `timeout_us`: timeout in microseconds; use `-1` to disable timeout detection.
|
|
- `handle`: the handle returned by the matching `dispatch()` call.
|
|
- `zero_copy`: when `True`, write expert outputs into
|
|
`buffer.get_next_combine_buffer(handle)` and pass that tensor to `combine()`.
|
|
- `out`: optional output tensor for the combined result.
|
|
|
|
### `Buffer.get_next_combine_buffer(handle)`
|
|
|
|
Returns the next combine buffer for zero-copy expert output. Use it only with the
|
|
matching dispatch `handle` and pass the resulting tensor back to `combine()` with
|
|
`zero_copy=True`.
|
|
|
|
### `Buffer.update_ep_member()`
|
|
|
|
Reconnects EP peers after backend membership changes. Call it after PG recovery
|
|
updates rank activeness so EP transport metadata and QPs can be refreshed.
|
|
|
|
## Active-rank tensors: PG vs EP
|
|
|
|
There are two active-rank tensors in the API surface:
|
|
|
|
- **PG active-rank mask**: passed to `pg.MooncakeBackendOptions`. This is the
|
|
backend-level health mask used by collective and recovery logic.
|
|
- **EP active-rank tensor**: passed to `Buffer.dispatch()` and `Buffer.combine()`.
|
|
It is also rank-level (`[num_ranks]`, `torch.int32`) and may be updated by EP
|
|
kernels when timeout detection marks a peer as failed.
|
|
|
|
In simple integrations these tensors often carry the same health information,
|
|
but they are passed through different API layers. Keep their dtype, device, and
|
|
shape consistent with the process group world size or reserved `max_world_size`.
|
|
|
|
## Tests and examples
|
|
|
|
- PG collectives: `mooncake-pg/tests/test_pg_collectives.py`
|
|
- PG elastic recovery and subgroup extension: `mooncake-pg/tests/test_pg_elastic.py`
|
|
- PG benchmark harness: `mooncake-pg/benchmark/README.md`
|
|
- EP correctness and failure simulation: `mooncake-ep/tests/test_ep_grid.py`
|
|
- Wheel-level EP example: `mooncake-wheel/tests/test_mooncake_ep.py`
|
|
|
|
See [PG/EP troubleshooting](../troubleshooting/pg-ep-troubleshooting.md) for
|
|
common setup and runtime issues.
|