ニュース
モデル
API
keyboard_arrow_down
読者
URL を読み取ったり検索したりすると、大規模なモデルのサポートが向上します。
ベクトルモデル
世界クラスのマルチモーダル、多言語埋め込み。
並べ替え者
検索の関連性を最大化する世界クラスのニューラルレトリーバー。
MCP terminalコマンドラインarticlellms.txtsmart_toyエージェントdata_objectモデルmenu_book書類



ログイン
login
Llama.cppでの画像入力を理解する
画像トークンへの注意
修正点
評価
残された課題
結論
技術ブログ
9月 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にマルチモーダル埋め込みを実装した方法を説明し、jina-embeddings-v4のPyTorchバージョンと比較して、そのパフォーマンス(および2つの量子化バージョン)を検証します。この記事全体を通して、PyTorchバージョンを「参照モデル」と呼びます。

tagLlama.cppでの画像入力を理解する

まず、参照モデルでマルチモーダル埋め込みがどのように行われるかを振り返りましょう。最初に、各画像入力を特別なプロンプトとペアにします。

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

次に、モデルは画像を前処理し、(ViTを介して)エンコードし、インターリーブされたシーケンス全体を1回の順方向パスで処理します。

ただし、llama.cppの場合、状況はより複雑です。チャット補完のための画像入力はサポートしていますが、マルチモーダル入力、つまりテキストと画像を組み合わせた(上記の例のような)入力はサポートしていません。これがまさにllama.cppをフォークした理由であり、埋め込みハンドラーがbase64エンコードされた画像を受け入れるように変更し、チャット補完ハンドラーと同様の方法でマルチモーダルコンテンツを処理できるようにしました。

そのため、llama.cppでマルチモーダル入力を処理するには、参照モデルで使用されるものと同様のプロンプトから開始できます。

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

プロセスは次のようになります。

  1. llama.cppは、<__image__> トークンをビジョンマーカー <|vision_start|> と <|vision_end|> で囲み、次のようなものにします。<|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を介してデコードされる一連の画像トークンが出力されます。これらのトークンはステップ1のトークンとは別に処理されますが、アテンションレイヤーは 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 の1つのトークンのアテンションメカニズムは、位置 [0:k-1] の前のトークンのみにアクセスします。
  • 非因果的注意 - 位置 k の1つのトークンのアテンションメカニズムは、シーケンス [0:n] 内のすべてのトークンにアクセスします。

次の図は、2番目のステップで 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サーバーの埋め込みエンドポイントで画像入力を有効にした後、ベンチマークで実装のテストを開始し、参照モデルと比較して驚くほど大きな違いが見られました。Qwen2.5 LLMが処理できる画像パッチ埋め込み(画像の正方形の密なベクトル表現)に画像をエンコードするためにQwen2.5-VLで使用されるViTのllama.cppの実装に問題があるのではないかと考えました。

