新闻
模型
API
keyboard_arrow_down
读取器
读取URL或搜索为大模型提供更好的依据。
向量模型
世界一流的多模态多语言向量模型。
重排器
世界一流的重排器,最大限度地提高搜索相关性。
弹性推理服务
在 Elasticsearch 中原生运行 Jina 模型。
MCP terminal命令行articlellms.txtsmart_toy代理人data_object模式menu_book文档



登录
login
了解 Llama.cpp 中的图像输入
关注图像词元
我们的修复
评估
剩余问题
结论
技术博客
九月 09, 2025

Llama.cpp 和 GGUF 中的多模态向量模型

我们为 llama.cpp 和 GGUF 引入了多模态 向量模型,并在过程中发现了一些令人惊讶的问题。
Andrei Ungureanu
Alex C-G
Andrei Ungureanu, Alex C-G • 11 分钟的读取量
llama.cpp/jina_embeddings at master · jina-ai/llama.cpp
LLM inference in C/C++. Contribute to jina-ai/llama.cpp development by creating an account on GitHub.
GitHubjina-ai

我们在 llama.cpp 的分支中引入了多个修复,使其能够在多模态jina-embeddings-v4 上工作。

jina-embeddings-v4 引入了最先进的多模态向量模型,可以处理文本、图像和复杂的视觉文档以进行向量搜索。几周前,我们发布了 v4 的 GGUF 和动态量化,用于纯文本任务,从而减少了 VRAM 占用并提高了性能。但是,GGUF 上的多模态向量模型支持仍然缺失。为了完善这一功能,我们现在已经弄清楚了如何使用 llama.cpp 和 GGUF 生成多模态向量模型。请查看 此 README 文件 以获取完整的演练。

公平地说,llama.cpp 上游确实对多模态输入提供了一些支持,但由于 llama.cpp 社区的大部分精力都集中在 LLM 和文本生成上,因此完全缺少对多模态向量模型输出的支持。在本文中,我们将解释如何在 llama.cpp 中实现多模态向量模型,并检查其性能(以及两个量化版本)与 PyTorch 版本的 jina-embeddings-v4 相比如何。在本文中,我们将把 PyTorch 版本称为我们的“参考模型”。

tag了解 Llama.cpp 中的图像输入

让我们首先回顾一下如何使用我们的参考模型完成多模态向量模型。首先,将每个图像输入与一个特殊的提示词配对:

<|im_start|>user\n<|vision_start|><|image_pad|><|vision_end|>Describe the image.<|im_end|>\n

然后,模型预处理图像,对其进行编码(通过其 ViT),然后在单个前向传递中处理整个交错序列。

然而,对于 llama.cpp 来说,事情变得更加棘手。虽然它支持用于聊天完成的图像输入,但它不支持多模态输入——即,将文本和图像结合在一起的输入(如上所示)。这正是 我们 fork llama.cpp 的原因,我们更改了向量模型处理程序以接受 base64 编码的图像,从而让我们能够以类似于聊天完成处理程序的方式处理多模态内容。

因此,现在,为了在 llama.cpp 中使用多模态输入,我们可以从类似于参考模型使用的提示词开始:

<|im_start|>user\n<__image__>Describe the image.<|im_end|>\n

该过程的工作方式如下:

  1. llama.cpp 用视觉标记 <|vision_start|> 和 <|vision_end|> 包围 <__image__> 词元,从而得到如下内容:<|im_start|>user\n<|vision_start|><__image__><|vision_end|>Describe the image.<|im_end|>\n
  2. 在对提示词进行词元化时,词元器将特殊的 <__image__> 词元替换为 -1,从而在内部发出信号,表示序列包含应在之前(之后处理)进行编码的图像。
  3. <__image__> 标记之前的文本词元(即 <|im_start|>user\n<|vision_start|>)通过 LLM 解码并注入到 KVCache 中。
  4. 图像通过 ViT 组件进行编码,从而输出一系列通过 LLM 解码的图像词元。虽然这些词元与步骤一中的词元分开处理,但注意力层仍然可以通过 KVCache 关注这些文本词元。但是,此时,注意力层无法关注任何后续的文本词元(<|vision_end|>Describe the image.<|im_end|>\n)。
  5. LLM 解码任何剩余的文本词元(<|end_vision|>Describe the image.<|im_end|>\n)。现在,注意力层可以通过 KVCache 关注所有先前的词元(包括文本和图像)。

