☰
CogVLM/CogAgent本地部署:24G显存4bit量化与排错实践
2026/10/27 21:01:51 网站建设 项目流程

本地把 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 python=3.10 -y conda activate cog # 先装 PyTorch,注意和你的驱动版本对齐 pip install torch==2.1.2 torchvision==0.16.2 --index-url https://download.pytorch.org/whl/cu121 # 核心依赖,版本区间卡死 pip install "transformers>=4.35,<4.45" accelerate sentencepiece pip install bitsandbytes==0.43.1 pip install pillow einops # 注意力加速库,编译耗时较长,建议单独一步做 pip install flash-attn==2.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_code=True) model = AutoModelForCausalLM.from_pretrained( MODEL_PATH, torch_dtype=torch.bfloat16, low_cpu_mem_usage=True, load_in_4bit=True, # 24G 卡的关键开关 trust_remote_code=True, device_map="cuda:0", ).eval() image = Image.open("demo.png").convert("RGB") query = "这张图里有几个人?分别在哪里?" inputs = model.build_conversation_input_ids( tokenizer, query=query, 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_tokens=512, do_sample=False, temperature=0.1, ) answer = tokenizer.decode(output[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True) print(answer)

几个关键点在注释里没写全,这里补上。low_cpu_mem_usage=True是必须的,否则加载过程中会先把整个权重读进内存再搬到显存,内存不够直接挂。load_in_4bit=True会让首次加载明显变慢,因为要做量化转换,但换来的是能塞进 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_side=1280): 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(maxsize=16) 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 取几百比较合适。第二件是温度监控,长时间高负载跑,卡的降频会悄悄拖慢速度,表现出来是"越跑越慢"但找不到原因。加一个简单的温度日志,超过阈值主动降速,比等到降频了再排查强。

最后分享一个我自己觉得挺有用的小习惯:每次换环境或者升级依赖之前,先把当前能跑通的版本组合记录进一个文件,包括所有关键库的版本号、驱动版本、模型权重版本。这套模型生态变化快,三个月后你很可能已经忘了当初是哪几个版本凑在一起才跑通的,有一份记录能省掉重来一遍的痛苦。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询