[Kimi-K3] Add option to shard the shared expert instead of replicating - #50656
tlrmchlsmth merged 2 commits into
Conversation
| # for two small collectives per layer, so it only wins below roughly a | ||
| # thousand tokens per step: intended for decode instances in a P/D | ||
| # disaggregated deployment, not for prefill or unified serving. | ||
| "VLLM_KIMI_K3_SHARD_SP_MLP": lambda: bool( |
There was a problem hiding this comment.
NIT
| "VLLM_KIMI_K3_SHARD_SP_MLP": lambda: bool( | |
| "VLLM_KIMI_K3_SHARD_SP_SHARED_EXPERT": lambda: bool( |
There was a problem hiding this comment.
much better, agreed will change it :)
| gate_up, _ = self.gate_up_proj(x) | ||
| x = self.act_fn(gate_up) | ||
| x, _ = self.down_proj(x) | ||
| if self.shard_sequence_parallel: | ||
| x = sp_reduce_scatter(x) |
There was a problem hiding this comment.
QQ:How about combining this RS and the sequential AG into one AR?
|
How's the perf compared to pure TP? 👀 |
|
This pull request has merge conflicts that must be resolved before it can be |
…cating
Under sequence-parallel MoE, KimiMLP threads use_sequence_parallel straight
through as disable_tp, so the dense and shared-expert MLPs are replicated on
every rank. Each rank then streams the whole weight to serve only its own
token shard: for the shared experts that is 264.2 MB per layer per rank,
against 66.1 MB TP4-sharded, or ~22.6 GiB per rank across the model.
Add VLLM_KIMI_K3_SHARD_SP_MLP to TP-shard them instead. Under the flag the
MLP all-gathers the full token set, computes this rank's partial over its
intermediate shard, and reduce-scatters -- the reduce-scatter both sums
across TP and restores the sequence sharding, so the block still ends with
one collective per direction rather than an all-reduce plus a re-shard.
The trade is weight bandwidth and resident memory for two collectives per
layer, which only wins at low token counts, so it is opt-in and intended for
decode instances under P/D disaggregation. It is deliberately not enabled on
the FusedMoE path, which hands the shared experts to the runner and assumes
the replicated layout.
Measured on 4x GB200 nodes (16 GPUs), Kimi-K3 pruned checkpoint, P/D
disaggregated, decode TP4/DP2/EP8 with the deep_gemm_mega_moe backend:
flag off flag on delta
weights per rank 94.62 GiB 76.71 GiB -17.91
GPU KV cache 2,988,288 3,679,187 +23.1%
decode kernel time 27,881 us 25,052 us -10.1%
ITL p50 24.49 ms 21.86 ms -10.7%
GSM8K ~40% 40.7%
End-to-end throughput did not improve at that operating point (926 -> 902
output tok/s); the deployment is prefill-bound there and the flag is
decode-scoped.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Tyler Michael Smith <tlrmchlsmth@gmail.com>
e505566 to
822cc87
Compare
|
ugh - sorry for the slop |
|
Need to follow this up with support for the non-mega-moe case. |
|
Some results running Kimi K3 on GB200 DP8xTP4 using the DecodeBenchConnector + uniform random expert routing: Replicated: TP-sharded: Pure decode performance on GB200 DP8xTP4 Kimi K3 + DecodeBenchConnector:
|
…nflicts Dropped the legacy TYPE_CHECKING block and environment_variables dict wholesale, then ported main's delta across 10 main-side commits: Additions: VLLM_ROCM_USE_AITER_MOE_SITUV2_A8W4 (vllm-project#50582), VLLM_USE_RUST_BENCH (vllm-project#50081), VLLM_KIMI_K3_SHARD_SP_SHARED_EXPERT (vllm-project#50656), VLLM_RAISE_ON_LOGIT_NANS (vllm-project#50323), VLLM_ENABLE_COHERE_API (vllm-project#47189). Modifications: VLLM_COMPUTE_NANS_IN_LOGITS is now implied by VLLM_RAISE_ON_LOGIT_NANS (cross-field, so a model_validator); _resolve_rust_frontend_path -> _resolve_rust_cli_path, resolving on either VLLM_USE_RUST_FRONTEND or VLLM_USE_RUST_BENCH. Deletions: VLLM_CPU_SGL_KERNEL (vllm-project#50801), Q_/K_/V_SCALE_CONSTANT (vllm-project#49389 -- main deleted the dict entries but left the TYPE_CHECKING annotations; followed the PR's intent). tests: ported VLLMValidationError assertions; adapted test_rust_bench_auto_path_missing_fails_fast to construct ServerSettings directly. Dropped the Q_SCALE_CONSTANT case from test_envs_pydantic.py. AI assistance (Claude) was used for this merge resolution. Co-authored-by: Claude Signed-off-by: Vinay Damodaran <vrdn@hey.com>
Shard the shared-expert weights to avoid redundant work. In order to do so we need to re-distribute the activations, which adds an extra AllGather and ReduceScatter.
This has been tested e2e to work and speed up decodes. So far, preliminary perf measurements show this is a decode win, and for prefill it's a throughput vs kv cache size tradeoff.
FYI the performance and memory footprint improvements are overstated since all of the numbers are from running @mgoin's 75% expert-sparse Kimi K3
Generated stuff below
Problem
Under sequence-parallel MoE,
KimiMLPthreadsuse_sequence_parallelstraight through asdisable_tpon both projections (model.py), so the dense and shared-expert MLPs are replicated on every rank. Each rank streams the entire weight to serve only its own token shard.For Kimi-K3's shared experts (hidden 7168, shared intermediate 6144):
The routed experts are EP-sharded and the latent projections are deliberately replicated, but the shared experts are sharded by nothing — they sit outside the EP machinery, and
disable_tpremoves the only sharding they had.This is visible from two independent directions:
dense/TP + experts/(TP*DP)under-predicted actual per-GPU usage by ~28 GiB, of which the replicated shared experts account for ~23 GiB.Corroborating detail:
KIMI_K3_PROJECTIONSinlow_latency_gemm.pycontainsshared_gate_up_proj/shared_down_projentries only at TP-sharded shapes (TP4/TP8/TP16). The replicated shapes have no entry, so under SP they fall through to cuBLAS — the table was tuned against the non-SP path, where these layers are TP-sharded.Note this replicate-under-SP pattern is a vLLM-wide convention (DeepSeek-V4 does the same, with an explicit comment). Kimi-K3 is an outlier only because its shared expert is unusually large.
Change
Add
VLLM_KIMI_K3_SHARD_SP_MLP(default off). Under the flag,KimiMLPTP-shards both projections and:The reduce-scatter does double duty, so the block still ends with one collective per direction rather than an all-reduce plus a re-shard.
Deliberately not enabled on the FusedMoE path, which hands the shared experts to the runner and assumes the replicated layout.
Trade-off and why it is opt-in
This trades weight bandwidth and resident memory for two collectives per layer. That only wins at low token counts, so it is off by default and intended for decode instances under P/D disaggregation, which can commit to the sharded layout and never see prefill-sized batches. Prefill instances should leave it off.
One known limitation: the flag is global, but prefill and decode want opposite answers. In a P/D deployment it can be scoped per role via the env var. In an aggregated deployment a single flag cannot be right for both — the dense MLP would shard on prefill, on the losing side of the crossover.
Testing
The added test models disjoint token shards per rank, which is what makes the failure mode visible: an earlier version of this change sharded both projections over their output dim and all-gathered the feature slices, on the reasoning that no reduction was then needed. That is wrong under sequence parallelism — each rank computes its intermediate chunk for its own tokens, so no rank ever holds every chunk for any single token. It ran cleanly and produced degenerate output. A test that models every rank holding the same tokens passes against that broken design; this one does not.
Evaluation
Measured on 4x GB200 nodes (16 GPUs), Kimi-K3 pruned checkpoint, P/D disaggregated, decode role TP4/DP2/EP8 with the
deep_gemm_mega_moebackend,max_num_seqs=128. Load: 64 concurrent, 9k ISL / 256 OSL, 7 min. Torch profiler on one DP rank, 5 iterations.In the profile the replicated shared
gate_upkernel (29.64 us, n=460) disappears entirely, and the lamport all-gather / reduce-scatter counts double (n=470 -> 935, n=465 -> 930) exactly as the design implies.Honest reading of these numbers:
gpu_memory_utilization=0.95.Not done: no congestion-terminated concurrency sweep, so the saturation knee is unlocated; no measurement at the batch sizes where replication should win, so the crossover is estimated (~700 tokens at TP8) rather than measured.
Not a duplicate
Checked before opening:
The only open Kimi-K3 PRs are #50404 (MLA with disabled context parallelism) and #50319 (ROCm gfx942); neither touches the MoE block or sequence-parallel weight layout. Nothing open addresses shared-expert replication under SP.
AI assistance
This change was developed with AI assistance (Claude). The analysis, implementation, and the cluster measurements above were produced with it. A human submitter has reviewed the diff and is responsible for defending it.