向量模型推理过程(图像编码和文本/图像词元解码)如下图所示:

图 1:jina-embeddings-v4 llama.cpp 版本的向量模型推理过程。

tag关注图像词元

由于注意力机制,此多步骤过程对于某些模型来说可能存在问题。让我们快速回顾一下模型中使用的不同类型的注意力:

  • 因果注意力 - 位置 k 上的一个词元的注意力机制仅关注先前的词元,即位置 [0:k-1] 上的词元。
  • 非因果注意力 - 位置 k 上的一个词元的注意力机制关注序列 [0:n] 中的所有词元。

下图显示了在第二步中处理 img_tok_n 时,注意力机制将关注的词元:

图 2:因果注意力与非因果注意力

在处理 img_tok_n 时,模型的状态如下:

  • 所有先前的文本词元(<|im_start|>、user、\n、<|vision_start|>)都已被处理并保存在 KVCache 中。
  • 所有图像词元(img_tok_1 到 img_tok_n)此时都作为同一序列的一部分进行处理。
  • 所有后续的文本词元(<|vision_end|>、Describe 等)将在稍后处理。

在因果注意力的情况下,在计算注意力分数时,仅考虑先前的词元,并通过 KVCache 检索过去的词元。

在非因果注意力的情况下,应考虑所有词元。但是,未来的文本词元(<|vision_end|>、Describe 等)尚未处理。它们将在将来的步骤中处理,因此事情会很快崩溃。

由于 jina-embeddings-v4 使用因果注意力,因此多步骤过程可以正常工作,但对于其他模型,情况可能并非如此。

在向量模型方面,每个词元的隐藏状态在处理时被准确捕获,并在最后合并为一个序列。目前,规范化和池化在 Python 中处理,但(经过一些额外的工作)也可以在 llama.cpp 端完成。

tag我们的修复

在为 llama.cpp 服务器中的向量模型端点启用图像输入后,我们开始使用基准测试来测试该实现,并且与我们的参考模型相比,看到了令人惊讶的巨大差异。我们怀疑 llama.cpp 对 ViT 的实现一定存在问题,Qwen2.5-VL 使用 ViT 将图像编码为图像补丁向量模型(图像正方形的密集向量表示),Qwen2.5 LLM 可以处理这些图像补丁向量模型。

以下是参考模型和 llama.cpp 实现之间的 ViT 输出差异的示例:

=== vit_out reference === Shape: [1008, 1280]
Logging patch 0, dimensions 0-9
Patch 0: -0.375000 -0.250000 -4.281250 -5.968750 2.953125 -8.125000 8.625000 -9.250000 8.937500 -0.332031 ... (dims 10-1279)
... (patches 1-1007 not shown)
=== vit_out llama.cpp === Shape: [1280, 1008, 1, 1]
Logging patch 0, dimensions 0-9
Patch 0: -2.998136 -2.226554 0.233671 -7.486460 0.596918 -12.889042 8.904849 -8.6
... (patches 1-1007 not shown)

如您所见,差异非常明显。为了确认这是唯一的问题,我们预先计算了 Python 中的图像词元,然后使用 llama.cpp 的 Qwen2.5 实现(仅使用 LLM)对其进行解码,希望生成的向量模型能够更接近地匹配参考模型的值——然而,情况并非如此。

tag修复 #1:注意力层的因果注意力掩码

