TGI 模型预热后首次请求延迟仍高的 prompt_cache 命中率排查思路

文章导读
先判断一个关键点:预热后首次请求延迟仍高,未必是 prompt_cache 没生效,而是缓存命中的条件没满足。TGI 的 prompt_cache(也常称 prefix cache)依赖完整的输入前缀、精确的 token 序列和位置信息。只要输入文本与预热时不完全一致,哪怕少一个空格,也会造成整条前缀无法复用。排查的目标不是“提高缓存命中率”,而是先确认“本次请求到底有没有命中缓存”,再沿着未命中
📋 目录
  1. Ⅰ 先确认“未命中”是事实还是猜测
  2. Ⅱ 检查预热请求的“可复用前缀”是否够长
  3. Ⅲ 检查缓存容量与淘汰策略
  4. Ⅳ 预热逻辑本身要覆盖“首包延迟”的完整场景
  5. Ⅴ 命中后仍延迟高的排查边界
  6. Ⅵ 快速验证清单
A A

先判断一个关键点:预热后首次请求延迟仍高,未必是 prompt_cache 没生效,而是缓存命中的条件没满足。TGI 的 prompt_cache(也常称 prefix cache)依赖完整的输入前缀、精确的 token 序列和位置信息。只要输入文本与预热时不完全一致,哪怕少一个空格,也会造成整条前缀无法复用。排查的目标不是“提高缓存命中率”,而是先确认“本次请求到底有没有命中缓存”,再沿着未命中的原因逐层查下去。

处理方向:用服务日志或监控接口确认首次请求的 cached tokens 数量;若为 0,则重点比较请求文本与预热文本的 token 序列是否完全一致,同时确认预热请求是否真正进入缓存、缓存是否因显存限制被淘汰。若命中但延迟仍高,再去查生成长度、调度排队或后端算子。不要在没有日志确认前改服务参数。

先确认“未命中”是事实还是猜测

TGI 通常会在日志或 metrics 中反映前缀命中的 token 数,例如 cached_tokens、prompt_cache_hit_rate 之类的字段。如果部署时没有暴露,可以用一个带 max_tokens=1 的请求强制短生成,同时观察延迟和日志中的缓存指标。

# 替换为你的 TGI 服务地址和模型路径
curl http://127.0.0.1:8080/generate \
  -H 'Content-Type: application/json' \
  -d '{"inputs":"你的固定系统提示词,后面接一个测试句子","parameters":{"max_new_tokens":1}}'

执行后直接从 TGI 进程日志里查本次请求返回的缓存 token 数。如果日志没有,可以临时把服务日志级别调到 debug,但注意生产环境不要长时间开。

检查预热请求的“可复用前缀”是否够长

prompt_cache 的粒度是 token 级别的 KV cache。一次预热请求只会缓存“当前请求全部 prompt token”对应的 KV。后续请求要从第一个 token 开始完全一致,才能逐段命中。下面这些日志能直接解释为什么没命中:

  • 预热时只发了短文本,例如“你好”,而真实请求带很长的系统提示词,两者公共前缀为空。
  • 预热请求带了随机参数、时间戳、乱序的 key,导致 token 序列每次不同。
  • 预热请求虽然文本一样,但生成参数不同,TGI 的内部分支可能按参数切换缓存策略,需要以实际代码为准。
  • 普通文本在分词后看起来“一样”,但某些 Unicode 字符被规范化,比如全角空格和半角空格,token 序列并不一致。

可以做一个最小化验证:把真实请求的 inputs 原封不动先发一次,再立刻发第二次,看第二次是否命中。如果第二次命中,说明缓存机制本身没问题,问题出在预热内容和正式请求不一致。如果第二次也不命中,就要看配置和显存。

检查缓存容量与淘汰策略

即使前缀一致,缓存也可能存不下来。TGI 的 prompt_cache 一般位于 GPU 显存或 CPU 内存,有统一的上限。预热后如果立刻有多个大请求,或者预热本身触发了较长的生成结果,旧缓存可能被覆盖。

可以从两个方向确认:

  • 看服务启动时的缓存大小配置,例如 `--max-cache-size` 或 `--prefix-cache-size`,确认是否设得太小。
  • 观察并发请求数量。并发越高,前缀缓存被拆散的概率越大;有的实现按 request 分配物理块,连续多个不同前缀请求会互相挤占。

这里没有通用调参公式。建议先降低同时在线请求数或分开部署前后台流量,确认缓存能保留住,再逐步恢复并发。

TGI 模型预热后首次请求延迟仍高的 prompt_cache 命中率排查思路

预热逻辑本身要覆盖“首包延迟”的完整场景

如果你的预热只是发送一次较短的文本,比如故意让模型输出几个 token,那么缓存只会覆盖这段短输入。真实请求的生产环境往往包含大段 system prompt、few-shot 示例和用户历史消息,一次短预热根本覆盖不到。

更好的做法是把真实请求中最常出现的公共前缀抽出来,作为静态字符串拼接成一次预热请求。例如:

# 预热请求的 inputs 应该与真实请求结构一致
system = "你是一名运维专家,请依据以下上下文回答..."
context = "设备日志片段..."
user = "请分析故障原因"

预热请求 = system + context + "temperature=0.7" + user
真实请求 = system + context + "温度参数占位符" + user

注意:如果真实请求中 temperature 这类参数是通过 JSON 传入而不是拼在文本中,那就不要拼进去;缓存只认 token 文本。预热时要把可能变化的尾部拆掉,让公共前缀尽量长。真实请求前部保持和预热完全一致。

如果系统提示词里已经有日期、随机 ID,这些字段会直接导致前缀失效。需要先做一次“前缀稳定性改造”,比如把动态内容挪到尾部,或者使用固定的占位符,在请求前替换。

命中后仍延迟高的排查边界

如果日志确认 cached_tokens 已经覆盖了大部分 prompt,但首 token 延迟仍很高,问题就不在缓存上,需要转向以下方向:

  • 模型是否真的被加载完毕,有没有发生首次 forward 时的 CUDA kernel 初始化或显存分配。
  • 请求是否到了 TGI 调度器,还是排在了另一个长生成请求之后。
  • 服务是否启用了动态批处理,批处理队列长度是否因并发而堆积。
  • 输入 prompt 本身长度过长,即使前部都命中,尾部新增的 token 仍要走完整 forward。

最后一种情况常被忽略。prompt_cache 只能节省“已缓存部分”的 prefill 计算,新增的尾部 token 仍然要参与计算。如果尾部很长,首 token 延迟会按尾部长度线性增长,这不能靠预热解决。需要结合业务场景缩短尾部或拆分请求。

快速验证清单

  1. 用真实请求文本连续发两次,确认第二次日志中 cached tokens 是否大于 0。
  2. 比较预热请求和真实请求的 byte 级 hexdump,排除不可见字符差异。
  3. 检查服务日志是否有缓存淘汰记录,或 metrics 中缓存占用是否接近上限。
  4. 临时降低并发,重复预热后立即发请求,看是否命中。
  5. 确认首次请求的生成长度是否超过预期,长生成会掩盖 prefill 命中效果。

这些检查都不需要改动代码。按顺序做一遍,通常能定位到是“前缀不一致”、“缓存被挤掉”还是“尾部计算量”造成的延迟。如果最后确认是缓存机制实现差异,需要结合当前 TGI 版本的源码注释来调整部署参数,避免盲目套用旧配置。