Wink Pings

同一段提示词,首Token为什么能从92.5秒降到0.54秒

Avi Chawla拆了LLM的四层缓存,并解释了prefix-aware路由。生产环境里,路由选错GPU会让共享前缀全部重算,p90首Token差距能到两个数量级。

生产环境里的提示词很少每次全新。系统提示、工具定义、前几轮对话通常一模一样,只有最后一段是新内容。GPU刚服务完上一轮,显存里还留着共享前缀的KV缓存。路由如果不知道这件事,把下一轮请求发给另一张GPU,那张卡就得把整个前缀重新算一遍,才写出第一个token。

Avi Chawla在推文里给了一个benchmark:8个vLLM pod、16张H100,prefix-aware路由把p90首Token时间压到0.54秒,随机路由在同一批pod上是92.5秒。差两个数量级。TTFT是首Token时间,p90表示90%的请求低于这个值。

prefix-aware router做的事情不复杂。请求带着前缀块A到D和新后缀E进来,它按块对prompt做hash,每个hash覆盖它前面的全部内容。然后拿这些hash查block-to-worker索引。索引存的是位置,不存KV tensor。示例里Worker A有2个匹配块,Worker C有3个,Worker B有4个,请求就给Worker B。Worker B复用A到D的缓存KV,只计算E的KV,开始解码。E写入缓存后,缓存事件刷新路由索引,下一轮继续落到Worker B。没有worker匹配时,请求在router选中的worker上做完整prefill。真实router还会看负载,避免缓存最热的GPU排最长队。这一切的前提是prefix cache本身有效,prompt前部一个小改动,后面所有块都会miss。

要理解这个路由,得把LLM栈里四层缓存分开。Avi Chawla的文章把KV cache、prefix caching、prompt caching和semantic cache放在一起讲,因为它们都叫缓存,但存的对象不同。前三层是精确匹配,miss只影响成本和延迟。第四层是模糊匹配,命中可能直接返回一个错误答案,而且HTTP状态还是200。有网友把这点说得很直白:前三层改变的是计算量,第四层可能改变答案。

KV cache属于单个请求。prefill阶段,模型为每个prompt token在每一层算key和value并存下来。decode阶段只算新token,然后attend这些存下来的向量。query不缓存,原因是causal masking:一个token的query只在处理它那一步用一次,key和value会被后面的token反复读取。缓存让每个decode step从矩阵乘矩阵变成矩阵乘向量,但每步都要从HBM加载整个缓存,decode从compute-bound变成memory bandwidth-bound。缓存大小随token数线性增长。70B模型BF16、128K上下文,单请求缓存约40GB,接近4-bit权重的整个模型。GQA共享KV head、DeepSeek的MLA压缩latent、4-bit cache量化,都是在压这块内存。普通generate调用结束,缓存会被释放,所以20轮聊天到第20轮如果不保留缓存,前19轮会按全价重新prefill。

Prefix caching把缓存活到请求结束之后。vLLM默认按16个token一个block,用父block的hash加上当前block的token IDs做链式hash。查block时按顺序走,遇到第一个miss就停,后面的块不再尝试,因为后面的key依赖前面的hash。命中的block引用计数加一,防止被驱逐。尾部不足16个token的部分块不会被索引,每次都要重算。hash函数里的salt可以按租户传值,让相同文本在不同租户下生成不同key,避免共享物理块。RAG流量最容易踩坑:system指令、检索块、query拼在一起,检索块每次顺序可能不同,链式hash下两个请求可能完全不共享。LMCache实现了CacheBlend,在任意位置复用块缓存,只重算少量偏差最大的token,修复跨块attention和位置编码,首Token时间能比全量重算快2到3倍。

Prompt caching是托管模型厂商把prefix caching包装成价格表。Anthropic写缓存收基础输入价的1.25倍,读收0.1倍,OpenAI在当前模型上用同样的乘数。缓存对象仍然是KV tensor,仍然要求完全一致的渲染后前缀。cache_control标在你想覆盖的最后一个块上,它写入从请求开头到该块的缓存条目。用户消息在标记下面,每次变化,不能放进缓存区。usage里的cache_creation_input_tokens和cache_read_input_tokens能看出有没有命中。两个都是0,说明前缀没达到模型的最小可缓存长度,请求正常处理,不报错。Anthropic回溯查找旧写入最多20个block,两次调用之间塞超过20个block,前面的写入就出了范围。

Semantic cache走另一条路。它把prompt做embedding,在已存prompt里找最近邻,相似度超过阈值就直接返回存好的响应字符串。它省输入token也省输出token,但每个请求都要付一次embedding,命中与否都要付。Avi Chawla给的例子里,How do I reset my password和How can I reset my password相似度0.961,是真正的改写。Is the API rate limited和Is the API not rate limited相似度0.952,差一个否定,答案应该相反。两个分数只差不到0.01。阈值调高,命中率掉;阈值调低,自信的错误答案变多。有网友提到,semantic cache最麻烦的是过期:同一个问题隔四小时答案会变,embedding看不出时间差异,时间敏感内容要在相似度检查前加硬TTL。还有网友指出,工具调用如果取有状态数据,semantic cache不适用。

生产里还有几个容易忽略的点。变量内容要放prompt后面,时间戳、请求id、用户名放在系统提示开头,会让后面每个块都失效。工具schema的顺序也会影响缓存,重排一次可能全冷。Anthropic那边切换web search、citations、thinking config或tool_choice,会重写prompt文本,下游块跟着失效。A/B测试两个reasoning effort,等于把缓存分成两半。总结历史会重写前缀,下一次调用按冷token付费;原地截断工具输出能保持前缀字节一致。缓存条目按模型绑定,路由到更便宜的模型仍然要冷prefill全部历史。想知道两个prompt从哪里开始不匹配,直接比较token IDs,不要只看日志里的文本。Avi Chawla的示例里,turn 1没有指定system message,chat template填了模型默认值,两个prompt在index 3就分叉,缓存无法复用。

来源:Avi Chawla的文章 KV, Prefix, Prompt and Semantic Caching in LLMs, clearly explained,推文 Daily Dose of Data Science,CacheBlend实现 LMCache。

这四层缓存里,哪一层让你调试时间最长?

发布时间: 2026-10-29 19:30