
大模型前缀缓存技术,有望将服务成本降低90% 原创
大模型应用程序中的提示词重复率高达70%。前缀缓存机制能够将推理成本降低达90%,显著优化性能并节约资金。
是不是总感觉ChatGPT响应缓慢?
大家可能没有留意,大模型应用程序的提示词重复率高达70%,问天气、问翻译和问节日安排的内容大量出现,且每次都要消耗算力进行处理。这样的情况在分布式集群的各节点上被无数次放大,白白烧掉宝贵的能源和金钱。
为此,Anthropic日前详细介绍了如何利用提示词缓存技术将推理成本降低90%。其实不少开源大模型运行时(包括vLLM、TRT-LLM和SGLang等)都拥有自动前缀缓存(也称上下文缓存)功能,负责将相同前缀请求中的输入提示词自动缓存起来。
前缀缓存的工作原理
为了更好地理解前缀缓存,我们先来聊聊大模型推理的工作原理。
推理过程在宏观上分为两个步骤:
- 通过正向传递处理给定的输入标记序列,即预填充阶段。
- 解码阶段,从首个token连续生成至最后一个token,且当前token依赖于上一token。
图一
由于此过程的自回归属性(即新token依赖于前一token),因此有效的内存管理非常重要。多数大模型会采取为中间状态保留KV缓存的做法。与简单提示词或语义缓存的不同之处在于,其不会将全文输入和输出保存在数据库内,因为这样就只有完全匹配(或者几乎完全相同的查询)才能立即命中缓存并收到响应。
在预填充阶段,在大模型处理token时会计算“注意力”,即每个token与其他token的关系。计算过程会为每个token生成键-值矩阵。如果不经任何KV缓存,那么模型每次回顾此前token时都需要重新计算这些矩阵。KV缓存在设计上只支持一次生成,即只会在生成一条输出的过程中捕捉中间状态。
如果有两条具有相同前缀的请求,该怎么处理?
KV缓存的基本思路启发并衍生出了前缀缓存,确保在生成包含相同前缀的提示词时给出不同的响应。简单类比一下,假设已经计算过2 * 6的结果,那么对于2 * 6 * 3 * 5 这个新问题,可以直接复用之前的答案,避免在序列中重复计算。
这对应用程序有何帮助?
我们可以使用以下最佳实践来充分发挥前缀缓存的优势:
为提示词结构设计策略
可以将系统提示词、基础指令或者共享上下文等常量元素放在提示词的开头(图二),从而为多条查询建立可复用基础。其他动态或特殊内容则可放在末尾。
图二
对请求进行重新分组
将共享通用结构/前缀的请求捆绑在一起(图三)。例如,在处理以常见问候语或称呼开头的多条客户查询时,可以尝试将它们分为一组,尽可能提高计算过程的缓存和复用率。
图三
监控缓存利用率
另外,需要注意跟踪缓存利用率。
包括命中率与未命中率:
- 找出哪些前缀比其他一般前缀更重要
- 识别缓存未命中的模式
依托这些见解,就能优化提示词结构以获得最佳性能。
简单示例
以下示例为当多条查询共享相同的上下文时,前缀缓存如何优化大模型推理。我们使用一份简单的员工数据库表,并对其中内容进行不同查询。
Python
import time
from vllm import LLM, SamplingParams
# A small table containing employee information
LONG_PROMPT = """You are a helpful assistant that recognizes content in markdown tables. Here is the table:
| ID | Name | Department | Salary | Location | Email |
|----|---------------|------------|---------|-------------|---------------------|
| 1 | Alice Smith | Engineering| 85000 | New York | alice@company.com |
| 2 | Bob Johnson | Marketing | 65000 | Chicago | bob@company.com |
| 3 | Carol White | Sales | 75000 | Boston | carol@company.com |
| 4 | David Brown | Engineering| 90000 | Seattle | david@company.com |
| 5 | Eve Wilson | Marketing | 70000 | Austin | eve@company.com |
"""
def get_generation_time(llm, sampling_params, prompts):
start_time = time.time()
output = llm.generate(prompts, sampling_params=sampling_params)
end_time = time.time()
print(f"Output: {output[0].outputs[0].text}")
print(f"Generation time: {end_time - start_time:.2f} seconds")
# Initialize LLM with prefix caching enabled
llm = LLM(
model='lmsys/longchat-13b-16k',
enable_prefix_caching=True
)
sampling_params = SamplingParams(temperature=0, max_tokens=50)
# First query - will compute and cache the table
get_generation_time(
llm,
sampling_params,
LONG_PROMPT + "Question: What is Alice Smith's salary? Your answer: Alice Smith's salary is "
)
# Second query - will reuse the cached table computation
get_generation_time(
llm,
sampling_params,
LONG_PROMPT + "Question: What is Eve Wilson's salary? Your answer: Eve Wilson's salary is "
)
运行以上代码,即可查询不同查询间的实际时间差异。第二条查询明显更快,因为其复用了缓存中的表上下文。具体时间将根据硬件和设置而有所浮动。
总结
前缀缓存是一项强大的大模型应用优化技术。实施上述最佳实践将帮助开发人员显著降低推理成本,且不致影响响应质量。参考示例也表明其操作难度极低,推荐大家马上在自己的应用程序中试一试。
原文标题:90% Cost Reduction With Prefix Caching for LLMs,作者:Mahak Shah
