长上下文把内存吃满时,先别急着加内存条,也别默认分层存储没生效。把内存拆成两部分看:一部分是模型权重加运行时基础占用,空载启动后就基本固定;另一部分是上下文带来的增长,随 token 数上升。分层存储主要搬的是权重和冷数据,如果增长主要来自上下文,分层能解决的有限;如果空载常驻已接近物理内存上限,分层才有发挥余地。下面这套测量顺序就是为了先把这两块分开。
先做两步:空载记录常驻内存,再按上下文长度阶梯记录峰值,看增量落在哪一侧。增量随上下文近似线性上升,优先压上下文上限;空载常驻已贴近物理内存,再固定上下文长度只切分层配置做对照,同时记录峰值内存、磁盘读和首字延迟。分层开着但首字延迟明显变差、每 token 耗时也上涨,说明是在换页而不是在省内存,这条路要退回去。
把权重占用与上下文增长分开测量
先测空载。用最短的提示词启动 Colibrì,等第一个 token 出来、进程稳定几秒后再记录一次内存。这一步记的是权重加运行时的基础开销,后面所有对比都以它为基准。命令按你的运行方式选,容器里跑就看 cgroup,裸机就看进程 RSS,走 GPU 就同时记显存。
# 进程常驻内存(替换成实际 PID)
ps -o pid,rss,vsz,comm -p <PID>
# 容器内看 cgroup
cat /sys/fs/cgroup/<group>/memory.current
cat /sys/fs/cgroup/<group>/memory.peak
# 走 GPU 时同步记录显存
nvidia-smi `--query-gpu`=memory.used `--format`=csv -l 1
然后测增长。用同一份测试请求,只改上下文长度这一项,按 2k、4k、8k、16k、32k 这样的阶梯往上走(具体档位以你的模型上限为准),每一档都等首 token 返回、内存稳定后记峰值。填入下表,差值就是上下文那一侧的真实开销。
- 上下文长度 / 峰值常驻内存 / 相对空载的增量 / 是否触发 OOM 或 swap
- 相邻两档的增量除以长度差,粗略看成“每千 token 的内存成本”
解读只有两种走向。若增量随上下文近似线性上升,而空载常驻离物理内存还有明显余量,主因是上下文,加内存条只能把上限往后推一段,效果有限。若空载常驻已经占掉大半物理内存,随上下文增长的增量相对小,主因是权重常驻,分层存储才有值得试的空间。中间地带两种都在起作用,需要按第 2 节做对照后再判断。
固定上下文长度只改分层配置做对照
对照的关键是把变量固定:同一上下文长度、同一 batch、同一量化配置、同一份请求,只关掉或打开分层。配置字段名以你本机的配置文件与 `--help` 输出为准,下面是骨架,不是官方字段表。
# 示例配置骨架,字段名请对照实际配置文件
storage:
tiering:
enabled: true # 对照组改成 false
offload_dir: /mnt/nvme/colibri-offload
cache_size: <两次运行保持同一值>
inference:
max_context: 8192 # 两次运行必须一致
batch_size: 1
每次运行记录三项:峰值常驻内存、磁盘读次数、首字延迟。磁盘读可以用 pidstat 或 /proc 里的 io 计数取差值。
pidstat -d 1 -p <PID>
# 或取前后差值
cat /proc/<PID>/io
有两个容易干扰结果的地方:第一次运行会把模型文件带进 page cache,也会建立 mmap,建议同一配置跑两遍,只取第二遍的数;切换配置前确认没有别的进程在读同一块盘,否则磁盘读次数会算到 Colibrì 头上。对照结果通常呈现三种形态:峰值内存基本不降、磁盘读也不多,说明分层没真正接管冷数据,先回去检查 offload_dir 是否真的落到了独立盘、阈值是否配得过高;峰值内存下降但磁盘读明显上升,说明分层生效了,接着做第 3 节的延迟判断;峰值内存下降、磁盘读变化不大,这是比较理想的情况,可以继续用。
观察分层生效后延迟是否可接受
省下来的内存是不是用响应速度换的,只能靠时间戳回答。用流式请求,记下发请求的时刻和每个分片到达的时刻,首字延迟取第一个分片的时间差,每 token 耗时取“最后一个分片时间减第一个分片时间”再除以输出 token 数。客户端不是这套接口也没关系,用你自己的调用方式打时间戳即可。
curl -s -N http://127.0.0.1:<port>/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"colibri","stream":true,"max_tokens":128,"messages":[{"role":"user","content":"<固定测试文本>"}]}' \
| while IFS= read -r line; do printf '%s %s\n' "$(date +%s.%N)" "$line"; done
判据分两层看。如果只是首字延迟变长、每 token 耗时接近不开启分层的基线,说明成本集中在第一次装载,适合对首字不敏感、对总时长敏感的批处理场景。如果每 token 耗时也一起变长,说明推理过程中一直在向磁盘取数据,这属于换页,不是分层在省内存,继续调大上下文只会更糟。自查线可以先按“首字延迟不超过不分层基线的两倍、每 token 耗时不超过基线”来定,超过这条线就把它当成需要处理的信号,回到第 4 节。这些倍数是自查用的经验线,不是标准值,具体要结合你的交互容忍度确认。
分层顶不住时的两条路
第一条是压上下文上限。找到 max_context 或同类字段,从当前值往下调一档,保持其它配置不动;同时检查调用侧有没有在硬塞长 prompt,比如检索条数、历史轮数、系统提示长度,这几处往往比模型参数更容易失控。超出上限的部分改用滚动窗口或摘要替代全量保留,注意摘要本身也要占上下文,别把省下的又花回去。验证方式是把第 1 节的阶梯测量重跑一遍,确认峰值回落到目标范围,再用第 3 节的方法确认首字延迟没有变差。适用前提是业务能接受更短的可见上下文,或者能靠摘要保住关键信息;如果任务本身要求逐字保留全量材料,这条路走不通。
第二条是加物理内存。加之前先确认内存确实是瓶颈:查 dmesg 有没有 OOM kill 记录,用 free 和 vmstat 看 si/so 是否长期非零,swap 被大量读写只说明内存在被挤压,不能当成解决方案。确认之后,用第 1 节记下的峰值常驻和物理内存的余量估算需要加多少,装好后重跑第 2 节的固定上下文对照,看峰值余量是否足够、是否还需要保留分层。适用前提是空载常驻本身就很大,或者上下文在业务上确实压不下来;如果上下文还能压缩,先压上下文比加内存更省事。