连续对话场景下内存持续上升,首先要区分“对话历史变长导致的内存占用上升”和“真正的内存泄漏”。两种问题处理方式完全不一样,混在一起排查会浪费大量时间。建议先做一轮可测量的观察,再决定改代码还是改调用方式。
连续对话内存增长通常有两个来源:对话上下文长度增长,或底层 llama.cpp 上下文对象没有被正确释放。排查先把两者分开:记录 token 数和 RSS 变化,再检查每个请求是否新建 Llama 实例。token 数同步增长是提示词管理问题;token 数不变而 RSS 持续上升才是真泄漏。
先量化:token 数在涨,还是内存单独在涨
在代码里同时输出“本次请求的 token 数”和“进程 RSS”。llama-cpp-python 的 Llama 类在返回结果里通常会带 usage 字段,包含 prompt_tokens 和 completion_tokens。把 usage 中的 prompt_tokens 之和当作对话上下文长度;用 psutil 读取进程内存。连续多轮后,如果 prompt_tokens 和 RSS 同步上升,说明是提示词越积越长,不是泄漏;如果 prompt_tokens 稳定,RSS 却一直升高,才需要继续往下查。
import os, psutil, time
from llama_cpp import Llama
llm = Llama(model_path="...", n_ctx=4096)
def process_memory_mb():
return psutil.Process(os.getpid()).memory_info().rss / 1024 / 1024
messages = []
for turn in range(10):
user_input = f"turn {turn}: " + "x" * 500
messages.append({"role": "user", "content": user_input})
resp = llm.create_chat_completion(messages=messages)
messages.append(resp["choices"][0]["message"])
print(turn, resp["usage"]["prompt_tokens"], process_memory_mb())运行后用输出画一条线。prompt_tokens 线性增长,RSS 也线性增长,二者趋势一致,就先不要怀疑底层绑定泄漏,优先处理上下文截断。
检查 Llama 实例:有没有被反复创建
内存泄漏最容易出现在“每个会话新建 Llama 对象,旧对象却没有显式释放”的写法里。llama-cpp-python 的 Llama 对象在初始化时会加载模型并分配内存;如果放在请求处理函数内新建,函数退出后对象应该被销毁,但线程、回调或异常处理可能让引用仍然存在。建议在模块加载时只创建一次实例,并在所有会话中复用。
# 不要这样做:每个请求都新建实例
def on_request(messages):
llm = Llama(model_path="...")
return llm.create_chat_completion(messages=messages)
# 建议这样做:进程级单例
_llm = None
def get_llm():
global _llm
if _llm is None:
_llm = Llama(model_path="...", n_ctx=4096)
return _llm如果必须每次请求新建实例,一定要在结束后调用 del llm,必要时再调用 gc.collect(),并确认没有其他变量还引用这个对象。可以用 weakref 或 gc.get_objects() 帮助找引用链,但不要指望垃圾回收能解决所有问题,先把实例生命周期改对。
带 context 的连续对话:问题多半在上下文管理
llama-cpp-python 的 create_chat_completion(messages=messages) 会把你传入的整段消息列表作为输入。如果你把 history 一直追加,prompt_tokens 会一直增长。超过 n_ctx 后,行为取决于后端:有的直接报错,有的会截断,有的可能保留部分消息但内存仍然在涨。即使模型支持长文本,内存占用也会随输入长度增加,这不叫泄漏,需要按“上下文窗口管理”来处理。
常见的保守处理方式有三种:第一,保留最近 N 轮消息,更早的压缩成摘要;第二,每次只传最近 K 条消息,让历史由外部存储维护;第三,限定最大 token 数,超了就触发重置或裁剪。你可以先用最简单的方式验证:每轮只传最后 4 条消息,观察内存是否还在持续上升。
history = []
def add_and_chat(user_text):
history.append({"role": "user", "content": user_text})
# 只保留最近 6 条消息,验证内存变化
recent = history[-6:]
resp = get_llm().create_chat_completion(messages=recent)
history.append(resp["choices"][0]["message"])
return resp改成限制消息条数后,如果 RSS 曲线趋于水平,说明原来就是消息列表无限增长造成的。此时不要继续在内存泄漏上找原因,而是设计对话历史的存储和裁剪策略。
参数层面:n_ctx、n_threads 和 batch 的影响
除了对话历史,n_ctx 本身也会影响内存占用。llama.cpp 在初始化时会按 n_ctx 分配 KV cache,n_ctx 设置得越大,基础内存就越高。如果进程已经存在持续增长,先把 n_ctx 调小观察,比如从 8192 改成 4096,确认基线内存是否下降。这不能解决泄漏,但能帮助判断是不是上下文窗口设置过大。
另外要注意 n_threads、n_batch 等参数不会导致无上限增长,但会影响整体资源占用。排查时尽量保持这些参数不变,一次只改一个变量。如果使用 OpenAI 兼容接口里的 max_tokens,它限制的是生成长度,不影响 prompt 长度,不要混淆。
验证清单与收尾动作
排查到这一步,至少可以给出一个保守结论:先用“单例 Llama + 固定消息条数 + 记录 prompt_tokens”跑 20 轮对话,如果内存不再持续上升,就说明问题出在调用方式,而不是绑定库本身。如果已经采用这些做法仍持续上升,需要进一步检查系统层的内存分配、llama.cpp 版本行为,以及是否有其他线程任务堆积。
最后提醒:不要只依赖任务管理器观察“内存变高了”。任何长期运行的服务都应有基础监控,至少在内存翻倍时能触发日志。对这种场景,合理的目标是先保持内存曲线可预测,再追求更低占用。