我们通过查看注意力层继续调试——这是数字差异的最可能原因。我们注意到注意力层使用的注意力掩码对于图像词元的计算不正确。要了解这一点,我们可以回到我们的示例序列:

<|im_start|>user\n<|vision_start|><__image__><|vision_end|>Describe the image.<|im_end|>\n

在处理图像词元时,<__image__> 标记将被解压缩为类似 img_tok_1 img_tok_2 .... img_tok_last 的内容。因此,完整的序列将是:

<|im_start|>user\n<|vision_start|> img_tok_1 img_tok_2 ... img_tok_last <|vision_end|>Describe the image.<|im_end|>\n
⚠️
注意:在此管道中的这一点上,文本词元(<|im_start|>、user 等)和图像词元(img_tok_1img_tok_2 等),它们是稠密向量,而不是字面文本词元。我们使用这种形式的序列来简化解释。

当解码 img_tok_2 时,注意力机制应该关注所有之前的词元,即:

<|im_start|>user\n<|vision_start|> img_tok_1

然而,注意力掩码中的一个错误反而导致该机制关注整个图像序列,如下所示:

 <|im_start|>user\n<|vision_start|> img_tok_1 img_tok_2 ... img_tok_last

在我们修复了这个错误后,我们的 llama.cpp 模型的 向量模型(使用来自 Torch 模型的 ViT 预计算的图像词元)最终与参考模型的 向量模型相匹配(在较小的误差范围内)。

tag修复 #2:图像处理和Patch 向量模型

llama.cpp 的 ViT 编码器也产生了与参考模型不同的图像 向量模型,这些数值在预处理后立即出现差异。这在初始的patch创建步骤中尤其明显,我们的参考模型和 llama.cpp 都将原始图像(像素值)分割成通过卷积层编码的patch。原始patch(在 ViT 处理之前)之间的差异如下所示:

=== raw_patches reference === Shape: [1008, 1176]
Logging patches 0-4, dimensions 0-9
Patch 0: 0.484375 0.484375 0.500000 0.500000 0.470703 0.470703 0.470703 0.484375 0.470703 0.484375 ... (dims 10-1175)
... (patches 1-1007 not shown)
=== raw_patches llama.cpp === Shape: [1176, 1008, 1, 1]
Logging patches 0-4, dimensions 0-9
Patch 0: 0.455895 0.455895 0.455895 0.455895 0.455895 0.455895 0.470494 0.470494 0.470494 0.470494 ... (dims 10-1175)
... (patches 1-1007 not shown)

我们的参考模型和 llama.cpp 以不同的方式处理这些patch:

  • 参考模型使用 reshape 操作对像素值进行分组,然后使用单个 conv3d 层来编码预分组的像素patch。
  • llama.cpp 模型使用两个 conv2d 层创建和编码这些patch。

为了使 llama.cpp 模型的 向量模型更接近参考模型,我们认为使用参考模型的精确操作比调试 llama.cpp 的方法更简单。

我们的参考模型使用复杂的 reshape 和转置操作生成像素patch,这些操作需要 9 维张量。llama.cpp 中使用的底层张量处理库——ggml——无法支持它们,因此为了解决这个问题,我们使用一个单独的 Python 服务生成这些patch,该服务通过 HTTP 调用 llama.cpp 服务器。

ggml 也不支持 conv3d 层。在我们的参考模型中,conv3d 层的配置如下所示:

kernel_size = [
	2,  # temporal_patch_size, 
	14, # patch_size
	14  # patch_size
]

proj = nn.Conv3d(
	3, # in_channels
	1152, # embed_dim, 
	kernel_size=kernel_size, 
	stride=kernel_size, 
	bias=False
)

您可以看到 stride 和 kernel_size 是相同的,这意味着我们可以简单地展平 conv3d 层的输入和权重,并执行一个简单的矩阵乘法运算。为此,我们修改了 llama.cpp 中的转换脚本 (convert_hf_to_gguf.py),以导出patch投影层的扁平化版本的 conv3d 权重:

