1、项目概况
部署环境
硬件平台:Jetson Orin Nano Super 8GB,CPU与GPU共用统一内存,内存资源有限。
推理服务:llama‑cpp 项目的 llama‑server HTTP推理服务,源码为最新版本本地编译。
测试模型:先后使用通义千问Qwen2.5‑1.5B‑Instruct纯文本大模型、Qwen2.5‑VL‑3B‑Instruct视觉‑语言多模态模型。
业务场景
无状态定时推理任务;上层应用每隔10秒发送一次独立推理请求;每次请求使用全新会话编号,不存在多轮对话、上下文复用的需求。
测试方案:长时间压力测试,循环访问HTTP推理接口,观察内存运行状态。
2、故障现象
- 模型刚启动时内存占用正常,程序无报错;
- 随着推理请求不断执行,内存占用持续单向上升,不会自动回落释放;
- 最长运行约1小时后进程触发内存溢出,被Linux系统OOM‑Killer强制终止;
- 先后排查上层调用代码,确认应用程序不存在内存泄漏;更换LLM、VLM两种不同模型,故障现象完全一致,说明故障和模型文件本身无关;
- 问题初期,误判为llama‑cpp底层框架存在代码内存泄漏缺陷。
3、根因分析
经过定位排查,本次故障并非llama‑cpp代码Bug或内存泄漏,而是llama‑server默认的会话缓存策略,不适用于8GB低内存边缘设备。
参数说明
--cache‑ram:该参数用于设置服务器保存已经结束会话的KV缓存 最大内存容量,单位为MB。
该缓存的作用:当客户端复用同一个会话ID发起多轮对话时,可以直接读取缓存内的KV数据,跳过重复计算,提升对话响应速度。
llama‑server默认配置:--cache‑ram 8192,默认预留最大8GB内存用于存放历史会话缓存。
冲突产生原因
- Jetson设备整机内存仅8GB,如使用Docker部署,容器内存上限同样被限制为8GB;
- llama‑server程序启动后基础常驻开销大约450MB;
- 当前业务属于无状态一次性推理:每次请求生成全新会话,推理结束后就会产生一条已完成会话的缓存条目;
- 会话缓存条目不断累积,内存占用持续上涨,直至触达硬件或容器内存上限,最终触发OOM。本次压力测试下内存上涨速度约为1GB/分钟;
- 默认8GB缓存配置,理论上至少需要为llama‑server预留9‑10GB可用内存才能稳定运行;8GB边缘设备硬件条件无法满足该要求。
缓存工作机制(原理补充)
--cache-ram 对应的是 llama-server 的会话缓存 / prompt cache(由上游 PR #16391 引入),完整生命周期如下:
- 推理阶段:模型为输入 token 逐层计算 KV(Key/Value)中间状态,这是注意力计算的必需数据;
- 请求结束:该序列的 KV 状态被序列化后存入 host 内存缓存池,登记为一条"已完成会话"条目------这正是"每次推理都会新增一条缓存"的来源;
- 命中复用 :后续请求按 prompt 前缀匹配查找缓存,命中则直接载入 KV 状态、跳过整段重算,显著降低首 token 延迟(TTFT)。同一会话多轮对话、共享 system prompt 的业务是该缓存的目标场景;
- 容量控制 :缓存池总量由
--cache-ram约束,默认 8192MB。
无状态业务下该机制退化为"只进不出":每次请求全新 prompt → 与既有条目前缀零重叠 → 永不命中 → 每请求固定新增一条条目,且没有任何复用路径消化它们。
增长速率量级估算 (与实测吻合):条目大小与上下文长度成正比------纯文本短请求单条约 MB 级;VLM 场景一张图折算 1000+ token,单条目序列化 KV 可达百 MB 级 。按本业务每 10 秒 1 次图片请求计算:6 条/分钟 × ~150MB/条 ≈ 0.9GB/分钟,与实测 ~1GB/分钟一致。
为什么默认上限在低内存设备上形同虚设
8192MB 默认值本质是进程内自留额:驱逐逻辑在缓存池超过上限时才启动。但在 8GB 设备上:
基础常驻 ~450MB + 模型权重(VLM ~4.3GB)+ 缓存增长 → 距 8GB 总量仅剩 ~3GB 缓存空间
缓存远未触及 8192MB 上限时,内核 OOM-Killer 已经先行杀死进程 ------进程内的上限机制根本没有执行机会。此外,旧版本的驱逐条件更宽松(仅在内存分配失败 std::bad_alloc 时触发,Linux overcommit 机制下几乎不会发生,见上游 issue #22629);2026-07 合并的 PR #25070 已改为严格强制上限,但只要"默认上限 ≥ 设备可用内存",结论不变:显式设置小上限或 0 仍是低内存设备的必需配置。
为什么症状酷似内存泄漏
| 内存泄漏特征 | 本故障表现 | 是否吻合 |
|---|---|---|
| 随请求线性上涨 | 每请求新增一条缓存条目 | 吻合 |
| 停载后不回落 | 条目保留在池中等待复用,进程不主动释放 | 吻合 |
| 换模型/换调用方依然复现 | 与上层应用无关,是服务端默认行为 | 吻合 |
| 泄漏检测工具报出泄漏 | heaptrack 等工具显示分配均正常释放------缓存是"活数据" | 不吻合(关键鉴别点) |
最后一项是关键鉴别依据:真泄漏是"丢失指针、无法释放"的孤儿内存;缓存是"持有指针、等待复用"的活数据。当"RSS 单向上涨 + 检测工具无泄漏 + 单一参数可解释全部增长量"三者同时成立时,应优先怀疑配置类"合法占用",而非代码缺陷。
社区佐证
该故障模式在 llama.cpp 社区已有同型案例(如 issue #25740:报告者最终通过 heaptrack 自行定位为 prompt cache 按请求累积,而非代码泄漏)。相关讨论中一位用户的补充说明与本案例高度一致,要点如下:
在低内存环境(8GB 主机,或限内存的容器)运行 llama.cpp 且未调整
--cache-ram时,缓存几乎随每个请求增长,直至触及默认 8192MB 上限------若设备根本没有这么多余量,数小时内必然 OOM(其实测约 1GB/分钟)。同时满足"没改过--cache-ram"和"没有为 llama.cpp 预留 9~10GB 内存"却频繁 OOM 的,大概率就是这个原因,不必再花几天去排查一个并不存在的巨大内存泄漏。
4、解决方案
方案一(推荐,适配无状态推理业务)
在启动命令中添加参数:--cache‑ram 0,关闭已结束会话的持久缓存功能。
生效逻辑:单次推理任务完成后,该请求对应的KV缓存内存立即释放,不再产生缓存残留堆积,彻底解决内存持续上涨问题。
修改后完整启动命令示例
bash
# Qwen2.5‑VL‑3B 视觉语言模型
./build/bin/llama-server \
-m ~/Desktop/workplace/models/qwen_vl_gguf/qwen2.5-vl-3b-instruct-q4_k_m.gguf \
--mmproj ~/Desktop/workplace/models/qwen_vl_gguf/Qwen2.5-VL-3B-Instruct-mmproj-f16.gguf \
-ngl 99 \
--flash-attn on --cache-type-k q8_0 --cache-type-v q8_0 \
--ctx-size 4096 \
--keep -1 \
--mlock \
--parallel 1 \
--host 0.0.0.0 \
--port 8080 \
--cache-ram 0
bash
# Qwen2.5‑1.5B 纯文本模型
./build/bin/llama-server \
-m ~/Desktop/workplace/models/qwen2.5-1.5b-instruct-q4_k_m.gguf \
-ngl 99 \
--host 0.0.0.0 --port 8080\
-c 4096 \
--temp 0.0 --top-p 1.0 \
--jinja \
--cache-ram 0
方案二(业务需要多轮对话时选用)
不将缓存设置为0,手动指定一个较小的缓存上限,例如--cache‑ram 512,即限定历史会话缓存最大占用512MB内存。在内存稳定性与多轮对话性能之间取得平衡,不要使用默认8GB缓存配置。
5、方案取舍与风险说明
--cache‑ram 0会完全关闭历史会话缓存,无法再通过旧会话ID快速恢复多轮对话。
✅ 适用场景:定时推理、一次性独立请求、无状态边缘推理任务(本次业务场景)
❌ 不适用场景:长会话人机聊天、频繁复用会话ID的对话业务
6、部署经验总结
- 在8GB、16GB低内存边缘硬件、设置内存限额的Docker容器部署 llama‑server,禁止直接使用默认参数启动,务必评估
--cache‑ram缓存上限; - 无状态推理场景优先开启
--cache‑ram 0; - 遇到内存缓慢上涨、长时间运行后OOM崩溃,不要直接判定框架存在内存泄漏,优先排查会话缓存配置;
- llama‑server默认8GB会话缓存是面向大内存服务器设计,该默认值并不适合嵌入式边缘设备。