llama.cpp 多模态实战:Granite Vision 模型从 HF 权重到 GGUF 的完整转换流程
Granite Vision(如 IBM 的 granite-vision-3.2-2b)由 SigLIP 视觉编码器 + Granite 语言模型组成,在 llama.cpp 中需要通过“外科手术式”拆权重、分别转换视觉投影组件(mmproj)与语言模型两个 GGUF 文件,最终用 llama-mtmd-cli 统一加载运行。本文基于仓库官方文档 docs/multimodal/granitevision.md 逐步骤展开,并结合 tools/mtmd/legacy-models 下的转换脚本与 tools/mtmd/models/llava.cpp 的推理图实现,讲清每一步“为什么这么做”。读完你可以独立完成:下载模型 → 拆分视觉塔与投影器 → 生成视觉组件 GGUF → 导出并转换 LLM → 量化 → 用 mtmd CLI 运行。
整体结构:一个模型,两个 GGUF
llama.cpp 的多模态支持的工作方式是:用独立的视觉组件把图片编码成 embedding,再送入语言模型;视觉组件与核心 libllama 保持分离,因此运行一个多模态模型通常需要两个 GGUF 文件(标准语言模型 + 对应的多模态投影器 mmproj),这一点在 tools/mtmd/README.md 中有明确说明。Granite Vision 属于“旧式”多模态模型,其 mmproj 不能直接用 convert_hf_to_gguf.py --mmproj 一键生成,需要走 legacy-models 目录下的传统流程(README 将 IBM Granite Vision 归入这类指南)。
本文的五个阶段对应:
- 运行 llava surgery v2,拆分视觉编码器与投影器;
- 用
convert_image_encoder_to_gguf.py生成视觉组件 GGUF(mmproj); - 从复合模型中导出 LLM 并转成 GGUF;
- (可选)量化 LLM;
- 用
llama-mtmd-cli运行。
0. 准备:下载模型并设置 GRANITE_MODEL
首先克隆 Granite Vision 模型,并把 GRANITE_MODEL 环境变量指向模型目录:
$ git clone https://huggingface.co/ibm-granite/granite-vision-3.2-2b
$ export GRANITE_MODEL=./granite-vision-3.2-2b
后续所有步骤都依赖这两个环境变量(GRANITE_MODEL 指向 HF 权重目录,LLM_EXPORT_PATH 在第三步引入)。
1. 运行 llava surgery v2:拆分视觉塔与投影器
第一步运行 LLaVA 手术脚本 v2:
$ python llava_surgery_v2.py -C -m $GRANITE_MODEL
该脚本在仓库中的实际位置是 tools/mtmd/legacy-models/llava_surgery_v2.py(仓库中旧版多模态转换脚本统一放在 tools/mtmd/legacy-models 目录下,tools/mtmd/README.md 中有此说明),因此实际执行时可写作 python tools/mtmd/legacy-models/llava_surgery_v2.py -C -m $GRANITE_MODEL。
执行后,模型的目录中应多出两个文件:
$ ls $GRANITE_MODEL | grep -i llava
llava.clip
llava.projector
投影器与视觉编码器被拆分成独立的 llava 文件。快速校验它们非空:
import os
import torch
MODEL_PATH = os.getenv("GRANITE_MODEL")
if not MODEL_PATH:
raise ValueError("env var GRANITE_MODEL is unset!")
encoder_tensors = torch.load(os.path.join(MODEL_PATH, "llava.clip"))
projector_tensors = torch.load(os.path.join(MODEL_PATH, "llava.projector"))
assert len(encoder_tensors) > 0
assert len(projector_tensors) > 0
如果进一步检查 .keys(),encoder_tensors 中应有大量 vision_model 张量,而多模态 projector_tensors 中应有 5 个张量:'multi_modal_projector.linear_1.bias'、'multi_modal_projector.linear_1.weight'、'multi_modal_projector.linear_2.bias'、'multi_modal_projector.linear_2.weight'、'image_newline'。
源码视角:手术脚本如何识别组件
从 llava_surgery_v2.py 的实现看,拆分逻辑基于张量名匹配,因此它对 PyTorch .bin 与 SafeTensors 两种格式都通用:
is_vision_tower()匹配以model.vision_tower、vit.、vision_tower开头的张量,-C(--clean-vision-tower)参数会把这些张量从原始 checkpoint 中剥离、写入llava.clip,并把名字中vision_model.之后的部分作为保存键(源码第 85 行:simple_name = name[name.index('vision_model.'):] if 'vision_model.' in name else name)——这正解释了为什么llava.clip里出现的是vision_model.*键;is_mm_projector()匹配model.mm_projector、vision_proj.、multi_modal_projector前缀,is_newline()匹配image_newline;脚本在分片中定位包含投影器与 newline 张量的 checkpoint,把它们统一以float()精度存入llava.projector(源码第 166-176 行)。
Granite Vision 的投影器恰好就是 multi_modal_projector 命名,因此这套为 LLaVA 设计的脚本可以直接复用。脚本结尾也会提示下一步:用 llava.projector 准备 encoder GGUF 文件。
2. 生成视觉组件 GGUF(mmproj)
2.1 组织视觉编码器目录
新建一个目录存放视觉组件,并复制 llava.clip/llava.projector 文件:
$ ENCODER_PATH=$PWD/visual_encoder
$ mkdir $ENCODER_PATH
$ cp $GRANITE_MODEL/llava.clip $ENCODER_PATH/pytorch_model.bin
$ cp $GRANITE_MODEL/llava.projector $ENCODER_PATH/
2.2 编写视觉编码器 config.json
接下来需要为视觉编码器写一份 config。转换时务必使用正确的 image_grid_pinpoints,因为它因模型而异——原始值可以在 $GRANITE_MODEL/config.json 中查到。Granite Vision 3.2-2B 对应的完整配置如下:
{
"_name_or_path": "siglip-model",
"architectures": [
"SiglipVisionModel"
],
"image_grid_pinpoints": [
[384,384],
[384,768],
[384,1152],
[384,1536],
[384,1920],
[384,2304],
[384,2688],
[384,3072],
[384,3456],
[384,3840],
[768,384],
[768,768],
[768,1152],
[768,1536],
[768,1920],
[1152,384],
[1152,768],
[1152,1152],
[1536,384],
[1536,768],
[1920,384],
[1920,768],
[2304,384],
[2688,384],
[3072,384],
[3456,384],
[3840,384]
],
"mm_patch_merge_type": "spatial_unpad",
"hidden_size": 1152,
"image_size": 384,
"intermediate_size": 4304,
"model_type": "siglip_vision_model",
"num_attention_heads": 16,
"num_hidden_layers": 27,
"patch_size": 14,
"layer_norm_eps": 1e-6,
"hidden_act": "gelu_pytorch_tanh",
"projection_dim": 0,
"vision_feature_layer": [-24, -20, -12, -1]
}
这份配置中有几个字段值得结合转换脚本理解(tools/mtmd/legacy-models/convert_image_encoder_to_gguf.py):
hidden_size、intermediate_size、num_attention_heads、num_hidden_layers、patch_size、image_size、layer_norm_eps会被逐项写入 GGUF 的clip.vision.*元数据(脚本第 271-277 行),也就是推理时重建 SigLIP ViT 结构所需的超参数;vision_feature_layer: [-24, -20, -12, -1]是 Granite Vision 的关键特性——它从视觉编码器的多个层抽取特征。转换脚本中的get_non_negative_vision_feature_layers()(脚本第 233-258 行)把这些负索引按num_hidden_layers + layer_idx + 1转换为非负索引(因为 hidden states 数组形式为[<emb input>, <block 0 输出>, ..., <block N 输出>],索引需整体偏移 +1),并允许-1作为“未设置”的哨兵值;projection_dim: 0是有意的:脚本对 SigLIP 分支明确“Siglip does not have a visual projector; set projection dim to 0”(脚本第 264-266 行),视觉输出维度改由后面的 MLP 投影器负责;image_grid_pinpoints与mm_patch_merge_type: spatial_unpad定义了输入分辨率档位与 patch 合并方式,写错会导致图片特征与投影器维度不匹配,所以文档特别提醒以模型自身的config.json为准。
此时目录结构应如下:
$ ls $ENCODER_PATH
config.json llava.projector pytorch_model.bin
2.3 转换为 GGUF
$ python convert_image_encoder_to_gguf.py \
-m $ENCODER_PATH \
--llava-projector $ENCODER_PATH/llava.projector \
--output-dir $ENCODER_PATH \
--clip-model-is-vision \
--clip-model-is-siglip \
--image-mean 0.5 0.5 0.5 \
--image-std 0.5 0.5 0.5
脚本实际路径为 tools/mtmd/legacy-models/convert_image_encoder_to_gguf.py。各参数含义(结合脚本第 90-115 行的参数定义):
| 参数 | 作用 |
|---|---|
-m / --model-dir |
HF 克隆下来的模型目录(这里是 $ENCODER_PATH),脚本从中读取 config.json |
--llava-projector |
指定 llava.projector 文件;一旦给出,文件名前缀即为 mmproj-,且标记 clip.has_llava_projector = true |
--output-dir |
GGUF 输出目录,默认为原模型目录 |
--clip-model-is-vision |
声明 clip 目录是纯视觉模型(无 vocab.json/tokenizer,脚本据此跳过文本分支,第 146-148 行) |
--clip-model-is-siglip |
声明视觉编码器是 SigLIP,脚本改用 SiglipVisionModel.from_pretrained() 加载而非 CLIP 家族(第 164-166 行);与 --clip-model-is-openclip 互斥 |
--image-mean / --image-std |
覆盖图像归一化均值/方差,脚本注释中给出的 SigLIP 示例正是 0.5 0.5 0.5(第 110-111 行) |
关于归一化参数:文档特别说明因为 Granite Vision 使用 SigLIP 视觉编码器,这里要把图像 mean/std 覆盖为 [0.5, 0.5, 0.5];这些数值在 transformers 侧可于模型的 preprocessor_config.json 中查到。若不指定,脚本默认使用 CLIP 的经典值 [0.48145466, 0.4578275, 0.40821073] / [0.26862954, 0.26130258, 0.27577711](脚本第 112-113 行),归一化不一致会直接劣化视觉理解效果。
执行后会在 $ENCODER_PATH 下生成第一个 GGUF 文件 mmproj-model-f16.gguf(mmproj- 前缀 + 默认 f16 精度,见脚本第 181-194 行),我们把它的绝对路径记作 $VISUAL_GGUF_PATH。
2.4 推理侧印证:feature_layers 在 mtmd 中的用途
转换出来的 vision_feature_layer 元数据并非摆设。在 tools/mtmd/models/llava.cpp 中,该图用于 llava/granite/glm 三族模型(源码注释:this graph is used by llava, granite and glm),且 hparams.feature_layers 处有明确注释:
// If we set explicit vision feature layers, only go up to the deepest one
// NOTE: only used by granite-vision models for now
即 ViT 前向传播时只计算到最深一个特征层为止,并对每个特征层把中间状态压入 embedding_stack(源码注释解释:因为 granite 用到 embedding_stack,无法复用 build_vit)。也就是说文档里 config 中的 [-24, -20, -12, -1] 会在运行时被真实消费,多尺度视觉特征随后由双线性 MLP 投影器(multi_modal_projector.linear_1/2 + image_newline)合并进语言模型的 embedding 空间。
3. 生成 LLM GGUF
Granite Vision 的语言模型部分是一个 Granite LLM。目前最简单的方式是用 transformers 加载复合模型、把 LLM 子模型导出,再走常规转换路径。先设置导出目录:
$ export LLM_EXPORT_PATH=$PWD/granite_vision_llm
导出脚本:
import os
import transformers
MODEL_PATH = os.getenv("GRANITE_MODEL")
if not MODEL_PATH:
raise ValueError("env var GRANITE_MODEL is unset!")
LLM_EXPORT_PATH = os.getenv("LLM_EXPORT_PATH")
if not LLM_EXPORT_PATH:
raise ValueError("env var LLM_EXPORT_PATH is unset!")
tokenizer = transformers.AutoTokenizer.from_pretrained(MODEL_PATH)
# NOTE: granite vision support was added to transformers very recently (4.49);
# if you get size mismatches, your version is too old.
# If you are running with an older version, set `ignore_mismatched_sizes=True`
# as shown below; it won't be loaded correctly, but the LLM part of the model that
# we are exporting will be loaded correctly.
model = transformers.AutoModelForImageTextToText.from_pretrained(MODEL_PATH, ignore_mismatched_sizes=True)
tokenizer.save_pretrained(LLM_EXPORT_PATH)
model.language_model.save_pretrained(LLM_EXPORT_PATH)
注意文档中的版本提示:transformers 对 granite vision 的支持非常新(4.49 才加入),如果遇到尺寸不匹配报错,说明版本过旧;老版本下用 ignore_mismatched_sizes=True 可以跳过视觉部分的加载错误,而我们只导出的正是 model.language_model(LLM 部分)与 tokenizer,所以不受影响。
导出完成后,用仓库根目录的标准转换器 convert_hf_to_gguf.py 转成 GGUF:
$ LLM_GGUF_PATH=$LLM_EXPORT_PATH/granite_llm.gguf
...
$ python convert_hf_to_gguf.py --outfile $LLM_GGUF_PATH $LLM_EXPORT_PATH
4. 量化(可选)
如果想量化 LLM,可像对待任何 LLM 一样使用 llama-quantize:
$ ./build/bin/llama-quantize $LLM_EXPORT_PATH/granite_llm.gguf $LLM_EXPORT_PATH/granite_llm_q4_k_m.gguf Q4_K_M
$ LLM_GGUF_PATH=$LLM_EXPORT_PATH/granite_llm_q4_k_m.gguf
重要限制:目前无法量化视觉编码器。因为 Granite Vision 使用 SigLIP,其部分张量维度不能被 32 整除,而 llama.cpp 的量化类型(如 Q4_K 等 block 量化)要求块对齐的维度。所以 mmproj-model-f16.gguf 保持 f16 精度,这也是输出文件命名中 -f16 后缀的由来。
仓库的 CI 测试也印证了这一量化形态:tools/mtmd/tests.sh 中的多模态测试用到了预量化的 ibm-research/granite-vision-3.2-2b-GGUF:Q4_K_M,即 LLM 部分为 Q4_K_M。
5. 用 llama.cpp 运行模型
按正常流程构建 llama.cpp 后,应得到名为 llama-mtmd-cli 的二进制(它由 tools/mtmd 中介绍的 libmtmd 统一 CLI 驱动,是整合了各模型专属 CLI 后的单一入口)。向其传入两个 GGUF 二进制,例如用 llama.cpp 的横幅图作为输入图片:
$ ./build/bin/llama-mtmd-cli -m $LLM_GGUF_PATH \
--mmproj $VISUAL_GGUF_PATH \
-c 16384 \
--temp 0
参数说明:
-m:语言模型 GGUF(第 3/4 步的granite_llm.gguf或granite_llm_q4_k_m.gguf);--mmproj:视觉组件 GGUF(第 2 步的mmproj-model-f16.gguf);-c 16384:上下文长度;--temp 0:温度置 0 做确定性解码,适合验证输出稳定可复现。
启动后按提示粘贴图片路径(或拖入终端),即可进行图文问答。
流程小结与注意事项
| 阶段 | 产物 | 关键脚本/二进制 |
|---|---|---|
| 拆权重 | llava.clip + llava.projector |
llava_surgery_v2.py(-C -m $GRANITE_MODEL) |
| 视觉组件 | mmproj-model-f16.gguf |
convert_image_encoder_to_gguf.py(--clip-model-is-siglip,mean/std 覆盖为 0.5) |
| 语言模型 | granite_llm.gguf |
transformers 导出 language_model + convert_hf_to_gguf.py |
| 量化(仅 LLM) | granite_llm_q4_k_m.gguf |
llama-quantize ... Q4_K_M |
| 运行 | 图文交互 | llama-mtmd-cli -m ... --mmproj ... -c 16384 --temp 0 |
实践中的几个易错点,均来自原文档与源码:
image_grid_pinpoints必须与具体模型匹配,以模型自身的config.json为准;- SigLIP 编码器必须显式传
--clip-model-is-siglip并覆盖 mean/std 为[0.5, 0.5, 0.5],否则归一化与模型结构都会走 CLIP 默认分支; - transformers 版本需足够新(4.49+),否则加载复合模型时会出现 size mismatch;
- 视觉编码器无法量化(SigLIP 张量维度不满足 32 整除约束),只能对 LLM 部分量化;
- 多模态支持在 llama.cpp 中属于高活跃开发中的子项目,tools/mtmd/README.md 明确提示存在破坏性变更的可能,转换脚本以仓库当前版本为准。
如需查看 Granite Vision 之外的其他多模态模型转换指南(LLaVA、MobileVLM、GLM-Edge、MiniCPM 系列等),可参考 docs/multimodal/ 目录下的各篇文档及 tools/mtmd/README.md 的模型清单。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00