Skip to content

feat(gpu): FFN-side EPLB support for CUDA AFD - #43

Draft
shlomitk1 wants to merge 19 commits into
mainfrom
enable-eplb
Draft

shlomitk1 wants to merge 19 commits into
mainfrom
enable-eplb

Conversation

@shlomitk1

Copy link
Copy Markdown

Purpose

Enable FFN-side EPLB (Expert Parallel Load Balancing) for CUDA AFD deployments on the pinned vLLM 0.30.0.

The FFN role now owns vLLM's native EPLBController and follows the upstream GPUModelRunner wiring: the loaded model registers its native DeepseekV2MoE layers (via the inherited MixtureOfExperts protocol), and every connector-driven step prepares the router's unpadded token count and steps the EPLB state. Rearrangement, load-window all-reduce, in-place map
commit (CUDA-graph compatible), and expert-weight transfer (nixl/torch_gloo auto-selected) all run in upstream EplbState machinery — no plugin duplication.

Two deployment recipes enable EPLB on DeepSeek-V2-Lite (2A2F colocation, single-node and multipod) with 32 redundant experts (96 physical, 48 per FFN rank at EP size 2).

Design notes: docs/design/module/ffn_runtime.md ("FFN-side EPLB" section).

Issue

  • Related issue(s): #
  • Closing keyword, only if fully resolved: Closes #

Scope

  • In scope:
    • GPUFFNModelRunner EPLB wiring: controller construction, model registration on load, per-step prepare_forward + step, dummy stepping for warmup/graph-capture paths
    • Unpadded token accounting for the router's map-and-record kernel (real_attention_token_total, placeholder rows excluded)
    • Validation: EPLB allowed on the FFN role only; rejected on Attention and with DBO/ubatching
    • Unit tests for all of the above; two GPU recipes; module doc update
  • Out of scope:
    • NPU FFN EPLB (NPU validator still rejects enable_eplb)
    • EPLB + DBO/ubatching on the FFN role (rejected — see Implementation Notes)
    • An E2E scenario under tests/e2e (recipes are the E2E vehicle for now; needs a GPU run first)
    • Elastic EP integration (setup_eplb_from_mapping not wired)

Implementation Notes

  • Why FFN-only: with compute_gate_on_attention=false, all MoE routing, expert weights, and EP-group membership live on the FFN ranks; the Attention role ships hidden states and its CUDA remote-expert shells reject EPLB (AFDDeepseekV2DeepseekV2DecoderLayer construction check retained as defense in depth). EPLB flags therefore go on the FFN serve command only
    (as in the recipes).
  • Extension point used: the runner reuses vLLM 0.30's EPLBController (vllm/v1/worker/gpu/eplb_utils.py) rather than reimplementing stepping. The connector daemon loop is the "once per engine step" cadence that upstream gets from sample_tokens/pool/_dummy_run decorators; the new eplb_step seam is documented with a patch-reason block referencing the pinned
    commit (ced6857afa0ea7b2e3f0846a62e1394e90f15607).
  • Step semantics: eager forwards and graph replays are real steps (the record kernel runs inside captured graphs and reads device-side should_record/map tensors, which are committed in place after rearrangement). Warmup and graph-capture steps are dummy steps that keep every FFN rank's rearrangement counter in lockstep — required or the rearrangement
    collectives hang.
  • Token counting: the unpadded count is the step's total real Attention tokens (TP-expanded, idle-peer placeholder rows excluded). Each FFN rank's router view covers those tokens either through TP-replicated delivery (FFN DP=1) or the MoE-level all-gather (FFN DP>1), matching what the map-and-record kernel processes. The shared TP-expansion logic was
    extracted to avoid a third copy of the connector's expansion rules.
  • DBO restriction: FFN EPLB rejects enable_dbo/use_ubatching. The record kernel selects its per-ubatch token tensor via dbo_current_ubatch_id(), and the FFN daemon loop never installs vLLM's DBO thread context, so multi-stage steps would read the wrong count. Enforced in validation and with a single-stage runtime check.
  • No monkey patches added; the eplb process group and override_envs_for_eplb come free from the inherited native Worker init path.

Test Plan

CPU-only (this PR):

  • uv run pytest tests/unit — full unit suite
  • uv run ruff check / ruff format --check on touched files
  • uv run mypy on touched plugin modules
  • bash -n on both recipes

GPU-gated (follow-up, not run here): serve both recipes' commands against DeepSeek-V2-Lite, confirm the FFN log shows EPLB is enabled for MoE part of model, a Rearranging experts line after step_interval - step_interval/4 steps, and GSM8K smoke accuracy; then wire an E2E scenario.

