Add embedding KV-cache skip fast path (skip redundant per-layer paged-KV write/gather) - #1
Open
xtangxtang wants to merge 3 commits into
Open
Add embedding KV-cache skip fast path (skip redundant per-layer paged-KV write/gather)#1xtangxtang wants to merge 3 commits into
xtangxtang wants to merge 3 commits into
Conversation
Embedding requests are pure prefill (no decode), so writing K/V into the
paged KV pool and gathering it right back per layer in forward_extend is
write-then-read-back that nobody else ever reads. When every safety guard
holds, skip both the set_kv_buffer write and the pool gather and run
per-sequence SDPA directly on the freshly-computed local k/v -- bit-exact
vs. the original path (verified cos=1.0 in prior monkey-patch validation,
torch.equal here).
Opt-in via SGLANG_SKIP_EMBED_KV_CACHE=1; OFF by default so forward_extend
runs the original path byte-for-byte (zero regression). All guards ported
from the validated monkey-patch, adapted to this fork's newer forward_extend
(KVWriteLoc, self.token_to_kv_pool, orig_seq_lens), plus a new SWA guard:
(1) SGLANG_SKIP_EMBED_KV_CACHE == "1" (cached at __init__)
(2) save_kv_cache is True
(3) not model_config.is_generation (_embed_no_decode, __init__)
(4) not layer.is_cross_attention
(5) extend_prefix_lens is not None and all == 0
(6) chunked-prefill safe: if chunking possible, extend_seq_lens ==
orig_seq_lens (else _kvskip_chunk_impossible short-circuits it)
(7) pool stores K/V verbatim: store_dtype == dtype and k.dtype == pool.dtype
(8) NEW (fork-specific): sliding window not enabled for this layer
(sliding_window_size is None or <= -1) -- local path builds no SWA
mask, so any SWA layer conservatively falls back to the original path.
Docs: README_embedding_kv_cache_skip.md (switch usage + guards). Recommends
the orthogonal companion config --max-total-tokens 16384, which shrinks the
embedding KV pool from the default (hundreds of GB) to ~1.8 GB.
Test: test/registered/attention/test_embedding_kv_cache_skip.py -- self-
contained (no server/weights), compares skip path vs original gather path
bit-exact (single/multi-seq, GQA, bf16) and asserts fallback (nonzero
prefix / sliding window / switch off) stays bit-identical to the original.
…escription, not in the source tree
Author
|
/tag-and-rerun-ci extra |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Embedding requests are pure prefill, no decode. SGLang's
forward_extendstill writes K/V into the paged KV pool and gathers it right back per layer — but for an embedding model nothing ever reads it back (there is no decode step). This PR adds an opt-in fast path that, when every safety guard holds, skips both theset_kv_bufferwrite and the pool gather and runs per-sequence SDPA directly on the freshly-computed local K/V. Bit-exact vs the original path.How
SGLANG_SKIP_EMBED_KV_CACHE=1; OFF by default →forward_extendruns the original path byte-for-byte (zero regression).torch_native_backend.pyis purely additive: a guard block is prepended and only returns early (into_forward_extend_kv_skip) when all guards pass, otherwise control falls through to the unchanged original body.Guards (all must hold, else fall back to the original path):
SGLANG_SKIP_EMBED_KV_CACHE == "1")save_kv_cachenot model_config.is_generation) — no decode reads KV backextend_prefix_lens == 0for all sequences)extend_seq_lens == orig_seq_lenswhen chunking is possible for the server config)store_dtype == dtype, no fp8/uint8 repacking)Usage
SGLANG_SKIP_EMBED_KV_CACHE=1 \ python3 -m sglang.launch_server \ --model-path <embedding-model> --is-embedding \ --attention-backend torch_native \ --max-total-tokens 16384Off by default — with
SGLANG_SKIP_EMBED_KV_CACHEunset,forward_extendruns the original path byte-for-byte.Companion config (no code)
The
--max-total-tokens 16384above is an orthogonal, pure-memory optimization (apply it with or without the switch). The KV pool only exists for decode, which embedding never does, so it does not need to be sized for a large concurrent decode population — capping it shrinks the paged KV pool from the default (hundreds of GB reserved) to ~1.8 GB, with no effect on throughput or correctness.Correctness
max_abs_diff = 0,torch.equal = True) across single-seq, multi-seq, GQA (16Q/8KV), bf16, MHA (8Q/8KV), and encoder-only (causal=False). Rationale: whenextend_prefix_lens == 0, the pool-gathered K/V equals the local K/V (guard fix radix cache match sgl-project/sglang#7 ensures verbatim storage), so both SDPA calls receive identical inputs.test/registered/attention/test_embedding_kv_cache_skip.py.Measured impact (bf16, Qwen3-Embedding-0.6B, single node, 48 cores)
CI States
Latest PR Test (Base): ❌ Run #29314861004
Latest PR Test (Extra): 🚫 Run #29316328565