if 'patch_embed.proj.weight' in name:
	c1, c2, kt, kh, kw = data_torch.shape
	# Note: this part of the script also exports other versions of this layer
	# Only showing the relevant parts
	# Flat matmul weight: row-major [out, in*kT*kH*kW] = [embed_dim, 1176]
	W_flat = data_torch.contiguous().view(c1, -1)
	outputs.append(("v.patch_embd.weight_flat", W_flat))

为了在 llama.cpp 中应用 matmul 运算而不是两个 conv2d 层,我们修改了构建 Qwen2.5-VL ViT 图的代码:

ggml_tensor * build_inp_raw_precomputed() {
  ggml_tensor * inp_raw = ggml_new_tensor_2d(
		ctx0, 
		GGML_TYPE_F32, 
		img.p_dim, 
		img.npx * img.npy
	);
  ggml_set_name(inp_raw, "inp_raw");
  ggml_set_input(inp_raw);
  return inp_raw;
}

ggml_cgraph * build_qwen2vl() {
	// NOTE: only showing the code we've added for using pre-arranged image patches
  const bool uses_precomputed_image = img.is_precomputed;
  ggml_tensor * inp = nullptr;
	if (uses_precomputed_image) {
      ggml_tensor * inp_raw = build_inp_raw_precomputed();
      cb(inp_raw, "inp_raw", -1);
      inp = ggml_mul_mat(ctx0, model.patch_embeddings_flat, inp_raw);
	} else {
		// Usual 2x conv2d path 
	}
	// rest of the code 
}

通过这些最终更改,与参考模型相比,最终图像 向量模型的误差范围在 2% 以内(如下一节的评估表中所示)。

tag评估

在进行这些更改后,我们使用 MTEB 基准在 ViDoRe 任务上针对我们的参考模型评估了 llama.cpp 模型。您可以在我们的 llama.cpp 分支中找到 脚本和说明来复现这些结果,以及两个量化版本。

任务 参考模型 llama.cpp (F16) llama.cpp (Q4_K_M) llama.cpp (IQ4_XS)
VidoreArxivQARetrieval 83.55 85.00 84.38 84.34
VidoreDocVQARetrieval 50.53 52.02 51.93 51.57
VidoreInfoVQARetrieval 87.77 87.31 87.61 87.28
VidoreShiftProjectRetrieval 84.07 82.25 82.56 81.73
VidoreSyntheticDocQAAIRetrieval 97.52 96.71 97.28 97.15
VidoreSyntheticDocQAEnergyRetrieval 91.22 90.34 90.47 90.30
VidoreSyntheticDocQAGovernmentReportsRetrieval 91.61 93.84 93.47 94.47
VidoreSyntheticDocQAHealthcareIndustryRetrieval 95.42 96.08 95.67 96.05
VidoreTabfquadRetrieval 94.52 94.94 94.83 94.72
VidoreTatdqaRetrieval 65.52 64.85 64.63 64.76
平均 84.17 84.33 84.28 84.23

查看结果表,平均而言,llama.cpp 模型及其量化变体与参考模型没有太大差异。

为了更深入地比较这些模型,我们使用了来自不同领域和具有不同分辨率的图像,绘制了图像patch 向量模型之间的距离(在池化/归一化之前)。patch越红,参考模型和 llama.cpp 模型针对该特定patch的向量之间的余弦距离越大。

图 3:来自 jina-embeddings-v4 技术报告的页面,分辨率为 372 × 526(左),分辨率为 2481 × 3508(右)

图 4:来自 Jina AI 网站的屏幕截图,分辨率为 594 × 428(左),分辨率为 1982 × 1428(右)

图 5:东京涩谷,由 S K 拍摄于 Unsplash,分辨率为 383 × 255(左),分辨率为 5472 × 3649(右)