参照モデルと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で画像トークンを事前に計算し、Qwen2.5のllama.cppの実装(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_1、img_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: 画像処理とパッチ**埋め込み**

llama.cpp の ViT エンコーダーも、参照モデルとは異なる画像**埋め込み**を生成していました。この数値は、前処理の直後から乖離していました。これは特に、初期のパッチ作成ステップで顕著でした。ここでは、参照モデルと llama.cpp の両方が、生の画像(ピクセル値)を畳み込み層を介してエンコードされるパッチに分割します。生のパッチ(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 は、これらのパッチを異なる方法で処理します。

  • 参照モデルは、reshape 操作を使用してピクセル値をグループ化し、次に単一の conv3d 層を使用して、事前グループ化されたピクセルパッチをエンコードします。
  • llama.cpp モデルは、2 つの conv2d 層を使用してこれらのパッチを作成およびエンコードします。

llama.cpp モデルの**埋め込み**を参照モデルのものに近づけるために、llama.cpp のアプローチをデバッグするよりも、参照モデルの正確な操作を使用する方が簡単だと考えました。

参照モデルは、9 次元のテンソルを必要とする複雑な reshape および転置操作を使用してピクセルパッチを生成します。llama.cpp で使用される低レベルのテンソル処理ライブラリ — ggml — は、これらをサポートできないため、これを回避するために、HTTP 経由で llama.cpp サーバーを呼び出す別の Python サービスを使用してパッチを生成しました。

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) を変更して、パッチ投影層の 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 で 2 つの conv2d 層の代わりに matmul 演算を適用するために、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評価

これらの変更を加えた後、ViDoRe タスクで MTEB ベンチマークを使用して、llama.cpp モデルをリファレンスモデルに対して評価しました。これらの結果を llama.cpp フォークと 2 つの量子化されたバージョンで再現するためのスクリプトと手順を参照できます。

タスク 参照モデル 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 モデルとその量子化されたバリアントは、参照モデルからあまり乖離していません。

モデルをより詳細に比較するために、さまざまなドメインおよび異なる解像度の画像を使用して、画像パッチ**埋め込み**間の距離をプロットしました(プーリング/正規化前)。パッチが赤いほど、その特定のパッチのリファレンスモデルと llama.cpp モデルのベクトル間のコサイン距離が大きくなります。

図 3: jina-embeddings-v4 技術レポートからのページ、372 × 526 解像度 (左)、2481 × 3508 解像度 (右)

図 4: Jina AI Web サイトからのスクリーンショット、594 × 428 解像度 (左)、1982 × 1428 解像度 (右)

図 5: 東京、渋谷 (撮影: S K) (提供: Unsplash)、383 × 255 解像度 (左)、5472 × 3649 解像度 (右)

数値的な精度の違いを超えるパターン、つまり、モデル間のさらなるバグや違いを明らかにする可能性のあるパターンを見つけることを目指しました。ただし、画像解像度が高くなるほど、乖離するパッチの数が増えることを除いて、特定のパターンは見られませんでした。これらの違いは、おそらくバックエンドの違いによるものであり、Qwen2.5-VL(jina-embeddings-v4 のバックボーンモデル)の実装における特定のバグによるものではありません。

とは言え、これらの差はごくわずかであり、ベンチマークの結果もその事実を反映していることを改めて強調する必要があります。全体として、llama.cpp モデルは、わずかに異なる埋め込みベクトルを生成するものの、参照モデルと同等の性能を発揮します。

tag残された課題

llama.cpp におけるマルチモーダルな 向量模型 に関しては、改善の余地がいくつかあります。

  • ビジョンエンコーダの量子化。現在、llama.cpp は 大規模言語モデル(LLM) の量子化のみをサポートしていますが、より優れたスケーリングを実現するためには、ビジョンエンコーダも量子化したいと考えています。
💡
注:llama.cpp は、以前は llama-llava-quantize-cli を使用して CLIP モデルの量子化をサポートしていましたが、mtmd ライブラリの導入以来、関連リソースは削除されています。
  • ビジョンエンコーダを専用のサービスに分離する。ビジョンエンコーダは通常、非因果マスキングを使用します。つまり、特定の画像は単一のフォワードコール内でエンコードする必要があります。したがって、継続的なバッチ処理を利用することはできません。しかし、ビジョンエンコーダを別のサービスに分離することを検討できます。これにより、複数の画像(別々のソースからの画像も含む)をまとめてバッチ処理し、すべてを単一のフォワードパスでエンコードできます。これは、より高い vRAM 要件を意味しますが、各画像を 1 つずつエンコードするよりもはるかに高速になります。この分離は、言語モデルとは独立してビジョンエンコーダを自動スケーリングできることも意味します。
  • マルチベクトル 向量模型 を有効にする。この記事では、シングルベクトルの 向量模型 のみを扱いました。しかし、jina-embeddings-v4 を最大限に活用するためには、マルチベクトル 向量模型 を有効にして、複雑な画像でより高い精度を達成したいと考えています。これらの 向量模型 は、ベースモデル上に単一の線形レイヤーで生成されるため、簡単に追加できます。

tag結論

最初のバグや後退にもかかわらず、マルチモーダルな 向量模型 を llama.cpp に統合することで、さまざまなベンチマークタスクを含め、参照 PyTorch モデルとほぼ一致する結果が得られるようになりました。アテンションマスクと画像パッチ処理の修正により、主な乖離の原因が解消され、量子化されたバリアントでさえ、はるかに少ないリソースを使用しながら同様の精度を達成しています。より高い画像解像度での残りの差はわずかであり、コアモデルの実装ではなく、バックエンドのバリエーションによるものである可能性があります。

今後に向けて、量子化をビジョンエンコーダに拡張し、別のサービスを介したバッチ処理を有効にし、マルチベクトル 向量模型 をサポートすることで、効率と精度の両方をさらに向上させることができます。これらの追加により、llama.cpp におけるマルチモーダルな 向量模型 は、よりスケーラブルになり、現実世界のユースケースにより適したものになるでしょう。

カテゴリー:
技術ブログ
rss_feed

もっとニュース
3月 11, 2026 • 7 読む時間
マルチモーダル大模型からのオーディオベクトルモデルのブートストラップ
Han Xiao
Abstract illustration of a sound wave or heartbeat, formed by blue, orange, and gray dots on a white background.
3月 06, 2026 • 6 読む時間
生の数値から埋め込みモデルを特定する
Han Xiao
Fingerprint illustration made from numbers, showcasing digital and high-tech design on a light background.
8月 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
エラスティックジナAI © 2020-2026.