
跑自养 Agent 这个项目一个月有余最让我意外的不是模型效果而是显存账面上的数字。当时ls -lh一看模型文件 5.9GB按以前跑 dense 模型的经验这种体量加载起来不占 6GB 起步显存才怪。结果服务跑了一整晚nvidia-smi一查常驻 2.7GB。我第一反应是模型偷懒跑到 CPU 上去了查了服务日志、确认 prompt 处理阶段确实有 CUDA 参与之后才开始认真算这笔账5.9GB 是文件体积2.7GB 是运行态真正占用中间差了三个多 GB——差在哪、靠什么省出来是这次最值得记录的东西。这篇文章就把这次自养 Agent 日志里的显存优化过程完整展开包含部署参数、日志排查方法以及几个我踩得比较深的坑。目标读者很明确手里只有 4G、6G、8G 显存但还想在本机跑量化模型、甚至自己搭 Agent 服务的同学应该能直接抄作业。1. 5.9GB 文件只占 2.7GB 显存这笔账到底怎么算1.1 模型文件大小和运行态占用从来不是一回事先说最容易被误解的一点GGUF 文件的大小只代表量化后的权重有多大不代表运行时占用的显存有多大。模型文件的计算公式很简单文件大小 ≈ 总参数量 × 每参数平均比特数 / 8我手头这枚模型用的是 Q4_K_M 量化平均下来每个参数大约 4.7 比特。反推一下总参数量5.9GB × 8 / 4.7 ≈ 10.0B 参数也就是说这其实是一枚 10B 级别的模型。如果是传统 dense 架构的 10B 模型Q4 权重大概 5.5GB光权重就得全量塞进显存再加上 KV Cache 和计算缓冲6GB 卡直接出局。所以5.9GB 文件只占 2.7GB 显存这个现象对 dense 模型来说是不可能的答案必然藏在架构里。运行时的显存账本通常由四部分组成权重复制加载到显存里的那一部分量化权重KV Cache注意力机制缓存的历史 Key/Value 张量计算缓冲激活值、中间张量、CUDA context 这类开销服务端排队缓冲llama.cpp server 申请的额外内存dense 模型四部分全都要在显存里兑现而我的模型属于 MoE混合专家架构其中大头权重可以不进显存这是整个方案成立的根本。1.2 MoE 混合架构不是每个专家都要进显存MoE 架构在 transformer block 内部做了一次分工attention 部分是所有 token 共享的MLP 部分被拆成多个专家比如 8 个。每个 token 进来后先经过一个 router 打分只激活其中得分最高的 top-2 专家做计算。这里有个容易被忽略的细节计算只需要激活专家的权重没被激活的专家完全可以放一边。10B 总参数可能只有 2.5B 到 3B 参数真正参与单次计算。所以MoE 架构是不是要全部参数进显存这个问题的答案是不需要至少在生产环境里不该这么干。全部塞进去不仅浪费显存还丢失了 MoE 最大的优势——小激活参数带来的低推理开销。llama.cpp 把这个策略做成了显式开关叫--cpu-moe老版本里叫--no-moe-gpu。开启之后所有 expert 张量默认常驻系统内存非专家部分attention、共享层、输出层这些按照-ngl参数正常分配进 GPU。路由选中哪个专家再把对应张量从 CPU 侧调度到 GPU 计算用完了按策略释放或复用。我当时看到 2.7GB 的第一反应是是不是 server 参数写错了、模型根本没加载 GPU 层后来确认就是--cpu-moe在起作用。这也解释了为什么网上搜低显存运行模型的人越来越多——架构的红利在大模型本地化之后才开始真正被挖掘。1.3 KV Cache 和滑动窗口在账本里的位置权重之外另一个显存消耗大户是 KV Cache。它的计算公式是KV Cache 字节数 2 × 层数 × n_kv_heads × head_dim × 上下文长度 × 数据类型字节数前面的乘 2是因为 K 和 V 各要一份。我用这枚模型的结构粗算一下假设 40 层、8 个 KV heads、head_dim 128、FP16 存储2 × 40 × 8 × 128 × 8192 × 2 1.31GB也就是全注意力、8K 上下文的情况下KV Cache 就要吃掉 1.3GB。很多混合架构会带滑动窗口注意力只保留最近 2048 个 token 的 KV窗口之外的直接丢弃。这样同样是 8K 上下文KV Cache 降到 0.33GB 左右省出来的就是整整 1GB 显存。所以低显存运行模型有两个独立杠杆一个是 MoE 的参数按需激活另一个是滑动窗口的历史按需保留。两者叠加才可能出现 2.7GB 跑 10B 模型这种账面结果。只靠其中一个都得挤在 4GB 以上的边缘徘徊。1.4 2.7GB 显存是怎么凑出来的把账摊开看2.7GB 的构成大概是这样组件估算占用说明非专家层权重attention/共享部分1.6 - 1.8GBQ4 量化按-ngl全量进 GPUKV Cache滑动窗口 20480.15 - 0.3GBFP16 存储激活值/计算缓冲0.3 - 0.5GBbatch 很小缓冲不大CUDA context 等固定开销0.2 - 0.3GB驱动侧固定占位加起来差不多 2.4GB 到 2.9GB实测 2.7GB 正好落在这个区间。重点在于账本里压根没有那 4GB 左右专家权重的名字它们全部在系统内存待命由--cpu-moe按需调度。注意这套账本的成立前提是系统内存要够。专家常驻内存 其他服务开销我个人推荐 16GB 起步8GB 机器会非常吃紧这个放在后面踩坑部分细说。2. 我的部署配置GGUF 加 llama.cpp--cpu-moe 是省显存的关键2.1 为什么选 GGUF 加 llama.cpp而不是 PyTorch自养 Agent 是长驻服务不是跑一次推理就退出的实验脚本所以工具选择上我优先考虑三点显存可控、接口标准、日志可查。PyTorch HuggingFace 那条路线好处是生态全、改模型方便但坏处是显存控制粒度太粗。fp16直接爆显存bitsandbytes4bit 加载虽然能塞进显存但对长驻服务来说内存申请不太透明Agent 跑几天之后很容易出现莫名其妙的内存膨胀。直接加载 GGUF 也绕过了 PyTorch 的前处理链路少了 Python 进程这一层本身就是一种省资源。llama.cpp 合我意的点在于GGUF 是量化格式的成熟载体Q4_K_M 这类位宽对显存抠得比较狠支持 mmap 加载模型启动快内存占用也低server 模式直接暴露 OpenAI 兼容 APIAgent 代码里把 base_url 指过来就能用CPU/GPU 混合调度、专家按需加载这类开关是物理存在的能精细控制最近搜问得比较多的 MiniMax H3 这类混合拓扑模型也是走 GGUF 量化这条路在低显存机器上跑的原理和我下面说的这套一致。2.2 启动参数逐行拆解实际启动命令长这样nohup llama-server \ -m /opt/agent/models/moe-10b-q4.gguf \ -ngl 99 \ --cpu-moe \ -c 8192 \ --parallel 1 \ --host 127.0.0.1 --port 8080 \ /var/log/agent/llama-server.log 21 逐个说下为什么这么设-m指定模型路径。显存紧张时尽量把模型放在本地 NVMe 上加载速度对 Agent 冷启动恢复很有影响。-ngl 99表示把 99 层放进 GPU。注意配合--cpu-moe时expert 张量会被忽略真正进 GPU 的是非专家层把层数拉满是为了让 attention 部分尽量吃 GPU 算力。--cpu-moe是这个方案的核心开关没有它上述配置会变成全部权重都往显存塞效果完全两样。-c 8192上下文长度。Agent 场景下 8K 足够处理绝大多数指令和文档摘要任务再往上加KV Cache 会按线性吃显存。--parallel 1并发槽位设为 1。Agent 调用是串行为主多槽位意味着每个槽位都要独立分配 KV 空间纯粹是给显存加负担。日志重定向放到/var/log/agent/下跑起来之后所有状态都在文件里为后面排障做准备。如果你用的是 Ollama 而不是 llama.cpp新版本也有对应机制设置环境变量即可OLLAMA_LOAD_EXPERT_ON_DEMAND1 OLLAMA_EXPERT_NUM_BATCH2 ollama serve原理一样专家按需加载不再一次性把整个 expert 张量塞进显存。区别是 Ollama 的调度黑盒程度更高日志不如 llama.cpp 直观我个人在排障期更推荐 llama.cpp。2.3 验证两条命令确认模型真的在 GPU 上跑低显存方案最怕的一件事是模型根本没加载到 GPU而是默默在 CPU 上硬跑显存看着很低速度其实惨不忍睹。所以要验证。先看显存真实占用用 query 格式避免人眼读表的误差nvidia-smi --query-gpumemory.used,memory.total --formatcsv,noheader,nounits nvidia-smi --query-compute-appspid,used_memory --formatcsv第二条命令按进程看显存占用能确认占用来自哪个 PID。再确认服务健康curl -s http://127.0.0.1:8080/healthllama.cpp 的/health返回{status:ok}表示模型加载完毕。然后跑一次真实请求观察日志里的两个指标ppprompt processing处理输入的速度和tgtext generation生成速度。只有看到pp速度明显高于 CPU 水平比如每秒几十到几百 token才能确认 CUDA 参与计算了。我实测下来的数据是pp大约 40-60 token/stg大约 12-20 token/s对 Agent 场景够用。如果这两个数字低得离谱多半是--cpu-moe让几十个专家张量在 CPU 和 GPU 之间反复搬运计算等待大于算力本身。3. 自养 Agent 的日志体系让运行状态自己开口说话3.1 先解决有没有日志输出重定向与日志目录这个项目叫自养 Agent 日志但很多人在这一步就栽了——日志目录根本不存在服务启动时报 redirect 失败或者 log 文件建不出来服务直接退掉。先补基础mkdir -p /var/log/agent chown -R $USER:$USER /var/log/agent然后在启动命令里统一重定向。我习惯用追加而非覆盖因为隔几天可能重启一次 server覆盖会把前一天的崩溃现场抹掉。Agent 侧自己的请求日志也要单独记录我通常在调用接口前后各写一行包含请求 ID、prompt 长度、返回 token 数、耗时。这样出了问题可以两侧对账模型侧 llama-server.log 看处理过程应用侧 requests.log 看请求链路。关于无日志文件夹怎么办这类高频问题90% 就是漏了mkdir -p剩下 10% 是权限写不进/var/log。如果你习惯在 MobaXterm 这类终端工具里操作记得把会话日志功能打开否则窗口一关终端里滚动过的关键报错就全丢了。3.2 定时采集显存和健康状态脚本加 crontab显存占用不是恒定值只靠手动nvidia-smi看一次没意义必须持续采样。我写了一个极简监控脚本#!/bin/bash # /opt/agent/check_agent.sh LOG/var/log/agent/status.log VRAM$(nvidia-smi --query-gpumemory.used --formatcsv,noheader,nounits | awk {print $1}) HEALTH$(curl -s -m 3 http://127.0.0.1:8080/health | head -c 60) PP$(tail -n 20 /var/log/agent/llama-server.log | grep -oE pp [0-9] token.* | tail -1) echo $(date %Y-%m-%d %H:%M:%S) vram${VRAM}MiB health${HEALTH} ${PP} $LOG然后用 crontab 每分钟执行一次*/1 * * * * /opt/agent/check_agent.sh /var/log/agent/cron.log 21这里有个很容易踩的坑cron 环境变量极简PATH 默认只有/usr/bin:/bin。如果脚本里用了脚本目录下的相对路径或自定义命令一定要写绝对路径否则脚本看起来没报错实际啥也没干。这套方案跑起来之后status.log就是 Agent 的黑匣子任何时刻的显存曲线、健康状态、处理速度都有据可查。3.3 crontab 日志怎么查以及日志轮转任务计划日志怎么看和查看 crontab 执行日志是同一类问题。我的经验是分三层排查crontab -l看计划任务是否真的写进去了/var/log/syslog | grep CRON或journalctl -u cron看 cron 守护进程有没有执行记录脚本自己的cron.log看执行后有没有报错输出三层都查完基本能定位 99% 的定时任务问题。另外一个必须提前做的是日志轮转。llama-server.log在长时间运行后增长很快如果不处理几个月后就是几个 GBtail都卡。我在/etc/logrotate.d/agent里放了一份配置/var/log/agent/*.log { daily rotate 7 compress missingok notifempty }单机自养阶段用 logrotate 足够。也有人问要不要上 Filebeat 做集中采集我的看法是日志量超过百 MB、机器超过三五台再考虑个人 Agent 服务tail加grep已经能解决全部问题。引入一套 ES 这类基础设施维护成本比你要解决的问题还大。4. 踩坑实录显存不像你想的那样省4.1 坑一上下文长度悄悄吃掉的 KV Cache跑了一周之后我为了处理更长的上下文把-c 8192改成了-c 16384重启后一看显存从 2.7GB 直接跳到 3.8GB 以上。这就是 KV Cache 的线性放大效应。对全注意力模型来说8K 到 16KKV 翻倍即使是滑动窗口架构窗口内的部分也要按比例扩张。排查链路很简单先nvidia-smi确认涨幅再回滚参数对比日志里 KV 分配的行会把数字写得明明白白。给 Agent 服务选上下文长度时别按模型最大支持来要按业务实际需要来。我最终定在 8K足够处理长文档摘要场景显存预算也稳。4.2 坑二并发请求会复制 KV 槽位llama.cpp server 的--parallel参数控制并发槽位数每个槽位都要预分配自己的 KV 空间。最开始我照搬别人的配置开了--parallel 2以为能支持 Agent 同时处理两个请求结果显存直接多吃了近半 GB而实际业务根本不会同时来两个请求——Agent 是串行编排的。这个坑的特点是不明显因为它不会让你 OOM只是每次看nvidia-smi都觉得好像比预期高一点。排查时要看日志里的slot相关行以及n_parallel的启动参数。自养场景老老实实--parallel 1把省下来的显存给上下文长度性价比更高。4.3 坑三没开--cpu-moe5.9GB 真的会全进显存这个坑我差点踩第二次。有次更新模型文件之后我图省事复制了旧的启动命令但那条命令里漏了--cpu-moe结果起来一看显存占用 5.6GB比平时翻了一倍还多。当时的第一反应是模型变大了完全没往参数丢没丢上面想。判断方法很简单看启动进程的完整命令行确认--cpu-moe在不在再看占用是不是明显大于模型文件的一半。如果 5.9GB 的文件几乎全量进了显存说明专家张量根本没走按需加载路径架构优势全丢了。这个坑的教训是任何一次启动方式的变更都应该在变更后立刻核对显存和日志而不是等到跑完任务再发现问题。4.4 坑四nvidia-smi 的数字怎么读才靠谱nvidia-smi的默认输出信息密度低而且很多人不知道里面有两类数字一个是整卡已经分配的内存一个是当前正在占用的计算资源。只看memory-used容易误判。我自己的读法第一眼用--query-gpumemory.used,memory.total看整卡第二眼用--query-compute-appspid,used_memory按进程核对模型刚启动但没跑请求时显存可能只有 CUDA context 的 200MB 到 400MB这是正常的别急着怀疑模型没加载Windows 下面的任务管理器对显存更新有延迟如果你在 Windows 上跑看到显存没被占满先别慌另外有同事问过显存位置顺序这类偏底层的问题对应用层来说其实没有实质影响显存分配顺序由驱动决定我们只需要关心总量和进程归属。4.5 坑五CPU 和 GPU 之间反复换页速度直接崩--cpu-moe省显存的代价是专家权重要在 CPU 内存和 GPU 之间按需调度。如果模型的路由非常分散每次请求激活的专家组合都不同系统内存带宽会成为瓶颈生成速度会剧烈波动上一秒 20 token/s下一秒掉到 4 token/s。这类问题的特征在日志里特别明显tg指标忽高忽低而nvidia-smi里 GPU-Util 波动极大。我的处理顺序是先确认系统内存是否充足16GB 以下优先加内存而不是调模型减少同时运行的内存大户服务给专家换页留出带宽如果波动依然严重把部分专家层也划给 GPU 长驻牺牲一点显存换稳定最后才考虑降低量化精度或缩短上下文顺便说一句监控日志里重视tg速度的波动曲线就像看数据库慢查询日志一样持续缓慢或间歇性陡降背后往往是资源争抢而不是模型本身变差了。5. 低显存运行模型的边界与取舍5.1 显存占用率低不代表没在用显卡跑完这套方案之后有朋友问显存用了不到三分之一是不是显卡没出力能不能想办法把显存占用率提上去。这问题本身就是一个认知误区。显存是工作台不是利用率显卡出没出力看的是 GPU-Util 和生成速度而不是显存水位。对 Agent 场景来说只要tg速度能满足业务响应要求显存占 2.7GB 还是 8GB 并不重要反而是省下来的显存可以再放一个 embedding 模型或者 rerank 模型。我在这台 12GB 卡上除了主推理模型还塞了 embedding 服务和两个小工具服务都是靠这套低显存方案腾出来的空间。真想提高显存占用率正道是把上下文拉长、把并发槽位调大但这些动作会拖慢单请求延迟对自养 Agent 来说是负优化没有必要。5.2 什么时候该放弃这套方案老实说--cpu-moe这套方案不是万能的有三个场景我建议直接放弃系统内存小于 16GB。专家常驻内存加上操作系统和其他服务开销很容易触发整机 swap那时候速度会崩得比纯 CPU 推理还惨业务需要 32K 以上长上下文。低显存方案在长上下文下要么 KV 吃不消要么滑动窗口丢信息质量会明显下滑机器显存 8GB 以上且只跑一个模型。这种情况下完全可以把更多专家层直接放 GPU 长驻换取更稳定的生成速度没必要学我抠到 2.7GB我之所以压这么狠是因为 12GB 卡要同时伺候好几个服务属于典型的既要又要局面。你的约束条件不一样最优解自然也不一样。5.3 一组监控日志样例与我的日常检查习惯最后分享一段真实监控日志的样子方便你对照自己的输出2025-01-12 21:00:02 vram2704MiB health{status:ok} pp 51 token/s 2025-01-12 21:01:02 vram2688MiB health{status:ok} pp 48 token/s 2025-01-12 21:02:02 vram3401MiB health{status:ok} pp 30 token/s 2025-01-12 21:03:02 vram2702MiB health{status:ok} pp 50 token/s看到 21:02 那一行我就知道当时某个请求把上下文拉长了KV Cache 临时涨了一波请求结束后又回落。如果这种尖刺频繁出现说明业务侧有人在用大 prompt我会去 requests.log 里找出对应请求ID决定要不要限流或切分。跑了大半个月这套 2.7GB 显存的 Agent 服务一直很稳。我个人最大的体会是低显存跑模型不是靠某个魔法参数一劳永逸而是把模型文件、KV Cache、上下文长度、并发策略当成一笔连续的账来算每次显存异常先去日志里找账本的哪一行变了。最后分享一个小技巧把启动命令、crontab、监控脚本和日志轮转配置放在同一个目录下写成注释清晰的脚本换模型或换机器时复制过去、改两个路径就能重新跑起来。下一步我准备试试把同样的方案压到 8GB 卡的极限到时候再记一篇日志补充分享。