|
译者 | 核子可乐
审校 | 重楼
是不是总感觉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 这个新问题,可以直接复用之前的答案,避免在序列中重复计算。
这对应用程序有何帮助?
我们可以使用以下最佳实践来充分发挥前缀缓存的优势:
为提示词结构设计策略
可以将系统提示词、基础指令或者共享上下文等常量元素放在提示词的开头(图二),从而为多条查询建立可复用基础。其他动态或特殊内容则可放在末尾。
图二
对请求进行重新分组
将共享通用结构/前缀的请求捆绑在一起(图三)。例如,在处理以常见问候语或称呼开头的多条客户查询时,可以尝试将它们分为一组,尽可能提高计算过程的缓存和复用率。
图三
监控缓存利用率
另外,需要注意跟踪缓存利用率。
包括命中率与未命中率:
- 找出哪些前缀比其他一般前缀更重要
- 识别缓存未命中的模式
依托这些见解,就能优化提示词结构以获得最佳性能。
简单示例
以下示例为当多条查询共享相同的上下文时,前缀缓存如何优化大模型推理。我们使用一份简单的员工数据库表,并对其中内容进行不同查询。
Pythonimport timefrom vllm import LLM, SamplingParams# A small table containing employee informationLONG_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 enabledllm = 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 tableget_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 computationget_generation_time( llm, sampling_params, LONG_PROMPT + "Question: What is Eve Wilson's salary? Your answer: Eve Wilson's salary is ")
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
运行以上代码,即可查询不同查询间的实际时间差异。第二条查询明显更快,因为其复用了缓存中的表上下文。具体时间将根据硬件和设置而有所浮动。
总结
前缀缓存是一项强大的大模型应用优化技术。实施上述最佳实践将帮助开发人员显著降低推理成本,且不致影响响应质量。参考示例也表明其操作难度极低,推荐大家马上在自己的应用程序中试一试。
原文标题:90% Cost Reduction With Prefix Caching for LLMs,作者:Mahak Shah |
|