首页
/ llama.cpp 多模态实战:Granite Vision 模型从 HF 权重到 GGUF 的完整转换流程

llama.cpp 多模态实战:Granite Vision 模型从 HF 权重到 GGUF 的完整转换流程

2026-09-04 14:59:28作者:何将鹤

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 归入这类指南)。

本文的五个阶段对应:

  1. 运行 llava surgery v2,拆分视觉编码器与投影器;
  2. convert_image_encoder_to_gguf.py 生成视觉组件 GGUF(mmproj);
  3. 从复合模型中导出 LLM 并转成 GGUF;
  4. (可选)量化 LLM;
  5. 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_towervit.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_projectorvision_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_sizeintermediate_sizenum_attention_headsnum_hidden_layerspatch_sizeimage_sizelayer_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_pinpointsmm_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.ggufmmproj- 前缀 + 默认 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.ggufgranite_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

实践中的几个易错点,均来自原文档与源码:

  1. image_grid_pinpoints 必须与具体模型匹配,以模型自身的 config.json 为准;
  2. SigLIP 编码器必须显式传 --clip-model-is-siglip 并覆盖 mean/std 为 [0.5, 0.5, 0.5],否则归一化与模型结构都会走 CLIP 默认分支;
  3. transformers 版本需足够新(4.49+),否则加载复合模型时会出现 size mismatch;
  4. 视觉编码器无法量化(SigLIP 张量维度不满足 32 整除约束),只能对 LLM 部分量化;
  5. 多模态支持在 llama.cpp 中属于高活跃开发中的子项目,tools/mtmd/README.md 明确提示存在破坏性变更的可能,转换脚本以仓库当前版本为准。

如需查看 Granite Vision 之外的其他多模态模型转换指南(LLaVA、MobileVLM、GLM-Edge、MiniCPM 系列等),可参考 docs/multimodal/ 目录下的各篇文档及 tools/mtmd/README.md 的模型清单。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
981
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384