Test Result

  • uv run pytest tests/unit -p no:warnings → 1255 passed, 114 skipped, 55 subtests passed (exit 0)
  • New coverage: accept-ffn-eplb, reject-attention-eplb, reject-ffn-eplb-dbo, reject-ffn-eplb-ubatching (validator); 8 runner tests (load/registration, real vs dummy stepping, replay stepping, TP-expanded count computation, disabled no-op, multi-stage rejection, capture-path stepping); 7 parametrized real_attention_token_total cases
  • ruff check → All checks passed; ruff format --check → clean
  • mypy (3 files) → Success: no issues found
  • GPU E2E: not run — no GPU host in this session; skipped per plan above
  • bash -n on both recipes → syntax OK

Docs Impact

  • Files updated: docs/design/module/ffn_runtime.md (new "FFN-side EPLB" section; amended the V2-pair static-EP sentence to note the FFN-only EPLB exception), plus the two recipes under recipe/gpu/P2pNcclAFDConnector/deepseek_v2_lite/prefill_decode_colocation/
  • If none, reason: N/A

────────────────────────────────────────────────────────────────────────────────

Essential PR Checklist
  • Purpose is clear and linked to public context when possible.
  • Scope is bounded.
  • Compatibility with vLLM v0.30.0 is considered.
  • No changes are made to the vLLM source checkout.
  • Plugin-owned classes or explicit dotted class paths are preferred over monkey patches.
  • Any compat shim or monkey patch is isolated, idempotent, version-guarded, documented, and tested. (no new patches; the one new seam, eplb_step, is documented with a patch-reason block and unit-tested)
  • Imports remain CPU-safe; CUDA-heavy work is delayed or GPU-gated. (new import vllm.v1.worker.gpu.eplb_utils is no heavier than the module's existing vLLM imports; tests/unit still collects and passes without CUDA)
  • Validation evidence is included, including skipped GPU tests when applicable.
  • Documentation impact is stated.

────────────────────────────────────────────────────────────────────────────────

Signed-off-by: Shlomit Koyfman <shlomitk@il.ibm.com>
Signed-off-by: Shlomit Koyfman <shlomitk@il.ibm.com>
# Conflicts:
#	docs/design/module/ffn_runtime.md
#	tests/unit/model_executor/models/test_qwen3_moe_construction.py
Signed-off-by: Shlomit Koyfman <shlomitk@il.ibm.com>
The pre-commit mypy run (pinned 1.11.1, follow-imports=skip for tests)
flagged two issues the local dev env run missed:

- The new M2N EPLB test annotated fake-dist buffers with torch.Tensor;
  under follow-imports=skip torch is not resolvable. Drop the
  annotations.
- metadata.py, touched by the EPLB change, still carried pre-existing
  one-argument Generator[...] annotations that mypy 1.11 rejects;
  complete them to Generator[..., None, None].
vLLM logs EPLB balancedness from EplbState.step, which the M2N driver
never runs (routing, and load recording, are on Attention). Wire the
existing log_balancedness eplb-config flag to the M2N boundary instead:
when set, FFN world rank 0 logs, per MoE layer, the logical experts each
FFN rank holds after a rearrangement, the replicated ones, and the
recorded per-rank token load that drove the choice.
The placement log added for DeepEP M2N now also runs on the P2P path,
where EplbState.step commits rearrangements itself: GPUFFNModelRunner's
eplb_step snapshots the committed map only on steps where the counter
boundary or a pending async transfer can commit, and logs the placement
(plus vLLM's balancedness stats, which log_balancedness already drove)
when the map actually changed, on EP rank 0 only.

The placement formatters move to afd_plugin/distributed/eplb.py so the
shared runner and the DeepEP M2N connector module use one implementation
without the runner importing an M2N-specific module.
Signed-off-by: Shlomit Koyfman <shlomitk@il.ibm.com>
Signed-off-by: Shlomit Koyfman <shlomitk@il.ibm.com>
Signed-off-by: Shlomit Koyfman <shlomitk@il.ibm.com>
Signed-off-by: Shlomit Koyfman <shlomitk@il.ibm.com>
Signed-off-by: Shlomit Koyfman <shlomitk@il.ibm.com>
Signed-off-by: Shlomit Koyfman <shlomitk@il.ibm.com>
Signed-off-by: Shlomit Koyfman <shlomitk@il.ibm.com>
# Conflicts:
#	docs/design/module/model_integration.md
#	tests/unit/connectors/test_p2p_connector.py
#	tests/unit/model_executor/models/test_deepseek_v2_proxy.py
#	tests/unit/model_executor/models/test_qwen3_5_contract.py
#	tests/unit/model_executor/models/test_qwen3_moe_construction.py
#	tests/unit/v1/worker/test_ffn_model_runner.py
#	tests/unit/v1/worker/test_ffn_routed_experts_model_runner.py
#	tests/unit/v1/worker/test_model_runner_v2.py
@github-actions github-actions Bot added documentation Improvements or additions to documentation NVIDIA labels Nov 9, 2026
Signed-off-by: Shlomit Koyfman <shlomitk@il.ibm.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation NVIDIA

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant