资讯动态

CogVLM/CogAgent本地部署:24G显存4bit量化与排错实践

发布时间:2026/10/27 21:02:00 来源:尧图企业网站定制
本地把 CogVLM 和 CogAgent 跑起来这件事我第一次动手的时候以为最多半天结果在一张 24G 显存的单卡上光是第一次加载权重就把显存顶满后面又陆续被 flash-attn 编译、依赖版本冲突和图像预处理这几个问题拖了两天。所以这篇不打算写成一份官方 README 的中文转述而是按我实际部署的顺序来讲哪些账要先算清楚最小可跑的闭环长什么样哪些报错是必然会遇上的以及跑通之后怎么让它稳定地待在机器里干活。CogVLM 是图文问答方向的开源多模态模型CogAgent 在它的基础上加了高分辨率屏幕理解和坐标输出能力能直接把点这个按钮翻译成像素坐标。这套东西适合谁适合手上有一到两张显卡、想拿它做内部工具或者研究基线的人如果你只是想体验一下效果公有接口更省事。下面所有路径和仓库地址我都写成占位符你换成自己拉到的实际位置就行。1. 先算这笔账本地跑这套模型换来什么、要付出什么1.1 两个模型不是一回事别当成一个东西部署很多人把 CogVLM 和 CogAgent 当成同一个模型的两个版本实际上它们的用途差得挺远。CogVLM 是通用的视觉语言模型输入一张图加一段文字输出文字描述、判断或者回答典型场景是图文问答、图表理解、文档信息抽取。CogAgent 是在同一套架构上加了一个面向图形界面的大分辨率视觉分支输入是整屏截图输出是可以直接驱动鼠标的坐标点典型场景是界面自动操作、UI 测试用例执行、无障碍辅助。部署上的差别也很直接CogAgent 的视觉输入分辨率高出一大截同样的参数量下显存占用和单次推理耗时都不是同一个量级。我第一次搭环境的时候偷懒想着先用 CogVLM 的脚本跑通再改两行切到 CogAgent结果发现两者的加载入口、参数名、对话构造函数都不通用。前者走的是通用对话模板后者需要把任务描述按特定格式塞进去还要在输出里解析坐标标记。所以建议一开始就把两条路分开建目录各自独立的环境和启动脚本别指望一套代码兼容省下的时间远比复用来得值。1.2 硬件成本要按能跑和能舒服地跑两档来估参数量摆在那里17B 到 19B 这个量级半精度权重本身就要三十多个 G。这是第一个硬门槛如果你只有一张 24G 的卡半精度是肯定塞不进去的只能走 4bit 量化量化的代价是精度下降和首次加载变慢但绝大多数问答场景肉眼看不出来。如果你有 40G 级别的卡半精度推理是可以的但要注意推理时的激活缓存和图像特征占位会再吃掉几个 G留出 20% 余量比较稳。我整理了一张粗略的对照数值是按实际测出来的显存峰值估的不同驱动和框架版本会有浮动配置档位权重精度CogVLM 显存峰值CogAgent 显存峰值单次响应体感24G 单卡4bit约 14-18G约 18-22GCogAgent 需要控制截图尺寸24G 单卡半精度无法加载无法加载不适用40G 单卡半精度约 36-39G接近上限风险高CogVLM 可用CogAgent 勉强两张 24G半精度 拆分可跑但通信开销明显不建议调试成本高于收益看这张表就能明白24G 卡是绝大多数人的现实起点4bit 量化不是可选项而是必需项。而 CogAgent 因为输入分辨率高量化之后仍然比 CogVLM 多吃三到五个 G这是我最初没预料到的导致我先跑通 CogVLM 后以为 CogAgent 也稳了结果一加载就报显存不足。1.3 有些场景其实不值得自己部署说句实在话如果你的需求是偶尔问几句图片内容本地部署的时间成本明显不划算。拉权重、配环境、调依赖顺利的话一整天不顺利两三天。真正值得自己部署的是这几类数据不能出内网、需要批量跑几千上万张图、需要固定版本做可复现的实验基线、需要在模型外面套自己的后处理逻辑。我见过有团队为了感觉更可控硬上本地部署结果维护成本比调用接口高出好几倍模型一升级又要重新折腾一遍这就属于没算清楚账。2. 环境搭建的三个硬约束显存、驱动、依赖版本2.1 显存不是唯一指标显存带宽和卡间通信同样要命新手容易盯着显存数字看觉得 24G 够用就行。实际跑下来瓶颈往往出现在别的地方。一是显存带宽同样 24G 的老卡和新卡加载三十多 G 权重的时间能差一倍以上二是如果走多卡拆分卡间通信的带宽会直接决定推理速度PCIe 通道数不够的话两张卡跑起来可能比一张卡量化版还慢。我实测过一组配置两张卡半精度拆分的吞吐反而不如单卡 4bit因为每层都要同步一次通信成了主要开销。还有个容易被忽略的点图像预处理是在 CPU 上做的高分辨率图像的缩放和归一化会占掉不少时间。CogAgent 那种上千像素的输入如果预处理写得粗糙一张图光缩放就要几十毫秒比模型推理的零头还多。建议把预处理放到数据加载线程里跟推理流水线错开别串行做。2.2 依赖版本的三条红线破一条就要重装这套模型对依赖版本的敏感度相当高我踩过的坑基本都集中在这里。第一条是 PyTorch 和 CUDA 的匹配驱动版本、CUDA 运行时、PyTorch 编译版本三者要对上装错了表现是能 import 但一跑就报 CUDA 错误排查起来很费时间。第二条是 transformers 的版本区间新版本经常改动内部接口模型自带的远程代码可能直接失效老版本又缺一些必要的算子支持。第三条是注意力加速库版本和 PyTorch、CUDA 三者强绑定装错就是编译失败或者运行时崩溃。我的建议是先用一个干净的 conda 环境把版本钉死不要图省事用系统 Python。下面这份清单是我目前在用的组合跑 CogVLM 系列比较稳conda create -n cog python3.10 -y conda activate cog # 先装 PyTorch注意和你的驱动版本对齐 pip install torch2.1.2 torchvision0.16.2 --index-url https://download.pytorch.org/whl/cu121 # 核心依赖版本区间卡死 pip install transformers4.35,4.45 accelerate sentencepiece pip install bitsandbytes0.43.1 pip install pillow einops # 注意力加速库编译耗时较长建议单独一步做 pip install flash-attn2.5.8 --no-build-isolation注意flash-attn 的编译对内存要求很高编译时如果机器内存不足会直接被杀掉。建议先确认机器至少有 32G 内存或者直接用官方提供的预编译轮子。2.3 模型仓库的加载入口不同别混用一个环境CogVLM 和 CogAgent 在早期版本里都依赖一套自研的模型加载框架这套框架需要单独安装而且它对 transformers 和 PyTorch 的版本有自己的要求经常和主环境打架。较新的版本则转向了标准的 transformers 加载方式覆盖面更广但功能支持可能滞后。我的做法是给两条路线各建一个环境一个装自研框架跑老版本一个用标准加载跑新版本彼此隔离。听起来浪费磁盘但省下来的是排查冲突的时间非常值。3. 跑通 CogVLM从权重下载到第一句回答3.1 权重下载的目录结构和校验权重一般分片存放几个 G 一个文件下载完整后目录里应该有配置文件、分词器文件和一堆分片。我遇到过下载中断但文件大小看起来差不多的情况加载时报的错却是权重形状不匹配非常误导。所以下载完先做两件事一是核对文件数量和总大小二是用加载脚本跑一次空推理能正常加载就说明权重没问题。目录结构大概长这样路径按你自己的实际位置替换models/ └── cogvlm-chat/ ├── config.json ├── tokenizer.model ├── tokenizer_config.json ├── special_tokens_map.json └── pytorch_model-00001-of-0000X.bin ...如果你拿到的是多分片形式注意别只下了一部分就急着跑加载器通常只会在真正前向的时候才发现缺片报错信息往往指向某个层的参数缺失看起来像是代码问题实际上是文件没下全。3.2 加载脚本的逐段拆解下面这段是 4bit 量化的最小可跑版本我把它拆成几段解释你对照自己的仓库改函数名就行不同版本的入口函数签名会有出入import torch from PIL import Image from transformers import AutoModelForCausalLM, AutoTokenizer MODEL_PATH models/cogvlm-chat tokenizer AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_codeTrue) model AutoModelForCausalLM.from_pretrained( MODEL_PATH, torch_dtypetorch.bfloat16, low_cpu_mem_usageTrue, load_in_4bitTrue, # 24G 卡的关键开关 trust_remote_codeTrue, device_mapcuda:0, ).eval() image Image.open(demo.png).convert(RGB) query 这张图里有几个人分别在哪里 inputs model.build_conversation_input_ids( tokenizer, queryquery, history[], images[image] ) inputs { input_ids: inputs[input_ids].unsqueeze(0).cuda(), token_type_ids: inputs[token_type_ids].unsqueeze(0).cuda(), attention_mask: inputs[attention_mask].unsqueeze(0).cuda(), images: [[inputs[images][0].cuda().to(torch.bfloat16)]], } with torch.no_grad(): output model.generate( **inputs, max_new_tokens512, do_sampleFalse, temperature0.1, ) answer tokenizer.decode(output[0][inputs[input_ids].shape[1]:], skip_special_tokensTrue) print(answer)几个关键点在注释里没写全这里补上。low_cpu_mem_usageTrue是必须的否则加载过程中会先把整个权重读进内存再搬到显存内存不够直接挂。load_in_4bitTrue会让首次加载明显变慢因为要做量化转换但换来的是能塞进 24G 卡这笔交易很划算。build_conversation_input_ids这个函数名不同版本可能不一样有的叫build_conversation_input_ids有的封装在别的类里你以自己仓库里的实现为准。3.3 提问方式对结果的影响比参数大得多调温度、调 top_p 这些参数很多人一上来就盯着改实际测下来对这类模型的影响远不如提问方式本身。CogVLM 在中文场景下表现不错但如果你问得太笼统比如描述这张图它会给你一段泛泛而谈的话换成图中左上角的表格有几列表头分别是什么回答的准确率立刻上一个台阶。我的经验是把问题拆成可验证的小问题一次只问一件事比一次性丢一个复杂任务效果稳定得多。还有个小技巧如果涉及计数或者位置关系让它先描述再回答比直接问数字准。原因不复杂模型在描述过程中会把视觉特征和语言对齐得更充分相当于给它一个中间推理步骤。这个技巧在文档抽取和界面理解上都很有效。4. CogAgent 的部署差异分辨率一高所有账都要重算4.1 高分输入带来的显存与耗时跳变CogAgent 最特别的地方是它接受接近原生的屏幕分辨率输入比常规视觉模型高出一个量级。这带来的直接后果是视觉特征的序列长度暴涨显存占用和推理耗时都跟着涨。我实测下来同一张图常规分辨率输入和 CogAgent 的高分输入耗时能差三到五倍显存峰值多出好几个 G。应对办法有两个。一是控制输入尺寸把截图按比例缩到模型支持的范围里但要注意缩放会损失小字号文本的可读性界面上那些十几像素的字缩完就糊了。二是对截图做区域裁剪只把当前关注的窗口区域喂进去这需要你在外面加一层窗口检测逻辑实现成本不低但效果明显。我目前的做法是先用整屏缩略图让模型判断该操作哪个区域再裁出该区域做精细识别两步走比一次性喂整屏稳。4.2 坐标输出的格式与解析CogAgent 的输出里坐标是带特殊标记的通常会被归一化到一个固定区间比如千分位表示再按实际图像宽高换算回像素位置。解析这段输出的时候有几个坑一是模型可能输出多个候选坐标你要决定取第一个还是做投票二是坐标可能越界比如输出了略大于区间的值这时候要做裁剪三是标记格式没配对完整比如只有开始标记没有结束标记解析器会抛异常必须做兜底。import re def parse_coord(text, width, height): # 匹配形如 |box|(x1,y1),(x2,y2)|/box| 或单个坐标 pattern r\|box\|(.*?)\|/box\| match re.search(pattern, text) if not match: return None nums re.findall(r\d, match.group(1)) if len(nums) 2: return None x, y int(nums[0]), int(nums[1]) # 归一化坐标还原到像素 px min(max(int(x / 1000 * width), 0), width - 1) py min(max(int(y / 1000 * height), 0), height - 1) return px, py这段代码的重点在最后两行的边界裁剪看起来是废话但实际跑几百次之后你一定会遇到越界值不做裁剪就会在点击环节点到屏幕外面去。另外归一化的分母要按你实际模型的约定来有的是 1000有的是实际宽高先拿几个已知答案的样本校准一次再上线。4.3 把它接进自动化流程之前要想清楚的事模型输出的是建议点击哪里不是保证点对。真正接进自动化流程中间必须有一层校验。我的做法是每次操作后重新截图用简单的图像差异判断界面有没有变化没变化就说明点空了或者点错了这时候重试一次或者降级到备用规则。这套机制听起来笨但比盲目相信模型输出稳定得多。还有一个现实问题这类模型的响应时间是秒级的做单步操作可以做几十步连续操作就太慢了。所以它更适合关键节点决策比如遇到没见过的弹窗时判断该点哪个按钮而不是把整个流程都交给它。把规则和模型混合使用规则处理高频固定路径模型处理长尾异常这是我目前觉得最划算的分工。5. 踩坑实录那些能让人卡一整天的报错5.1 注意力加速库编译失败的各种形态这个库的编译失败几乎是必经之路表现形式有好几种。最常见的是编译过程中内存被打满进程直接被系统杀掉日志里看不到明确的错误只有一行 Killed。这种情况要么加内存要么限制并行编译数。第二种是 CUDA 版本不匹配报错会指向一些底层的头文件找不到这时候要检查 CUDA_HOME 环境变量指的对不对。第三种是 PyTorch 版本太新这个库还没适配报错是一堆模板相关的编译错误非常难读。我的处理顺序是这样的先看日志里第一个报错的位置不要被后面一堆连锁错误带偏然后确认 CUDA 版本和 PyTorch 编译版本是否一致用一行命令验证最后才是考虑换版本。实在折腾不动就先用不依赖这个库的加载方式跑通流程功能上会慢一些但不影响验证效果等整套流程跑通了再回来优化。5.2 版本冲突的排查链路依赖冲突的典型表现是昨天还能跑今天换个环境就报错或者报错信息指向的地方看起来完全无关。我整理了一个排查顺序按这个顺序走能省不少时间现象最可能原因验证方式处理办法import 时报找不到符号版本过旧缺接口打印版本号比对要求升级到要求区间内加载权重时报形状不匹配权重分片不全或版本不配套核对文件数与总大小重新下载完整权重前向时算子不存在加速库版本不匹配单独 import 加速库重装匹配版本显存不足但看着够加载时用了全精度打印模型 dtype打开量化开关输出乱码或重复分词器与权重不配套检查分词器文件用同仓库配套分词器这张表我贴在便签上用了很久里面每一条都是实际踩过的。特别说下最后一条输出重复或者乱码很多人第一反应是采样参数问题实际上是分词器文件和权重不配套比如用了新版分词器配老版权重这类问题换多少个采样参数都没用。5.3 显存不足的三种不同表现显存不足不一定是那句经典的 out of memory。它有三种表现处理方式完全不同。第一种是加载阶段就报分配失败说明权重本身塞不下只能降精度或者换卡。第二种是加载成功但第一次前向就崩说明权重能放下但激活缓存不够这时候降输入分辨率或减短输出长度可能救回来。第三种是跑了几十次之后突然崩这叫显存碎片原因是不同长度的请求交替进来缓存块被切成碎片总显存看着还有但分不出连续的大块。前两种靠调整参数能解决第三种要动架构。最简单有效的办法是把推理服务做成每次请求处理完就清理缓存牺牲一点性能换稳定。再进一步是限制输入长度分档把长度接近的请求批量处理减少碎片产生。我现在的服务里加了这个清理动作连续跑几万次没再出现莫名其妙的崩溃。5.4 图像预处理那些不起眼但致命的问题图像通道数不对是最常见的某些截图工具导出的图带透明通道直接喂进去会报维度错误必须显式转成 RGB。EXIF 旋转信息是第二个坑手机拍的图可能带旋转标记用某些库读出来是旋转前的状态模型看到的是躺着的图回答自然离谱。第三个坑是色彩空间某些图是 CMYK 或者 16 位通道不转换的话数值范围完全不对。from PIL import Image, ImageOps def load_image(path, max_side1280): img Image.open(path) img ImageOps.exif_transpose(img) # 处理旋转 img img.convert(RGB) # 统一通道 w, h img.size if max(w, h) max_side: scale max_side / max(w, h) img img.resize((int(w * scale), int(h * scale)), Image.LANCZOS) return img这三行处理我放在所有入口的最前面不管后续走哪个模型都先过一遍省掉了大量为什么这张图结果这么怪的排查。6. 跑通之后让它稳定待在生产环境里的几件事6.1 量化带来的收益与代价要量化着看4bit 量化把显存需求砍掉一半多这是它最大的价值。代价分两块一是精度损失在通用问答上基本无感但在需要精确读数字或者识别小字的场景错误率会明显上升二是加载变慢首次加载要做量化转换几十秒到几分钟不等。我的建议是把量化版本和非量化版本都保留日常批量任务用量化版需要高精度的少量任务走非量化版按任务分流。这样既控制了成本又保住了关键场景的准确率。还有一个细节量化之后模型的输出风格会略微变化比如回答变短、更倾向于给结论而不给推理过程。如果你的下游逻辑依赖输出格式量化前后要各跑一批样本做对比别直接替换上线。6.2 常驻进程与请求队列的基本骨架每次请求都重新加载模型是最浪费的做法加载一次几十秒完全没法用。正确做法是进程常驻模型只加载一次请求排队处理。骨架大概是这样import queue import threading import torch task_queue queue.Queue(maxsize16) result_map {} def worker(model, tokenizer): while True: item task_queue.get() if item is None: break req_id, image, query item try: with torch.no_grad(): ans run_one(model, tokenizer, image, query) result_map[req_id] (ok, ans) except Exception as e: result_map[req_id] (err, str(e)) finally: torch.cuda.empty_cache() # 关键控制碎片 task_queue.task_done()队列长度要限制不要无限收。模型处理慢队列堆太多只会让用户等待时间失控超时的请求应该直接拒绝而不是排队。我一般把队列长度设在 16 到 32 之间配合一个明确的超时返回体验比无限排队好得多。6.3 长稳运行最容易被忽略的两件事第一件是显存缓存的定期清理。前面提过碎片问题除了每次请求后清理还可以设一个计数器每处理 N 次请求做一次完整清理N 取几百比较合适。第二件是温度监控长时间高负载跑卡的降频会悄悄拖慢速度表现出来是越跑越慢但找不到原因。加一个简单的温度日志超过阈值主动降速比等到降频了再排查强。最后分享一个我自己觉得挺有用的小习惯每次换环境或者升级依赖之前先把当前能跑通的版本组合记录进一个文件包括所有关键库的版本号、驱动版本、模型权重版本。这套模型生态变化快三个月后你很可能已经忘了当初是哪几个版本凑在一起才跑通的有一份记录能省掉重来一遍的痛苦。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