先量热层复用率再决定要不要落盘——Colibrì 分层推理的适用边界

文章导读
先给判断口径:分层推理值不值得开,不取决于模型能省下多少内存,而取决于热层在一轮又一轮请求之间是否被反复读取。如果每轮问答都要把冷层从磁盘读回来,落盘换来的内存下降通常会在首字延迟上还回去;反过来,若热层长期驻留、冷层只在少数请求上被读到,分层才是净收益。所以动手前先做一件事:把各层的读取次数记下来,用读数代替“应该会缓存住”的猜测。统计数据一般只需要一轮完整问答或连续几轮的日志,不需要压测环境。
📋 目录
  1. Ⅰ 统计一轮问答里各层被读取的次数
  2. Ⅱ 用读盘次数与首字延迟判断落盘是否值得
  3. Ⅲ 把热层钉在内存里的配置写法与验证
  4. Ⅳ 哪些场景不适合开启分层
A A

先给判断口径:分层推理值不值得开,不取决于模型能省下多少内存,而取决于热层在一轮又一轮请求之间是否被反复读取。如果每轮问答都要把冷层从磁盘读回来,落盘换来的内存下降通常会在首字延迟上还回去;反过来,若热层长期驻留、冷层只在少数请求上被读到,分层才是净收益。所以动手前先做一件事:把各层的读取次数记下来,用读数代替“应该会缓存住”的猜测。统计数据一般只需要一轮完整问答或连续几轮的日志,不需要压测环境。

开启 Colibrì 分层推理前,先用日志或系统 IO 统计量出每个请求里各层的读取次数与来源(常驻内存或磁盘)。热层在多轮请求中被重复读取、冷层回读稀疏,才考虑落盘;若一轮请求就把各层各读一遍、或磁盘本身很慢,退回全内存加载更快,也更省调参成本。判断依据是读盘次数与首字延迟的并列对比,不是内存降了多少。

统计一轮问答里各层被读取的次数

采集分两条路,建议两条都开,交叉对照。应用侧的日志最直接:让推理进程在每层加载或读取时打一行结构化日志,至少包含请求 ID、层号、来源(resident / disk)、读取耗时。示意字段如下,字段名按自己代码里的命名来:

{"req":"q-1024","layer":7,"source":"disk","read_ms":38,"stage":"prefill"}
{"req":"q-1024","layer":7,"source":"resident","read_ms":0,"stage":"decode"}

系统侧用来验证日志是否可信:iostat -x 1 看承载模型权重的那个设备的 r/s 与 r_await;需要按进程拆分时用 pidstat -d -p <pid> 1,或读 /proc/<pid>/io 里的 read_bytes 前后差值。统计窗口定义为“一次请求从进入排队到最后一个 token 输出”,而不是固定时间窗,因为分层的代价集中在单次请求的路径上。若要判断跨请求复用,再取连续 5~10 轮同一类问题,看第二轮起 source=disk 的行数是否明显下降。

把一轮的读数记成这样一张表,比看总内存有用得多:

层号读取次数来源分布平均读取耗时备注
0-11(热层)每轮 24 次全部 resident—未发生回读
12-27(冷层)每轮 16 次disk 16 / resident 0与磁盘型号相关每轮都回读

表里的“读取次数”是判断复用率的分子:同一层在连续 N 轮里被读取的次数除以 N,得到每轮回读频次。这个频次接近 0,说明该层适合放在磁盘;频次稳定大于 0 且每层都要读,说明这层实质上还是热数据,落盘只是在给自己制造 IO。

用读盘次数与首字延迟判断落盘是否值得

收益判断要并列看两列数,而不是只看驻留内存。第一列是常驻内存下降幅度,用 /proc/<pid>/status 的 VmRSS 或 ps -o rss= -p <pid> 在请求间隙取样;第二列是首字延迟,用客户端打点或服务端 accept 到首个 token 的时间差。两列要来自同一组问题、同一台机器、同样的并发,否则对比没有意义。

判为不划算的情形通常长这样:常驻内存确实降下来了,但每轮都出现冷层读盘,首字延迟的增量与读盘耗时增量基本吻合,说明省下的内存是拿请求路径换的。一个保守的临界描述是——当一轮请求里 source=disk 的读取次数接近或超过该轮总层读取次数的一半,或者读盘累计耗时占到该轮 prefill 阶段耗时的可观比例时,就不建议继续开分层,先回到全内存加载做基线。反过来,若连续多轮里只有第一轮出现读盘、后续轮次读盘次数趋近于零,且内存占用稳定在可接受范围,分层带来的复杂度才是可付的。

先量热层复用率再决定要不要落盘——Colibrì 分层推理的适用边界

不要用单次请求做结论。至少取多轮同分布请求,保留一份“全内存”和一份“分层”的两列对照记录,哪个口径变差、变差在哪一层,要能指到具体层号。

把热层钉在内存里的配置写法与验证

如果判断可以开分层,下一步是保证热层不被换出。多数实现会提供热层数量上限或驻留预算一类的配置,字段名以仓库文档为准,下面是示意写法,替换成实际字段即可:

layers:
  hot:
    count: 12          # 示意:常驻内存的层数
    pin: true          # 示意:禁止换出
  cold:
    path: /data/model  # 示意:冷层权重目录
    preload: false

配置后要验证两件事:热层是否真的常驻,以及回读次数是否随之下降。改完重启进程,先记录 VmRSS 基线,再连发几轮问题,回看日志里热层是否出现 source=disk;同时用 pidstat -d 确认读盘速率没有随轮次持续上升。若热层仍被换出,检查是否被系统页缓存策略或其他进程挤占,必要时用 cgroup 的内存上限把推理进程的驻留范围框住,而不是靠反复调大层数。

哪些场景不适合开启分层

以下情形建议直接退回全内存加载,把调参时间省下来:

  • 层数本来就少。模型层数不多、单层权重不大时,分层省下的内存有限,却引入了一次额外的读取分支和配置面。判断依据是:即便全部层常驻,VmRSS 也在机器内存余量之内。
  • 单次请求只把各层走一遍。没有跨请求复用、每个请求都是一次性的长文本生成,热层概念就失去了意义,日志里每层每轮都是 disk 命中。
  • 权重放在慢盘上。机械盘、网络挂载、共享存储的随机读延迟会把首字延迟拉得很明显,这类介质上分层基本只剩成本。可以先量一次 r_await,再决定要不要试。
  • 内存其实够用。为了“架构上更优雅”而上分层,通常会在后续排障里付出更多时间。

退回的做法不复杂:把热层数量配成等于总层数、或直接关掉分层开关,让冷层目录不再被访问;重启后用同一组问题复测首字延迟与 VmRSS,确认延迟回落到全内存基线、日志里不再出现 source=disk 的读取行。保留这份基线记录,下次想再试分层时可以直接对比,不必重新摸索。