戰(zhàn):顯存、版本與坐標(biāo)換算避坑指南)
1. 先把這兩個(gè)模型能干什么弄清楚再談怎么跑想把 CogVLM 與 CogAgent 部署到本地最容易踩的第一個(gè)坑是以為它們是同一類(lèi)東西裝個(gè)環(huán)境就能一起用。實(shí)際情況差得挺遠(yuǎn)CogVLM 是通用的視覺(jué)語(yǔ)言模型你給它一張圖加一句話它給你一段描述、一段分析或者一段 OCR 結(jié)果CogAgent 是在前者的底座上繼續(xù)往 GUI 方向訓(xùn)練出來(lái)的智能體模型它看的是截圖輸出的是“點(diǎn)哪里、輸入什么、往下滾多少”這類(lèi)動(dòng)作指令。部署 CogVLM 的流程和部署 CogAgent 的流程九成相似但剩下那一成——輸入分辨率、prompt 模板、坐標(biāo)換算——決定了你能不能真正跑起來(lái)。我前后在兩臺(tái)機(jī)器上折騰了差不多一周第一臺(tái)是單卡 24G 的消費(fèi)級(jí)卡第二臺(tái)是雙卡 48G 的工作站中間踩的坑基本都集中在顯存、依賴版本和坐標(biāo)映射這三處所以下面按這三條主線展開(kāi)。先說(shuō)清楚“本地部署”這件事的價(jià)值。很多人第一反應(yīng)是調(diào)接口不就完了何必自己折騰顯存和依賴。但實(shí)際項(xiàng)目里需要本地跑的場(chǎng)景非常硬內(nèi)網(wǎng)隔離的機(jī)器沒(méi)有外網(wǎng)、每天要處理幾萬(wàn)張截圖不能按次計(jì)費(fèi)、需要對(duì)模型輸出做自由的后處理、需要把模型嵌進(jìn)已有的自動(dòng)化流程里當(dāng)一個(gè)小模塊用。這些需求疊在一起本地部署就成了唯一解。而 CogVLM 這類(lèi)模型的好處在于它的權(quán)重和推理代碼都是開(kāi)放的你能看到視覺(jué)專(zhuān)家模塊怎么接進(jìn)去、圖像怎么預(yù)處理、tokenizer 怎么拼 prompt這些細(xì)節(jié)在調(diào)效果的時(shí)候全是關(guān)鍵。1.1 CogVLM看圖說(shuō)話的通用底座CogVLM 的結(jié)構(gòu)可以拆成三塊來(lái)理解一塊視覺(jué)編碼器負(fù)責(zé)把圖片變成特征一塊語(yǔ)言模型負(fù)責(zé)生成文字中間還有一個(gè)“視覺(jué)專(zhuān)家”模塊專(zhuān)門(mén)在語(yǔ)言模型的若干層里插入可訓(xùn)練的視覺(jué)通路。打個(gè)比方語(yǔ)言模型是一條跑了很久的高速公路視覺(jué)專(zhuān)家相當(dāng)于在關(guān)鍵路口修了幾條匝道讓圖像信息能就近匯入而不是全部擠在入口處硬塞進(jìn)去。這個(gè)設(shè)計(jì)帶來(lái)的直接好處是模型對(duì)圖像細(xì)節(jié)的敏感度比“把圖像特征拼在 prompt 前面”那種樸素做法要好不少尤其是小字、表格、界面元素這類(lèi)需要精確讀取的內(nèi)容。對(duì)部署者來(lái)說(shuō)這個(gè)結(jié)構(gòu)意味著兩件事。第一模型總參數(shù)量比同級(jí)別的純文本模型大權(quán)重占用的顯存更多17B 級(jí)別的模型在半精度下光權(quán)重就要 34GB 上下單卡 24G 根本放不下必須走量化或者多卡。第二它依賴trust_remote_codeTrue加載自定義代碼也就是說(shuō)模型目錄里自帶建模文件這些文件對(duì)transformers的版本相當(dāng)挑剔版本差一點(diǎn)就可能報(bào)出一堆看著莫名其妙的錯(cuò)。后面我會(huì)具體說(shuō)版本怎么選。1.2 CogAgent盯著屏幕找按鈕的那一類(lèi)CogAgent 的定位更垂直。它的輸入是一張屏幕截圖輸出是結(jié)構(gòu)化的動(dòng)作比如點(diǎn)擊某個(gè)坐標(biāo)、在某個(gè)位置輸入文本、滾動(dòng)頁(yè)面。它的圖像輸入分辨率比通用版更高因?yàn)?GUI 界面上的按鈕、輸入框、圖標(biāo)都很小壓縮得太狠模型就看不見(jiàn)了。高分辨率帶來(lái)的代價(jià)是顯存和計(jì)算量同步上升同樣一張圖編碼出來(lái)的視覺(jué) token 數(shù)量可能是普通圖片的好幾倍。部署 CogAgent 時(shí)最需要提前想清楚的是坐標(biāo)系問(wèn)題。模型是在縮放后的圖像上做定位的而你的真實(shí)屏幕是原始分辨率兩者之間差一個(gè)縮放比和一個(gè)可能的填充偏移。這個(gè)換算如果做錯(cuò)模型明明“看對(duì)了”點(diǎn)下去卻偏了半個(gè)屏幕排查起來(lái)非常折磨人。我建議在正式接入自動(dòng)化流程之前先做一次坐標(biāo)標(biāo)定用幾個(gè)已知位置的元素驗(yàn)證映射關(guān)系這一步花十分鐘能省掉后面幾小時(shí)的懷疑人生。1.3 什么配置的機(jī)器能跑先說(shuō)結(jié)論很多人卡在第一步不知道自己手上的機(jī)器夠不夠。我把幾種常見(jiàn)配置的實(shí)際情況列出來(lái)方便你對(duì)照。注意這里說(shuō)的是“能跑起來(lái)并輸出合理結(jié)果”不是“能跑得飛快”。模型規(guī)模精度權(quán)重顯存加上激活與KV后的實(shí)際占用建議硬件17B 級(jí)別bf16約 34GB38GB 到 42GB單卡 48G 或雙卡 24G17B 級(jí)別4bit 量化約 10GB13GB 到 16GB單卡 24G 可用19B 級(jí)別bf16約 38GB42GB 到 46GB單卡 48G雙卡更穩(wěn)19B 級(jí)別4bit 量化約 11GB14GB 到 18GB單卡 24G 可跑18B 級(jí)別GUI 版4bit 量化約 11GB15GB 到 19GB單卡 24G高分辨率截圖更吃顯存這張表里的“實(shí)際占用”不是拍腦袋后面會(huì)給出估算方法。簡(jiǎn)單說(shuō)權(quán)重只是地基真正把顯存撐爆的往往是長(zhǎng) prompt 的 KV Cache 和高分辨率圖像產(chǎn)生的視覺(jué) token。一張 1120 見(jiàn)方的截圖編碼出來(lái)的 token 數(shù)量可能頂?shù)蒙蠋装賯€(gè)中文字做多輪對(duì)話時(shí)這部分會(huì)持續(xù)累積。2. 環(huán)境這一關(guān)版本對(duì)齊比裝包重要得多我先說(shuō)一個(gè)反直覺(jué)的結(jié)論這類(lèi)模型的部署失敗八成不是硬件不夠是版本不對(duì)。你按某個(gè)教程裝了一套包代碼跑起來(lái)報(bào)AttributeError或者KeyError搜半天發(fā)現(xiàn)是transformers小版本差了兩位。這種問(wèn)題沒(méi)有通用解只能靠鎖定版本。我的做法是先在一個(gè)干凈環(huán)境里裝最小依賴集跑通官方示例再往里面加?xùn)|西一旦加出問(wèn)題立刻回退絕不在一棵已經(jīng)長(zhǎng)歪的樹(shù)上繼續(xù)嫁接。2.1 依賴版本的三角關(guān)系需要同時(shí)對(duì)齊的有三樣CUDA 驅(qū)動(dòng)、PyTorch、transformers。三者是鏈?zhǔn)揭蕾嚾魏我画h(huán)錯(cuò)位都會(huì)在加載模型時(shí)炸出來(lái)。CUDA 與 PyTorch先看nvidia-smi右上角顯示的驅(qū)動(dòng)支持的最高 CUDA 版本然后選一個(gè)不高于它的 PyTorch 輪子。驅(qū)動(dòng)支持 12.1你裝 cu121 的輪子最省事硬裝 cu118 的輪子通常也能跑但要留意量化庫(kù)有沒(méi)有對(duì)應(yīng)編譯版本。PyTorch 與transformers這一對(duì)最容易出事。模型自帶的建模代碼里會(huì) import 一些內(nèi)部類(lèi)新版transformers改了簽名就會(huì)直接報(bào)錯(cuò)。量化庫(kù)4bit 加載依賴bitsandbytes它需要和 CUDA 運(yùn)行時(shí)匹配版本太舊會(huì)報(bào)找不到符號(hào)。我實(shí)測(cè)下來(lái)比較穩(wěn)的一組是PyTorch 2.1 到 2.3 區(qū)間transformers4.37 到 4.42 區(qū)間accelerate0.26 以上bitsandbytes0.41 以上。這個(gè)組合不是唯一解但它是個(gè)“大多數(shù)教程和模型自帶代碼都能兼容”的安全區(qū)。如果你拿到的是更新的模型版本可以適當(dāng)往上走但務(wù)必先用最小腳本驗(yàn)證加載別一上來(lái)就跑完整業(yè)務(wù)。提示不要用pip install -U無(wú)腦升級(jí)。跑通之后立刻pip freeze requirements.lock把當(dāng)前環(huán)境凍住這是血淚教訓(xùn)。我有一次為了裝另一個(gè)庫(kù)順手升了transformers第二天回來(lái)模型加載直接崩回滾花了半小時(shí)。2.2 隔離環(huán)境與目錄規(guī)劃不要往系統(tǒng) Python 里裝東西這是底線。用 conda 或者 venv 建一個(gè)獨(dú)立環(huán)境名字起得直白點(diǎn)比如vlm-local。模型權(quán)重體積大一般十幾個(gè)到幾十個(gè) G別放在系統(tǒng)盤(pán)找個(gè)數(shù)據(jù)盤(pán)建個(gè)統(tǒng)一目錄mkdir -p /data/models/cogvlm-chat-hf mkdir -p /data/models/cogagent-chat-hf mkdir -p /data/workspace/vlm-demo權(quán)重放/data/models下代碼放/data/workspace下緩存目錄也要單獨(dú)指出來(lái)否則 HuggingFace 默認(rèn)會(huì)往家目錄的隱藏文件夾里塞跑幾個(gè)模型下來(lái)把根分區(qū)撐滿是常有的事。環(huán)境變量這樣設(shè)export HF_HOME/data/hf_cache export TRANSFORMERS_CACHE/data/hf_cache/transformers export HF_HUB_OFFLINE1最后一行是給內(nèi)網(wǎng)機(jī)器準(zhǔn)備的。權(quán)重提前從有網(wǎng)的機(jī)器下載好、整目錄拷進(jìn)來(lái)之后把離線模式打開(kāi)避免程序啟動(dòng)時(shí)還去聯(lián)網(wǎng)檢查更新卡在那里轉(zhuǎn)圈。2.3 權(quán)重文件的完整性檢查從外部拷貝權(quán)重最容易出的問(wèn)題是文件不齊。模型目錄里一般有config.json、modeling_*.py、tokenizer.model、若干*.safetensors分片還有一個(gè)*.index.json記錄分片映射??截愔袛鄷?huì)導(dǎo)致最后一個(gè)分片體積不對(duì)加載時(shí)報(bào) safetensors 解析錯(cuò)誤但報(bào)錯(cuò)信息往往指向別的地方很容易誤判。我的檢查習(xí)慣是兩步先看分片數(shù)量和 index 文件里列的數(shù)量是否一致再逐個(gè)比對(duì)文件大小是否和源端一致。如果是用工具下載的注意下載工具的靜默失敗——看著進(jìn)度條走完了實(shí)際缺一個(gè)文件。穩(wěn)妥做法是下載完之后立刻跑一次“只加載 tokenizer”的測(cè)試這一步幾乎不耗顯存能快速確認(rèn)目錄結(jié)構(gòu)對(duì)不對(duì)。from transformers import LlamaTokenizer tok LlamaTokenizer.from_pretrained(/data/models/cogvlm-chat-hf, trust_remote_codeTrue) print(len(tok), tok.convert_tokens_to_ids(|endoftext|))能正常打印出詞表大小說(shuō)明 tokenizer 這塊沒(méi)問(wèn)題可以繼續(xù)往下走。3. 從零把模型加載起來(lái)環(huán)境齊了權(quán)重到位了接下來(lái)是最激動(dòng)人心也最容易翻車(chē)的一步把模型讀進(jìn)顯存。這一節(jié)從最小可運(yùn)行代碼開(kāi)始逐步加量化、加多卡最后給一個(gè)能直接抄的模板。3.1 半精度加載先跑通再優(yōu)化第一版代碼不要開(kāi)量化不要開(kāi)多卡就在能裝得下的機(jī)器上用半精度跑一次。目的是確認(rèn)模型本身沒(méi)問(wèn)題把變量降到最少。import torch from transformers import AutoModelForCausalLM, LlamaTokenizer MODEL_PATH /data/models/cogvlm-chat-hf tokenizer LlamaTokenizer.from_pretrained(MODEL_PATH, trust_remote_codeTrue) model AutoModelForCausalLM.from_pretrained( MODEL_PATH, torch_dtypetorch.bfloat16, low_cpu_mem_usageTrue, trust_remote_codeTrue, device_mapcuda:0, ).eval()幾個(gè)參數(shù)值得解釋。low_cpu_mem_usageTrue會(huì)分片加載權(quán)重避免先把整個(gè)模型讀進(jìn)內(nèi)存再搬到顯存對(duì)于內(nèi)存不大比如 64G的機(jī)器這個(gè)開(kāi)關(guān)是從“卡死”到“能跑”的區(qū)別。device_map這里我故意寫(xiě)死成cuda:0而不是auto原因后面講多卡時(shí)會(huì)說(shuō)到。加載完成后先用一段純文本跑一次確認(rèn)語(yǔ)言部分正常resp, _ model.chat(tokenizer, 用一句話說(shuō)明什么是視覺(jué)語(yǔ)言模型, history[]) print(resp)這一步不涉及圖像速度很快。如果這一步就出問(wèn)題那多半是 tokenizer 或者模型文件的問(wèn)題和視覺(jué)模塊無(wú)關(guān)排查范圍小很多。3.2 圖像輸入的正確喂法圖像輸入不是簡(jiǎn)單地把 PIL 對(duì)象傳進(jìn)去就完事。模型自帶的build_conversation_input_ids會(huì)做一套預(yù)處理縮放、歸一化、切成 patch然后把視覺(jué) token 按模板拼進(jìn) prompt。你需要注意的是圖像尺寸和格式。from PIL import Image image Image.open(/data/workspace/demo.png).convert(RGB) inputs model.build_conversation_input_ids( tokenizer, query這張圖里有什么請(qǐng)?jiān)敿?xì)描述。, history[], images[image], ) inputs {k: v.to(cuda:0).to(torch.bfloat16) if v.dtype torch.float32 else v.to(cuda:0) for k, v in inputs.items()} with torch.no_grad(): out model.generate(**inputs, max_new_tokens256, do_sampleFalse) print(tokenizer.decode(out[0][inputs[input_ids].shape[1]:], skip_special_tokensTrue))這里有個(gè)細(xì)節(jié)很多人忽略build_conversation_input_ids返回的是一個(gè)字典里面的images字段類(lèi)型是float32的張量。如果你的模型是 bf16 精度這個(gè)張量也要轉(zhuǎn)成 bf16否則會(huì)在視覺(jué)編碼器里報(bào) dtype 不匹配。上面那行推導(dǎo)式就是干這個(gè)的雖然寫(xiě)得有點(diǎn)丑但比在模型內(nèi)部改代碼安全。另一個(gè)細(xì)節(jié)是do_sampleFalse。做圖像理解和 GUI 定位這類(lèi)任務(wù)時(shí)我強(qiáng)烈建議用貪心解碼也就是關(guān)掉采樣。采樣帶來(lái)的多樣性在這里是負(fù)面因素——同一個(gè)按鈕兩次運(yùn)行給你兩個(gè)不同坐標(biāo)下游流程根本沒(méi)法用。只有做開(kāi)放式描述、創(chuàng)意類(lèi)問(wèn)答時(shí)才需要打開(kāi)temperature。3.3 4bit 量化把大模型塞進(jìn)小顯存單卡 24G 是很多人的現(xiàn)實(shí)條件這時(shí)候 4bit 量化幾乎是唯一選擇。做法不復(fù)雜加一個(gè)量化配置對(duì)象就行import torch from transformers import AutoModelForCausalLM, LlamaTokenizer, BitsAndBytesConfig MODEL_PATH /data/models/cogvlm-chat-hf bnb_config BitsAndBytesConfig( load_in_4bitTrue, bnb_4bit_quant_typenf4, bnb_4bit_compute_dtypetorch.bfloat16, bnb_4bit_use_double_quantTrue, ) tokenizer LlamaTokenizer.from_pretrained(MODEL_PATH, trust_remote_codeTrue) model AutoModelForCausalLM.from_pretrained( MODEL_PATH, quantization_configbnb_config, torch_dtypetorch.bfloat16, low_cpu_mem_usageTrue, trust_remote_codeTrue, device_mapcuda:0, ).eval()nf4是正態(tài)分布友好的 4bit 數(shù)據(jù)類(lèi)型比普通 int4 在這類(lèi)模型上掉的精度少double_quant是對(duì)量化常數(shù)再做一次量化能再省一兩個(gè) G代價(jià)是加載稍微慢一點(diǎn)。compute_dtype一定要設(shè)成 bf16設(shè)成 fp16 在部分卡上會(huì)出現(xiàn)激活溢出輸出變成一堆重復(fù)的字符或者直接 NaN。量化之后的實(shí)際體驗(yàn)在 24G 卡上跑 17B 模型留出 4G 左右給上下文對(duì)話輪次控制在 3 輪以內(nèi)基本不會(huì) OOM。速度方面生成一句話二三十個(gè) token大概一兩秒描述一張中等復(fù)雜度的圖兩百 token十幾秒的量級(jí)。這個(gè)速度做批處理不算快做交互式工具完全夠用。注意量化后模型對(duì)數(shù)字和精確坐標(biāo)的敏感度會(huì)下降。做 GUI 定位時(shí)如果發(fā)現(xiàn)坐標(biāo)明顯漂移先試著回到半精度驗(yàn)證是不是量化導(dǎo)致的別一頭扎進(jìn)坐標(biāo)換算里查半天。3.4 雙卡切分與那些說(shuō)不清的報(bào)錯(cuò)顯存夠但單卡裝不下時(shí)第一反應(yīng)是把device_map改成auto。這一步能成但成功率不高。原因在于這類(lèi)模型的視覺(jué)專(zhuān)家模塊是通過(guò)自定義邏輯插入的accelerate的自動(dòng)切分策略不一定能正確識(shí)別這些層結(jié)果就是一部分權(quán)重被放到 cuda:1但計(jì)算時(shí)張量在 cuda:0報(bào)一個(gè) device mismatch。我的處理順序是這樣的先試device_mapauto能跑就不管了報(bào) device 錯(cuò)誤就改成手工指定的字典把視覺(jué)相關(guān)的層固定在第一張卡上語(yǔ)言層往后排。手工字典寫(xiě)起來(lái)麻煩但可控。另一個(gè)辦法是干脆用兩張卡各跑一個(gè)模型實(shí)例前面掛個(gè)簡(jiǎn)單的調(diào)度做并發(fā)處理這在批處理場(chǎng)景下反而更簡(jiǎn)單粗暴有效。還有一個(gè)常見(jiàn)問(wèn)題是雙卡機(jī)器上的 NVLink 缺失。沒(méi)有 NVLink 時(shí)跨卡通信走 PCIe速度會(huì)掉一截特別是層間頻繁交換數(shù)據(jù)的結(jié)構(gòu)。如果你的機(jī)器沒(méi)有 NVLink別對(duì)雙卡加速抱太大期望能跑通就行。4. CogAgent 的 GUI 能力怎么真正用起來(lái)CogVLM 的用法相對(duì)直白進(jìn)去一張圖出來(lái)一段字。CogAgent 的鏈路長(zhǎng)得多截屏、預(yù)處理、構(gòu)造 prompt、推理、解析動(dòng)作、坐標(biāo)換算、執(zhí)行、再截屏形成一個(gè)閉環(huán)。任何一個(gè)環(huán)節(jié)沒(méi)對(duì)齊整個(gè)鏈路就是錯(cuò)的而且很難從最終現(xiàn)象倒推是哪一環(huán)。4.1 分辨率與 letterbox 預(yù)處理高分辨率是 CogAgent 的核心賣(mài)點(diǎn)但“高分辨率”不等于“直接把原圖丟進(jìn)去”。模型有一個(gè)固定的輸入尺寸通常是 1120 這個(gè)量級(jí)你的截圖可能是 1920x1080也可能是 2560x1440直接 resize 會(huì)拉伸變形模型對(duì)按鈕形狀的判斷會(huì)受影響。正確做法是等比縮放加填充也就是 letterbox。from PIL import Image def letterbox(img, target1120): w, h img.size scale min(target / w, target / h) nw, nh int(round(w * scale)), int(round(h * scale)) resized img.resize((nw, nh), Image.BICUBIC) canvas Image.new(RGB, (target, target), (0, 0, 0)) pad_x (target - nw) // 2 pad_y (target - nh) // 2 canvas.paste(resized, (pad_x, pad_y)) return canvas, scale, pad_x, pad_y函數(shù)返回的scale、pad_x、pad_y三個(gè)值要一路帶著走后面坐標(biāo)換算全靠它們。這里用 BICUBIC 而不是默認(rèn)的 NEAREST是因?yàn)榻缑嫔系男∽趾图?xì)線條在 NEAREST 下會(huì)丟信息縮完以后模型讀不出來(lái)。填充色用黑色0,0,0比較穩(wěn)妥。有些實(shí)現(xiàn)用白色在某些淺色主題的界面上會(huì)和內(nèi)容混在一起模型分不清邊界。4.2 動(dòng)作解析別信“模型會(huì)給你標(biāo)準(zhǔn)格式”模型輸出的動(dòng)作文本理想情況下是規(guī)整的一行比如“點(diǎn)擊坐標(biāo) (x, y)”。實(shí)際跑起來(lái)你會(huì)發(fā)現(xiàn)它偶爾加一句解釋、偶爾換個(gè)動(dòng)詞、偶爾把坐標(biāo)寫(xiě)進(jìn)括號(hào)里又跟一句補(bǔ)充說(shuō)明。所以解析層必須寫(xiě)得寬容一些用正則去撈關(guān)鍵信息而不是拿字符串精確匹配。我一般的做法是先按行切找包含動(dòng)作關(guān)鍵詞的那一行再用正則提取動(dòng)作類(lèi)型和參數(shù)。動(dòng)作類(lèi)型做一層映射把同義詞歸到統(tǒng)一的枚舉里比如“點(diǎn)擊”“按”“點(diǎn)一下”統(tǒng)一成 CLICK。坐標(biāo)提取用\(?\s*(\d)\s*[,]\s*(\d)\s*\)?這種寬松模式中英文逗號(hào)都要考慮。解析失敗時(shí)不要直接拋異常讓流程中斷應(yīng)該記錄原始輸出、記錄當(dāng)時(shí)的截圖然后走一個(gè)兜底分支。這個(gè)日志在調(diào) prompt 的時(shí)候價(jià)值極高你能看到模型到底在哪一步“想偏了”。4.3 坐標(biāo)換算與標(biāo)定這是整個(gè)鏈路里最容易出錯(cuò)的地方。模型輸出的坐標(biāo)是在 1120 空間里的你要換成真實(shí)屏幕坐標(biāo)公式是def to_screen(x_model, y_model, scale, pad_x, pad_y): x (x_model - pad_x) / scale y (y_model - pad_y) / scale return int(round(x)), int(round(y))看起來(lái)很簡(jiǎn)單但有兩個(gè)陷阱。第一有些模型的輸出坐標(biāo)已經(jīng)內(nèi)部還原到原圖了你再還原一次就錯(cuò)得離譜。第二縮放用的是min而不是等比拉伸如果原圖不是正方形橫縱比例其實(shí)是一致的但填充偏移只在某一個(gè)方向上有。判斷到底是哪種情況靠標(biāo)定。截一張圖上面畫(huà)幾個(gè)位置已知的標(biāo)記讓模型去找其中一個(gè)看輸出的數(shù)值落在哪個(gè)區(qū)間。如果數(shù)值都在 0 到 1120 之間那就是縮放空間如果數(shù)值接近原圖寬高那就是已經(jīng)還原過(guò)。這個(gè)測(cè)試做一次后面就不用反復(fù)猜了。實(shí)操心得標(biāo)定的時(shí)候多用幾個(gè)不同位置的元素比如左上角、右下角、正中心各一個(gè)。如果中心點(diǎn)準(zhǔn)而邊角偏說(shuō)明是填充偏移沒(méi)減對(duì)如果整體線性偏移說(shuō)明縮放比算錯(cuò)了如果只有某個(gè)方向偏檢查一下是不是寬高搞反了。5. 踩坑實(shí)錄與排查速查表這一節(jié)把我實(shí)際遇到的和同行反饋過(guò)的問(wèn)題整理成表按現(xiàn)象倒查原因。這類(lèi)問(wèn)題的特點(diǎn)是報(bào)錯(cuò)信息經(jīng)?!拔牟粚?duì)題”看報(bào)錯(cuò)猜原因大概率猜錯(cuò)還是按現(xiàn)象查比較快。5.1 加載階段的問(wèn)題現(xiàn)象大概率原因處理方式KeyError: xxx出現(xiàn)在 config 解析處transformers版本過(guò)新或過(guò)舊回退到 4.37 到 4.42 區(qū)間AttributeError: module has no attribute _supports_sdpa版本不匹配的典型表現(xiàn)同上鎖版本而非改代碼safetensors解析失敗權(quán)重分片不完整核對(duì)分片數(shù)量與 index 文件CUDA out of memory在加載時(shí)未開(kāi)low_cpu_mem_usage加上該參數(shù)并考慮量化卡在Loading checkpoint shards很久磁盤(pán) IO 瓶頸或內(nèi)存不足換數(shù)據(jù)盤(pán)檢查 swap 是否被瘋狂使用No module named xxx模型自帶代碼依賴了沒(méi)裝的包看報(bào)錯(cuò)補(bǔ)裝注意版本別太新加載階段的問(wèn)題九成能靠“鎖版本 查文件完整性”解決。我遇到過(guò)最詭異的一次是 safetensors 報(bào)錯(cuò)查了半天發(fā)現(xiàn)是拷貝過(guò)程中磁盤(pán)滿了最后一個(gè)分片被截?cái)?。這種問(wèn)題報(bào)錯(cuò)信息完全不指向磁盤(pán)只能靠比對(duì)文件大小發(fā)現(xiàn)。5.2 推理階段的問(wèn)題推理階段的現(xiàn)象更隱蔽因?yàn)槌绦虿粓?bào)錯(cuò)只是結(jié)果不對(duì)。輸出全是重復(fù)字符或者亂碼。第一反應(yīng)是量化精度問(wèn)題特別是compute_dtype設(shè)成了 fp16 的情況。改成 bf16 通常就好。如果沒(méi)開(kāi)量化也這樣檢查一下是不是max_new_tokens設(shè)得太大配合了錯(cuò)誤的eos_token_id模型不知道什么時(shí)候停。答非所問(wèn)或者把圖片內(nèi)容完全說(shuō)錯(cuò)。先確認(rèn)圖像張量的 dtype 和 device 都對(duì)。我踩過(guò)一次圖像張量留在了 CPU 上模型居然沒(méi)報(bào)錯(cuò)只是把圖像當(dāng)成全黑處理輸出了一段完全不相干的描述。這種“靜默失敗”最坑所以加載完之后一定要打印一下輸入張量的 shape、dtype 和 device 做核對(duì)。生成到一半卡住不動(dòng)。多半是顯存臨界系統(tǒng)在做內(nèi)存和顯存之間的交換。把max_new_tokens降下來(lái)或者減少歷史輪次。多輪對(duì)話越跑越慢。KV Cache 在累積。這類(lèi)模型每輪都要重新處理歷史長(zhǎng)對(duì)話下顯存和耗時(shí)都是線性增長(zhǎng)的。做批處理時(shí)我一般每張圖開(kāi)一個(gè)全新會(huì)話不共享歷史反而更快更穩(wěn)。5.3 GUI 定位專(zhuān)項(xiàng)問(wèn)題GUI 場(chǎng)景的問(wèn)題和通用理解不太一樣單獨(dú)拎出來(lái)說(shuō)。坐標(biāo)系統(tǒng)性偏移。大概率是 letterbox 參數(shù)沒(méi)跟上。檢查縮放比是用min算的還是用寬高分別算的填充偏移有沒(méi)有在不同分辨率下重新計(jì)算。我見(jiàn)過(guò)一個(gè)實(shí)現(xiàn)把pad_x、pad_y寫(xiě)成了全局常量換一臺(tái)顯示器就全錯(cuò)。模型說(shuō)“找不到該元素”。先看截圖里那個(gè)元素是不是太小或者對(duì)比度太低。高分辨率輸入的意義就在這如果元素在縮放后只有幾個(gè)像素模型確實(shí)看不見(jiàn)。解決辦法是局部截圖——先把屏幕分成幾塊逐塊送進(jìn)去找找到了再在原圖里換算。這個(gè)策略在復(fù)雜界面上比整屏一次送入準(zhǔn)確得多。動(dòng)作類(lèi)型判斷錯(cuò)誤。比如該點(diǎn)擊的它說(shuō)要輸入。這種情況多半是 prompt 描述得不夠明確。把當(dāng)前界面的上下文、上一步做了什么、目標(biāo)是什么寫(xiě)清楚準(zhǔn)確率會(huì)明顯上升。GUI 任務(wù)對(duì) prompt 的敏感度遠(yuǎn)高于通用問(wèn)答。連續(xù)兩步動(dòng)作互相矛盾。因?yàn)槊恳徊蕉际仟?dú)立推理模型沒(méi)有記憶。要么把歷史動(dòng)作拼進(jìn) prompt要么在外部加一層狀態(tài)機(jī)做約束。我傾向于后者狀態(tài)機(jī)邏輯清晰、可控模型只負(fù)責(zé)“看”決策交給代碼。6. 讓它跑得更好幾個(gè)真正有效的調(diào)優(yōu)點(diǎn)模型能跑起來(lái)只是及格線實(shí)際用起來(lái)你會(huì)發(fā)現(xiàn)效果差異很大而差異的來(lái)源往往不是模型本身是輸入的組織方式。6.1 prompt 的組織方式視覺(jué)語(yǔ)言模型對(duì) prompt 的敏感度比純文本模型高因?yàn)閳D像信息和文本信息要在同一個(gè)上下文里對(duì)齊。幾條我驗(yàn)證過(guò)有效的經(jīng)驗(yàn)提問(wèn)要具體不要說(shuō)“描述這張圖”要說(shuō)“圖中表格第三行第二列的內(nèi)容是什么”。前者模型會(huì)給你一段泛泛而談后者能直接命中你要的信息。需要結(jié)構(gòu)化輸出時(shí)在 prompt 里給格式示例比事后用正則去撈要可靠得多。比如你要 JSON就在 prompt 里寫(xiě)一個(gè) JSON 骨架模型會(huì)照著填。中文和英文混著用一般沒(méi)問(wèn)題但涉及界面元素名稱(chēng)時(shí)用界面上實(shí)際顯示的語(yǔ)言更準(zhǔn)。界面上寫(xiě)的是英文按鈕你問(wèn)“那個(gè)藍(lán)色的確認(rèn)按鈕在哪”不如問(wèn)“Where is the Submit button”。做 OCR 類(lèi)任務(wù)時(shí)明確告訴模型“只輸出識(shí)別到的文字不要解釋”能省掉大量后處理。6.2 推理參數(shù)的取舍參數(shù)不多但每個(gè)都影響結(jié)果參數(shù)通用理解任務(wù)GUI 定位任務(wù)說(shuō)明do_sample可開(kāi)可關(guān)必須關(guān)采樣會(huì)引入不確定性temperature0.2 到 0.7不適用關(guān)閉采樣后此參數(shù)失效max_new_tokens256 到 51264 到 128定位類(lèi)輸出很短設(shè)大了浪費(fèi)repetition_penalty1.0 到 1.11.0過(guò)高會(huì)讓輸出變得生硬num_beams11束搜索在這類(lèi)任務(wù)上收益很低GUI 任務(wù)里我把max_new_tokens壓到 128 甚至 64效果反而是提升的。原因很簡(jiǎn)單輸出空間被限制后模型沒(méi)有余地在那里“思考人生”必須盡快給出動(dòng)作。這是個(gè)很實(shí)用的小技巧。6.3 批處理與吞吐如果要做批量圖片理解逐張串行跑會(huì)非常慢??尚械募铀俜绞接袔讉€(gè)層次第一層是把多張圖拼成一個(gè) batch 送進(jìn)去。這類(lèi)模型對(duì) batch 的支持要看具體實(shí)現(xiàn)有的版本在build_conversation_input_ids層面就只支持單圖硬拼會(huì)出錯(cuò)得自己改預(yù)處理邏輯。改動(dòng)有風(fēng)險(xiǎn)建議先在小樣本上驗(yàn)證結(jié)果一致性。第二層是起多個(gè)進(jìn)程每個(gè)進(jìn)程占一張卡或者共享一張卡的不同顯存段。簡(jiǎn)單粗暴但有效尤其是處理幾千張圖的離線任務(wù)多進(jìn)程的收益比任何模型層面的優(yōu)化都直接。第三層是推理框架。有些通用推理加速框架對(duì)這類(lèi)自定義結(jié)構(gòu)的支持有限需要額外的適配工作投入產(chǎn)出比要先算清楚。如果只是自己用多進(jìn)程加 4bit 量化基本夠如果要上生產(chǎn)那值得花時(shí)間做專(zhuān)門(mén)的適配。提示批處理時(shí)務(wù)必給每張圖記錄完整的輸入?yún)?shù)和輸出原文。模型偶爾會(huì)在某張圖上抽風(fēng)沒(méi)有日志你根本不知道是哪張、為什么。我習(xí)慣把原圖、prompt、原始輸出、解析后的結(jié)果四樣一起落盤(pán)出問(wèn)題直接翻記錄。7. 我在這套流程里攢下的幾點(diǎn)體會(huì)第一點(diǎn)關(guān)于顯存。別一上來(lái)就追求半精度跑滿血版本量化帶來(lái)的精度損失在大多數(shù)任務(wù)里感知不到但省下來(lái)的顯存能讓你把上下文拉長(zhǎng)、把 batch 開(kāi)大綜合體驗(yàn)反而更好。真正需要半精度的場(chǎng)景是坐標(biāo)精度要求極高的 GUI 定位和細(xì)小文字的 OCR這兩種情況我才會(huì)回到 bf16 并接受更慢的速度。第二點(diǎn)關(guān)于版本。這類(lèi)模型的自定義代碼是把雙刃劍它讓模型能做很多標(biāo)準(zhǔn)架構(gòu)做不到的事也讓部署變得脆弱。我的做法是每個(gè)項(xiàng)目建一個(gè)獨(dú)立環(huán)境并凍結(jié)依賴寧可磁盤(pán)多占幾個(gè) G也不要讓兩個(gè)項(xiàng)目共用一個(gè)環(huán)境互相打架。這個(gè)習(xí)慣救過(guò)我很多次。第三點(diǎn)關(guān)于坐標(biāo)。GUI 相關(guān)的坑九成能在標(biāo)定這一步提前發(fā)現(xiàn)。別嫌麻煩畫(huà)幾個(gè)標(biāo)記點(diǎn)測(cè)一次把縮放比和偏移量打印出來(lái)肉眼確認(rèn)數(shù)值合理再往下接自動(dòng)化。我見(jiàn)過(guò)太多人跳過(guò)這一步然后在“為什么點(diǎn)不準(zhǔn)”上耗掉一整天。最后分享一個(gè)排查思路。當(dāng)模型輸出明顯不對(duì)時(shí)先別改 prompt、別調(diào)參數(shù)先把輸入可視化出來(lái)看一眼——圖像有沒(méi)有變形、有沒(méi)有被裁掉、顏色通道對(duì)不對(duì)、張量在不在正確的設(shè)備上。我遇到過(guò)的“模型不行”里至少一半最后發(fā)現(xiàn)是輸入喂錯(cuò)了。把輸入管好剩下的問(wèn)題才好談。