[Kimi-K3] Use the fused K/V pack kernel on the decode-context-parallel prefill path - #56881
brandonfzhang wants to merge 1 commit into
Conversation
…l prefill path Chunked-context prefill under decode context parallelism (DCP) runs through the generic MLACommonImpl._context_parallel_compute_prefill_context loop, which owns the allgather and reorg of the gathered KV. That loop still finishes every chunk with four PyTorch ops per layer: cast kv_nope to fp8, cast k_pe to fp8, split off v, and write [k_nope | k_pe] into a fresh key tensor with two strided slice copies. With fp8 prefill each of those ops is a separate launch over the whole chunk. vllm-project#51772 fused exactly that pack for Kimi-K3's own (non-DCP) chunked-context loop with fused_mla_kv_concat / fused_mla_kv_concat_quant_fp8, and left the DCP loop out of scope because it is shared with every MLA model. This change adds the smallest seam that lets the layer reuse those kernels there: the generic loop takes an optional kv_pack callable. With None it runs today's statements unchanged; with a callable it hands over the bf16 kv_b_proj output view, the gathered k_pe and use_fp8_prefill, and uses the returned (k, v). The Kimi-K3 layer supplies a kv_pack that splits kv_nope and calls the two existing fused kernels, mirroring its run_chunk. The fused route is decided once at init and logged with info_once. It is taken only when dcp_world_size > 1, the impl's loop accepts kv_pack (probed via the signature, so an impl without the hook keeps the stock tail) and neither kill switch is set: VLLM_K3_DCP_FUSED_KV_PACK=0 or VLLM_K3_DCP_FUSED_KV_PACK_DISABLE=1 each restores the stock tail on their own, and each logs which switch fired. The pack is a pure relayout plus the same bf16 -> fp8 cast, so the outputs are byte-identical to the stock tail. Tests: the generic loop with kv_pack=None produces the stock cast/split/ concat result for bf16 and fp8 prefill; a supplied kv_pack receives the un-cast projection output and the gathered k_pe and its (k, v) reach the prefill backend; both kill switches and their defaults are honoured and each route logs its marker; and, on CUDA with the _C op available, the fused kernels' key and value are byte-identical to the stock path on random inputs for bf16 and fp8. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Brandon Zhang <31413216+brandonfzhang@users.noreply.github.com>
|
👋 Hi! Thank you for contributing to the vLLM project. 💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment Once the PR is approved or has the If you have any questions, please reach out to us on Slack at https://slack.vllm.ai. Agent GuidelinesIMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban. 🚀 |
Purpose
Kimi-K3 prefill with a long context runs in chunks. For every chunk of every MLA layer, the model projects the gathered latent cache into per-head keys and values, casts them to FP8, and lays them out as the contiguous key and value tensors the attention kernel expects.
#51772 fused that cast-and-layout step into a single kernel (
fused_kimi_k3_mla_kv_concat_quant_fp8) for the normal prefill path. That PR left decode context parallelism (DCP) out of scope. Whendecode_context_parallel_size > 1, the Kimi-K3 layer hands chunked-context prefill to the shared MLA implementation, whose per-chunk tail still does the work as four separate PyTorch operations: two FP8 casts and two strided slice copies.On the deployment that motivated this change (TP8 x DCP8 on two GB300 nodes, FP8 KV cache, an agentic-coding replay at 70 concurrent sessions), that tail costs about 380 microseconds and moves 879 MB per 66k-token chunk, for each of 24 MLA layers, on almost every engine step. The fused kernel reads the projection output once and writes both outputs, roughly a quarter of the bytes.
This PR routes the DCP path through the same fused kernel the non-DCP path already uses.
What changes
Two files.
vllm/model_executor/layers/attention/mla_attention.py._context_parallel_compute_prefill_contextgains an optionalkv_packcallable. When it is not given, the method runs exactly the statements it runs today. When it is given, the method calls it in place of the cast, split, and concatenate tail and uses the returned key and value. No other model is affected, because nothing else passes the argument.vllm/models/kimi_k3/nvidia/mla.py. The Kimi-K3 layer supplies thekv_packfunction: given the projection output and the rope key, it returns the finished key and value tensors by callingfused_kimi_k3_mla_kv_concat_quant_fp8when the prefill query is FP8 andfused_kimi_k3_mla_kv_concatotherwise. These are the same two kernel calls the layer's non-DCP loop already makes. The docstrings that said DCP is not fused are updated.When it activates, and how it falls back
The fused pack is used only when all of the following hold:
decode_context_parallel_size > 1. Non-DCP deployments are unchanged.kv_packargument. This is checked once at layer initialization by inspecting the method signature, so an older or patched implementation without the hook keeps the stock tail.VLLM_K3_DCP_FUSED_KV_PACK=0orVLLM_K3_DCP_FUSED_KV_PACK_DISABLE=1restores the stock tail. The second spelling exists because A/B harnesses conventionally disable a feature by setting a variable to 1.The route decision is logged once at initialization.
Testing
Unit tests in
tests/models/kimi_k3/test_mla_dcp_kv_pack.py:kv_packsupplied, the shared MLA code produces exactly what it produced before this change, for both bf16 and FP8 inputs. This is the regression guard for every other MLA model.kv_packfunction is supplied, it is called with the projection output before any conversion, and the key and value it returns are what get passed to the attention backend.End-to-end performance A/B, same build, same hardware, same 3600-second agentic replay, differing only in this change:
For scale, two stock runs on this route differ by about 1.5% in throughput and 2.6% in p90 interactivity, so these deltas are well outside run-to-run noise. Latency tails (p99) move by 10 to 15 percent between identical runs and are not reported as evidence.
Accuracy. Standard probes such as GSM8K use prompts far shorter than the 16384-token scheduler chunk and never reach the DCP context path, so they cannot test this change. Instead we ran a paired teacher-forced perplexity test on long inputs: ten public-domain texts of roughly 30k to 36k tokens each, 330,944 tokens in total, so every request spans at least two scheduler chunks and exercises the changed code. Each token's log-probability was scored on two servers, one with the fused pack and one with the kill switch set, and the two runs were compared token by token. Logs confirm which path each server took.
Six of the ten texts produced bit-identical log-probabilities on both servers. The residual differences on the other four are consistent with batching nondeterminism between two independent server starts: on a repeat run, a different subset of texts differed.
Test Result
ruff checkandruff format --checkon the changed files: clean.kv_pack) with stubbed dependencies; all assertions pass. A fullpytest tests/models/kimi_k3/test_mla_dcp_kv_pack.pyrun inside a vLLM environment has not been executed yet and should be confirmed by CI or a reviewer with a GPU before merge.Scope and limitations
vllm/envs.pyalongside the existingVLLM_KIMI_K3_*variables is a reasonable follow-up, or can be folded into this PR if reviewers prefer.kv_b_proj, so the two are complementary, but whichever lands second will need a small rebase.AI assistance was used to prepare this change.