
如果你也在本地养 Agent大概率和我一样为“显存告急”掉过几次头发。最近我重新整理自养Agent日志时翻到一条很值得记录的观测模型文件在磁盘上整整 5.9GB但真正运行时只占了大约 2.7GB 显存。这听起来像标题党但其实一点都不玄乎背后是“模型文件大小”和“显存占用”被很多人划了等号而造成的误解。这篇文章就从我这份 Agent 日志出发把这 5.9GB 和 2.7GB 的差距拆开看顺便聊聊低显存设备上跑 Agent 的一堆实操细节和踩坑记录。先说结论模型下载体积、参数体积、运行时显存占用这三件事虽然都有“5.9GB”“2.7GB”这种数字但衡量对象完全不同。你把 5.9GB 当成模型真身又拿 2.7GB 当成显存里的全部家当自然觉得不可思议。只要把量化、KV Cache、加载器行为、显存统计口径这几件事串起来就会发现这个结果不仅正常甚至可以说是低显存跑 Agent 的标准玩法。1. 先把账算清模型文件大小和显存占用是什么关系1.1 5.9GB 的原始模型真实身份是什么我手头这个自养 Agent 用的模型是一个 3B 参数规模的开源对话模型。为什么说 5.9GB 大概率是 3B 模型因为这类模型在仓库里默认给的权重大多是 bf16 或者 fp16 格式每个参数占用 2 字节。3B 参数乘 2 字节正好是 6GB 左右仓库里再把配置文件、分词器、补充权重文件算进去下载体积是 5.9GB 就非常正常。这里有个容易被忽略的点一个模型文件夹里不一定只有一份“纯净权重”。有些仓库会同时放多个分片文件、附带 tokenizer 模型文件甚至保留一点训练用的额外参数。你从 Hugging Face 或别的模型站看到的总大小是所有文件加起来的大小而不是单一权重文件的精确大小。所以“5.9GB”更应该被理解为“这个模型资源包有多大”而不是“运行它必须费多少显存”。另外同样一个 3B 模型如果用 fp32 保存文件会膨胀到 12GB 左右如果转成 GGUF 的 int4 格式可能只有 1.8GB。同样是“一个模型”文件体积天差地别。这里最先要建立的认知就是文件大小取决于存储精度显存占用取决于加载精度和运行时状态两者没有固定换算关系。1.2 显存里真正被加载的是什么在推理阶段显存里放的通常不是“整个模型文件”而是“模型权重张量的某种精度副本”再加上推理所需的中间状态。最常见的公式是推理显存 ≈ 权重占用 激活值 KV Cache 框架预留 少量碎片如果以原始 bf16 权重加载3B 模型的权重就大约需要 6GB显存占用自然比 5.9GB 只多不少。但我实际用的是 int8 量化加载每个参数从 2 字节变成 1 字节理论权重占用大约 3GB。再从显存里减去一些不常驻的小张量、embedding 或 lm_head 部分的颗粒度差异最终看到 2.7GB 是合理的。这里顺便解释一个大家最爱问的问题“显存的作用和模型参数的关系”。遇到这种情况直接说乘 2 乘 2 可不够。正确做法是按“加载精度”算再看有没有开量化、有没有把层放到 CPU、有没有开长上下文。否则你拿着一份 7B 模型的 fp16 原始文件去算显存按公式应该是 14GB但人家用 4bit 量化加载可能只要 5GB 多然后你说别人骗你那就闹笑话了。1.3 显存里的电容器权重之外还有多少隐形开销光有权重还不够。Agent 场景尤其容易忽略两个隐性开销激活值和 KV Cache。激活值是前向推理过程中每层产生的中间结果batch size 越大、序列越长激活值越大。一般单条请求、短上下文时激活值不算夸张但如果你给 Agent 开了流式输出或者多个请求并发过来这部分会迅速涨起来。KV Cache 则是“大语言模型在推理时缓存历史 token 的 Key 和 Value 向量”的内存结构。它的大小取决于层数、注意力头数、上下文长度。Agent 跑多轮工具调用后历史消息越来越多KV Cache 就会成为显存里面的第二支柱。后面我会用日志里的实际数字展示2.7GB 只是权重部分真正跑完一轮完整 Agent 任务峰值往往比这个高不少。2. Agent 日志里我留了哪些观测点2.1 自养 Agent 的基本结构先说我这套“自养 Agent”是怎么搭的。它不是一个躺着聊天的大模型镜像而是一个能调用工具的智能体我给它配了文件读取、命令执行、网页检索、代码运行几个工具它会在对话中生成“工具调用请求”由一个调度器解析后执行工具再把结果拼回上下文继续让模型决策。结构上大概是这样一个模型推理服务负责响应用户和工具结果一个 Agent 编排层负责管理状态、工具列表和调用历史还有一层日志记录把所有关键事件按时间线写下来。这个“Agent日志”不是随便打两行 print而是我用来判断模型健康度、显存水位和上下文膨胀速度的重要工具。如果你也在做类似的自托管 Agent我强烈建议把日志当成第一优先级的可观测设施。模型能不能跑、工具调用成不成功、显存还有多少余量都应该能从日志里一眼看出来而不是卡死了再开监控面板。2.2 一份简化版 Agent 日志长什么样下面是我日志里截出来的一段字段做了脱敏但结构保留[09:12:07|INFO] load model start, repo3b-chat, file_size5.9GB [09:12:08|INFO] load model done, quantint8, devicecuda:0 [09:12:08|INFO] weights alloc2.71GiB, kv_cache_pool0.34GiB [09:12:09|INFO] context_len8192, gpu_mem_util0.42 [09:12:10|INFO] agent session started, system_prompt_tokens548 [09:12:22|INFO] round1, toolsearch_web, input_tokens1563, cached_tokens548 [09:12:38|INFO] tool result chars28431, truncated to 4000 chars [09:12:41|INFO] round2, toolread_file, input_tokens2214, cached_tokens1904 [09:14:33|INFO] round7, toolrun_code, input_tokens4122, cached_tokens3587, peak_mem3.46GiB [09:14:35|WARN] peak_mem_above_warning_line, current_gpu_mem3.41GiB注意里面两个关键数字第一次加载后权重分配是2.71GiBKV Cache 池是0.34GiB合计 3.05GiB。到了第七轮工具调用、日志回显、代码执行结果都堆进上下文后峰值显存到了3.46GiB。所以标题里只说“2.7GB”其实只是权重启动值不是长期运行的最终水位。日志里我还会记录每个 round 的input_tokens和cached_tokens。cached_tokens是能命中 KV Cache 的 token 数它越高说明模型不需要重新计算前面的内容但也说明 KV Cache 占用正在变大。一旦日志里的peak_mem明明不高但input_tokens还在持续上涨你就应该担心下一轮工具结果会不会把显存顶爆。2.3 日志里最该盯的三个指标我给自养 Agent 设了三个必看指标权重占用、上下文 token 数、显存峰值。权重占用是固定水位一般加载完就稳定了。上下文 token 数代表 KV Cache 的压力。显存峰值代表这次会话有没有逼近硬件极限。三者的关系和车辆的油耗很像权重是怠速油耗上下文是加速油耗峰值是瞬时油耗。你不能只看怠速数字就认为整段路都很省油。日志还有一个好处出现故障时有据可查。有一次 Agent 在第五轮突然 OOM我翻日志发现是某个工具把一段 8 万字符的原始日志直接塞回了上下文KV Cache 直接跳了一截。后来我加了一条规则工具输出超过 4000 字符必须截断或摘要。日志里的tool result chars28431, truncated to 4000 chars就是在那一刻记下来的。3. 低显存跑 Agent 的几类优化手段3.1 量化不是玄学int8/int4 到底省在哪能让 5.9GB 模型只占 2.7GB 显存第一功臣就是量化。量化简单说就是降低权重参数的数值精度bf16 每个参数 2 字节int8 每个参数 1 字节int4 每个参数大概 0.5 字节。所以同样一个 3B 模型int8 理论权重约 3GBint4 约 1.5GB。我日志里的 2.7GB 就是 int8 量化后的权重占用。但量化不是无损的。int8 通常损失小到可以忽略int4 在数学推理、代码生成这类任务上会有可感知的掉点有时候还会明显影响 Agent 的工具调用准确性。为什么因为 Agent 需要严格按格式输出 JSON 工具调用参数模型一旦在数字边界上犯迷糊工具名写错、参数类型写错整个流程就断了。我的经验是如果显存余量够优先选 int8只有卡实在太小、又必须塞进大模型时才上 int4。而在 int4 里也得挑方法GPTQ、AWQ、GGUF 这些量化方式的损失分布不一样不能只盯着显存占了多少还要刷一遍 Agent 任务集看看工具调用的成功率。3.2 如果模型是 MoE参数可以不全进显存吗很多朋友看到“低显存跑大模型”的热词会想到 MoE 架构觉得“专家模型不是只激活一部分参数吗那能不能只把部分专家放进显存”这里需要把概念说清楚。MoE 模型虽然在计算时只激活一部分专家但“哪个专家被激活”取决于输入 token 的 router 结果。理论上如果你精确知道这批请求只走某几个专家可以只把那几个专家放显存。但实际推理框架往往还是会把所有专家权重加载进去否则遇到随机路由时访问不在显存里的专家会导致灾难性的性能下降。所以操作上低显存设备上跑 MoE 更现实的做法是把不常用的层或专家 offload 到内存由框架按需换入显存。显存占用可以被压住但换入换出会带来延迟。如果你的 Agent 对实时性要求不高这个方案可用如果要求快速响应不如选一个小一点的稠密模型走 int8 量化。3.3 KV Cache 压缩和上下文管理前边反复提到 KV Cache在 Agent 场景里它才是真正的“显存刺客”。Agent 多轮调用工具后每轮工具结果都会变成新的 token 进入上下文而 KV Cache 和上下文长度成正比。上下文从 2k 涨到 8kKV Cache 可能翻好几倍。解决办法有很多截断工具输出、做历史摘要、只保留最近几轮的关键信息、限制max_tokens。这些方法本质都是“减少上下文里需要缓存的信息量”。我在日志里看到过一条特别直观的线系统提示词 548 token第一轮工具调用后涨到 1563第二轮 2214第七轮 4122。如果不管制工具输出再跑十轮上下文冲到 1 万多 token 很简单那时候哪怕权重只占 2.7GB整卡也不安全。我的做法是给 Agent 的每次工具返回设硬限制。长日志、长网页内容一律截断到几千字符必要时让模型先用摘要工具压缩一下再把摘要塞回上下文。这样 KV Cache 的增长会慢很多显存也能稳得住。3.4 加载器的显存预留策略差异同样的一个模型用 Transformers 加载、用 llama.cpp 加载、用 vLLM 加载显存占用数字可能完全不一样。Hugging Face Transformers 配合device_mapauto时会做一次智能分配尽量把层塞进显存塞不下的放 CPU。vitellm 这类服务框架为了追求高吞吐会提前从显存里划走一大块作为 paging 池gpu_memory_utilization默认值往往不低所以nvidia-smi一查就是好几个 GB哪怕当前根本没有请求。这不是模型本身要吃这么多而是框架“预留”出来的。所以你不要一看到nvidia-smi显存占用高就以为模型变大要去看是模型权重、KV Cache 还是框架预留池。我日志里的 2.7GB 之所以能测出来就是因为用了比较轻的部署方式没有让框架把整个显存提前圈走。4. 复现一个“5.9GB只占2.7GB”的部署4.1 我的具体配置与启动参数如果你想在自己机器上复现这个现象并不需要什么特殊硬件。我当时的配置是NVIDIA 显卡显存 6GBCPU 内存 32GB模型是一个 3B 参数开源模型加载时开 int8 量化。参考代码如下import torch from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig quant_config BitsAndBytesConfig( load_in_8bitTrue, llm_int8_enable_fp32_cpu_offloadTrue, ) model AutoModelForCausalLM.from_pretrained( your-model-path/3b-chat, quantization_configquant_config, device_mapauto, torch_dtypetorch.float16, low_cpu_mem_usageTrue, ) tokenizer AutoTokenizer.from_pretrained(your-model-path/3b-chat)这里的重点是device_mapauto加load_in_8bitTrue。Transformers 会按层做切分能放 GPU 的放 GPU放不下的层放到 CPU。因为我的卡只有 6GB最终权重在显存里占了约 2.7GB其余少部分放到了内存。如果你用 8GB 或 12GB 的卡数字可能略有变化但逻辑一样。4.2 如何把显存占用写进 Agent 日志光看nvidia-smi不够我习惯用 NVML 把显存占用直接采样到日志里这样每次 Agent 执行完一步都能记录当时的显存水位。import pynvml pynvml.nvmlInit() handle pynvml.nvmlDeviceGetHandleByIndex(0) util pynvml.nvmlDeviceGetUtilizationRates(handle) mem pynvml.nvmlDeviceGetMemoryInfo(handle) print(fgpu_util{util.gpu}%, mem_used{mem.used / 1024**3:.2f}GiB)把这段采样代码嵌到 Agent 的每轮工具调用结束后日志里就能看到类似peak_mem3.41GiB的数据。我不建议每一条消息都采样太多次那样日志太吵一般每轮结束时记一次遇到异常时额外记一条就够用了。4.3 跑一个真实工具链从 2.7GB 到 3.4GB启动后模型加载完成时日志显示权重 2.71GiB非常接近标题的 2.7GB。但真正跑 Agent 任务时事情就变了。第一个任务是“搜索几个关键词读取两个文件总结差异并生成一段代码”。搜索接口返回了 28431 个字符我截断到 4000 字符之后塞回上下文。然后是文件读取内容又是一大段日志。接着模型要基于这些内容生成代码。到第七轮时日志记录peak_mem3.46GiB比启动时的 2.7GB 高了差不多 0.8GB。增长主要来自 KV Cache 和激活值。换句话说这个任务如果在 4GB 显存的卡上跑已经接近危险区。所以我经常和别人说低于 4GB 显存想养 Agent不光要关注模型权重还要强烈限制上下文长度否则就是走钢丝。5. 常见问题速查与实战避坑5.1 报 CUDA OOM 但模型明明只占 2.7GB这种情况我至少遇到过三次。排除掉模型权重本身常见原因有两个。第一是机器上还有其他进程占显存。比如前面有人跑过 Stable Diffusion进程没退干净。先执行nvidia-smi看是谁占了卡杀掉无用进程再跑。第二是框架临时申请了额外 buffer。Transformers 在生成时可能分配一段临时显存vLLM 的 paging 池也会预留。解决方案是调低max_new_tokens、限制批大小、清掉临时缓存或者给框架设置显存上限。注意CUDA OOM不等于模型太大很多时候是上下文或生成缓冲太大。日志里一定要记录 peak memory而不是只看加载完成时的数字。5.2 Agent 跑着跑着显存一直涨这是自养 Agent 最经典的坑上下文不断累积导致 KV Cache 持续上升。工具调用的历史、每次返回的原始文本、中间推理过程全都堆在上下文里。我见过最离谱的一次Agent 跑了 20 多轮上下文 token 数到了 4 万多显存占用从 3GB 涨到接近 6GB。解决办法就是上下文压缩隔几轮把前面的内容总结成一段摘要丢掉原始工具输出对工具返回做截断超过阈值就不往上下文塞实在不行就重启会话。另外torch.cuda.empty_cache()在长会话中不一定有用它只清空 PyTorch 的缓存块不会减少真正被张量占用的显存。与其指望缓存清理不如从源头减少 KV Cache。5.3 nvidia-smi 显示的显存比你日志里大我的日志里显存是 2.7GB但nvidia-smi可能显示 4GB 甚至更多这是框架预分配导致的。Transformers 调用 CUDA 后会留一部分缓存vLLM 按gpu_memory_utilization预留比例更多。你真正能控制的是“模型权重 KV Cache 激活值”的有效部分框架预留则是你为吞吐和速度交的“押金”。遇到这种差异先看nvidia-smi --query-compute-appspid,used_memory确定是当前进程占用的显存再和日志对比。如果确实大很多可以考虑换加载器或调整gpu_memory_utilization、max_memory参数。5.4 低显存养 Agent 的其他小习惯最后分享几个长期攒下来的习惯不一定写在哪篇文档里但对稳定性帮助很大。Agent 工具调用范围要克制不是所有命令都能让 Agent 直接执行。我的日志系统会把工具输入和输出做脱敏密钥、个人路径、内网地址都打马赛克避免日志本身变成泄露源。还要给工具加超时和输出上限防止某个脚本卡死或者把日志刷到无限大。还有一点模型服务最好单独跑Agent 编排层和模型服务之间用标准接口通信。这样显存采样、日志记录、模型热重启都能隔离不至于 Agent 崩一次把模型也带走。最后再分享一个小技巧如果你卡在 6GB 显存别总盯着权重文件大小。真正决定你能不能在低显存设备上养 Agent 的从来不是“下载了多大的模型”而是“每次工具调用后上下文把显存推向哪里”。把峰值显存记进日志当作每次发布的检查项你也能用一块小卡养一个干活很勤快的 Agent。