我们的目标是发现任何超出数值精度差异的模式——可能揭示我们的模型之间更多错误或差异的模式。但是,没有发现任何特定的模式,只是发散的patch数量随着图像分辨率的增加而增加。这些差异很可能由于后端差异而出现,而不是由于 jina-embeddings-v4(主干模型)的 Qwen2.5-VL 实现中的任何特定错误。

尽管如此,我们需要重申这些差异是最小的,并且基准测试结果也反映了这一事实。总的来说,llama.cpp 模型表现与参考模型一样好,尽管产生略有不同的 向量模型。

tag剩余问题

llama.cpp 中多模态 向量模型 还有几个潜在的改进领域:

  • 量化视觉编码器。目前,llama.cpp 仅支持 大模型 的量化,但为了实现更好的扩展,我们也希望量化视觉编码器。
💡
注意:llama.cpp 曾经支持使用 llama-llava-quantize-cli 量化 CLIP 模型,但自从引入 mtmd 库以来,相关资源已被删除。
  • 将视觉编码器分离为专用服务。视觉编码器通常使用非因果掩码,这意味着任何给定的图像都需要在单个前向调用中进行编码。因此,我们无法利用连续批处理。但是,我们可以考虑将视觉编码器分离为单独的服务,该服务可以将多个图像(甚至来自不同来源的图像)批量处理,并在单个前向传递中对它们进行编码。这将意味着更高的 vRAM 要求,但也会比逐个编码每个图像快得多。这种分离也意味着我们可以独立于语言模型自动缩放视觉编码器。
  • 启用多向量 向量模型。在本文中,我们仅使用了单向量 向量模型。但是为了充分利用 jina-embeddings-v4,我们还希望启用多向量 向量模型,以在复杂图像上实现更高的准确性。这将是一个简单的补充,因为这些 向量模型 是通过基础模型之上的单个线性层生成的。

tag结论

尽管最初存在错误和挫折,但将多模态 向量模型 集成到 llama.cpp 中现在产生的结果与我们的参考 PyTorch 模型非常匹配,包括在一系列基准测试任务中。对注意力掩码和图像块处理的修复消除了主要的差异来源,即使是量化的变体也能在使用更少资源的同时实现类似的准确性。在较高图像分辨率下剩余的差异似乎很小,并且可能是由于后端差异而不是核心模型实现造成的。

展望未来,将量化扩展到视觉编码器,通过单独的服务启用批量处理,以及支持多向量 向量模型 将进一步提高效率和准确性。这些改进将使 llama.cpp 中的多模态 向量模型 更具可扩展性,更适合实际用例。

类别:
技术博客
rss_feed

更多新闻
三月 11, 2026 • 7 分钟的读取量
从多模态大模型中引导音频向量模型
Han Xiao
Abstract illustration of a sound wave or heartbeat, formed by blue, orange, and gray dots on a white background.
三月 06, 2026 • 6 分钟的读取量
通过原始数值识别向量模型
Han Xiao
Fingerprint illustration made from numbers, showcasing digital and high-tech design on a light background.
八月 29, 2025 • 9 分钟的读取量
使用 Jina Remote MCP Server 的 Agentic Workflow
Alex C-G
Digital map of Europe formed with binary code in shades of blue, grey, and white, with red, yellow, and blue highlights in so
办公室
location_on
加利福尼亚州桑尼维尔
710 Lakeway Dr, Ste 200, 桑尼维尔, 加州 94085, 美国
location_on
德国柏林
Prinzessinnenstraße 19-20,10969 柏林,德国
搜索底座
读取器
向量模型
重排器
弹性推理服务
获取 Jina API 密钥
速率限制
API 状态
公司
关于我们
联系销售
新闻
实习生项目
下载 Jina 标志
open_in_new
下载 Elastic 徽标
open_in_new
条款
安全
条款及条件
隐私
管理 Cookie
email
Elastic Jina AI © 2020-2026.