Problem
The paged KV path recomputes KV from scratch on every prefill, even when requests share the same prefix (system prompts, multi-turn history). This hurts TTFT.
What the upstream scheduler already does
The upstream vLLM scheduler handles block hashing, cache hit detection, and block reuse:
Request.update_block_hashes() computes chained block hashes on request creation
kv_cache_manager.get_computed_blocks(request) finds the longest prefix cache hit
NewRequestData.num_computed_tokens carries the hit count to the model runner
block_ids includes both cached and newly allocated blocks
enable_prefix_caching defaults to True, nothing in vllm-metal forces it off
No hash/LRU code needed on our side — the scheduler handles all of that.
What vllm-metal's paged path does today
The model runner reads num_computed_tokens (model_runner.py:1949), but only uses it for intermediate chunk detection. For complete prefills, it always processes all tokens from position 0.
Why this is blocked on kernel_v2
Our prefill uses mx.fast.scaled_dot_product_attention(Q, K, V) — it takes Q/K/V as input tensors and can't read cached K/V from paged blocks. If we skip the prefix tokens, SDPA won't have the prefix K/V and attention output will be wrong.
On GPU, flash attention reads cached K/V directly from paged blocks via block tables. We need kernel_v2 (varlen attention, #148 Stage 2) to do the same on Metal.
Once kernel_v2 lands:
- When
num_computed_tokens > 0, only prefill token_ids[num_computed_tokens:]
- Add offset to
prepare_prefill() so slot_mapping starts from the right position
- Set RoPE offset =
num_computed_tokens
- Kernel_v2 reads cached prefix K/V from paged blocks via block tables
RFC @WindChimeRan @LxYuan0420
Problem
The paged KV path recomputes KV from scratch on every prefill, even when requests share the same prefix (system prompts, multi-turn history). This hurts TTFT.
What the upstream scheduler already does
The upstream vLLM scheduler handles block hashing, cache hit detection, and block reuse:
Request.update_block_hashes()computes chained block hashes on request creationkv_cache_manager.get_computed_blocks(request)finds the longest prefix cache hitNewRequestData.num_computed_tokenscarries the hit count to the model runnerblock_idsincludes both cached and newly allocated blocksenable_prefix_cachingdefaults toTrue, nothing in vllm-metal forces it offNo hash/LRU code needed on our side — the scheduler handles all of that.
What vllm-metal's paged path does today
The model runner reads
num_computed_tokens(model_runner.py:1949), but only uses it for intermediate chunk detection. For complete prefills, it always processes all tokens from position 0.Why this is blocked on kernel_v2
Our prefill uses
mx.fast.scaled_dot_product_attention(Q, K, V)— it takes Q/K/V as input tensors and can't read cached K/V from paged blocks. If we skip the prefix tokens, SDPA won't have the prefix K/V and attention output will be wrong.On GPU, flash attention reads cached K/V directly from paged blocks via block tables. We need kernel_v2 (varlen attention, #148 Stage 2) to do the same on Metal.
Once kernel_v2 lands:
num_computed_tokens > 0, only prefilltoken_ids[num_computed_tokens:]prepare_prefill()so slot_mapping starts from the right positionnum_computed_tokensRFC @WindChimeRan @LxYuan0420