Новости
Модели
API
keyboard_arrow_down
Читатель
Читайте URL-адреса и ищите информацию в Интернете для получения более подходящей подготовки для получения степени магистра права.
Вложения
Мультимодальные многоязычные вложения мирового класса.
Реранкер
Нейронный ретривер мирового класса для максимального повышения релевантности поиска.
MCP terminalCLIarticlellms.txtsmart_toyАгентыdata_objectСхемаmenu_bookДокументы



Авторизоваться
login
Понимание ввода изображений в Llama.cpp
Внимание на Tokens изображений
Наши исправления
Оценка
Остающиеся проблемы
Заключение
Технический блог
сентябрь 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 представляет собой современный мультимодальный 向量模型, который может обрабатывать текст, изображения и сложные визуальные документы для векторного поиска. Несколько недель назад мы выпустили GGUF и динамическую квантизацию v4 для задач, связанных только с текстом, что обеспечивает меньший объем занимаемой VRAM и повышенную производительность. Однако поддержка мультимодальных 向量模型 на GGUF по-прежнему отсутствовала. Чтобы завершить картину, мы выяснили, как генерировать мультимодальные 向量模型 с помощью llama.cpp и GGUF. Ознакомьтесь с этим файлом README для получения полной информации.

Справедливости ради стоит отметить, что в llama.cpp upstream есть некоторая поддержка мультимодального ввода, но поскольку большая часть сообщества llama.cpp сосредоточена на 大模型 и генерации текста, поддержка вывода мультимодальных 向量模型 полностью отсутствует. В этой статье мы объясним, как мы реализовали мультимодальные 向量模型 в 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, все становится сложнее. Хотя он поддерживает ввод изображений для завершения чата, он не поддерживает мультимодальный ввод, а именно ввод (например, приведенный выше), который объединяет как текст, так и изображение. Это именно почему мы форкнули llama.cpp, изменив обработчик 向量模型, чтобы он принимал изображения, закодированные в base64, что позволяет нам обрабатывать мультимодальный контент аналогично обработчику завершения чата.

Итак, теперь, чтобы работать с мультимодальным вводом в llama.cpp, мы можем начать с 提示词, аналогичного тому, который используется эталонной моделью:

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

Процесс работает следующим образом:

  1. llama.cpp окружает <__image__> токен маркерами vision <|vision_start|> и <|vision_end|>, что дает нам что-то вроде этого: <|im_start|>user\n<|vision_start|><__image__><|vision_end|>Describe the image.<|im_end|>\n
  2. Токенизатор заменяет специальный токен <__image__> на -1 при токенизации 提示词, внутренне сигнализируя о том, что последовательность содержит изображение, которое следует закодировать до (последующей обработки).
  3. Текстовые Tokens перед маркером <__image__> (а именно, <|im_start|>user\n<|vision_start|>) декодируются через 大模型 и внедряются в KVCache.
  4. Изображение кодируется через компонент ViT, выводя серию токенов изображений, которые декодируются через 大模型. Хотя эти Tokens обрабатываются отдельно от токенов на первом шаге, слои внимания по-прежнему могут обращаться к этим текстовым токенам через KVCache. Однако на этом этапе слои внимания не могут обращаться к каким-либо более поздним текстовым токенам (<|vision_end|>Describe the image.<|im_end|>\n).
  5. 大模型 декодирует любые оставшиеся текстовые Tokens (<|end_vision|>Describe the image.<|im_end|>\n). Теперь слои внимания могут обращаться ко всем более ранним токенам (как текстовым, так и изображению) через KVCache.

Процесс логического вывода 向量模型 (кодирование изображения и декодирование текстовых/графических токенов) показан на рисунке ниже:

Рисунок 1: Процесс логического вывода 向量模型 версии jina-embeddings-v4 llama.cpp.

tagВнимание на Tokens изображений

Из-за механизма внимания этот многоэтапный процесс может быть проблематичным для некоторых моделей. Давайте быстро повторим различные типы внимания, используемые в моделях:

  • Причинное внимание — механизм внимания для одного токена в позиции k обращает внимание только на предыдущие Tokens в позициях [0:k-1].
  • Непричинное внимание — механизм внимания для одного токена в позиции k обращает внимание на все Tokens в последовательности [0:n].

На рисунке ниже показаны Tokens, на которые механизм внимания обращал бы внимание при обработке img_tok_n на втором шаге:

Рисунок 2: Причинное и непричинное внимание

При обработке img_tok_n состояние модели следующее:

  • Все предыдущие текстовые Tokens (<|im_start|>, user, \n, <|vision_start|> ) уже обработаны и сохранены в KVCache.
  • Все Tokens изображений (img_tok_1 to img_tok_n) обрабатываются на этом этапе как часть одной и той же последовательности.
  • Все следующие текстовые Tokens (<|vision_end|> , Describe , и т. д.) будут обработаны позже.

В случае причинного внимания при вычислении оценок внимания учитываются только предыдущие Tokens, а прошлые Tokens извлекаются через KVCache.

В случае непричинного внимания следует учитывать все Tokens. Однако будущие текстовые Tokens (<|vision_end|> , Describe и т. д.) еще не были обработаны. Они будут обработаны на следующем шаге, поэтому все быстро ломается.

Поскольку jina-embeddings-v4 использует причинное внимание, многоэтапный процесс работает без проблем, но для других моделей это может быть не так.

С точки зрения 向量模型, скрытые состояния каждого токена захватываются именно тогда, когда они обрабатываются и объединяются в единую последовательность в конце. В настоящее время нормализация и объединение обрабатываются в Python, но (при некоторой дополнительной работе) это также можно сделать на стороне llama.cpp.

tagНаши исправления

После включения ввода изображений для конечной точки 向量模型 на сервере llama.cpp мы начали тестировать реализацию с помощью тестов и увидели на удивление большие различия по сравнению с нашей эталонной моделью. Мы заподозрили, что с реализацией ViT в llama.cpp, используемой Qwen2.5-VL для кодирования изображений в 向量模型 патчей изображений (плотное векторное представление квадратов изображений), которые может обрабатывать 大模型 Qwen2.5, что-то не так.

Вот пример того, как результаты ViT различаются между эталонной моделью и реализацией llama.cpp:

=== 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)

Как видите, различия весьма заметны. Чтобы убедиться, что это единственная проблема, мы предварительно вычислили Tokens изображения в Python, а затем декодировали их с помощью реализации Qwen2.5 в llama.cpp (используя только 大模型), надеясь, что результирующие 向量模型 будут гораздо ближе соответствовать значениям эталонной модели, однако это было не так.

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
⚠️
Примечание: на этом этапе конвейера как текстовые Tokens (<|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 (используя предварительно вычисленные токены изображений из ViT модели Torch) наконец-то совпали с векторными моделями эталонной модели (с небольшой погрешностью).

tagИсправление #2: Обработка изображений и патч-векторные модели

ViT-кодировщик llama.cpp также создавал векторные модели изображений, отличные от эталонной модели, причем числа расходились сразу после предварительной обработки. Это было особенно заметно на начальном этапе создания патчей, когда и наша эталонная модель, и 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 обрабатывают эти патчи по-разному:

  • Эталонная модель группирует значения пикселей с помощью операций изменения формы, а затем использует один слой conv3d для кодирования предварительно сгруппированных пиксельных патчей.
  • Модель llama.cpp создает и кодирует эти патчи с помощью двух слоев conv2d

Чтобы приблизить векторные модели llama.cpp к векторным моделям эталонной модели, мы решили, что будет проще использовать точные операции эталонной модели, чем отлаживать подход llama.cpp.

Наша эталонная модель генерирует пиксельные патчи с использованием сложных операций изменения формы и транспонирования, которые требуют 9-мерных тензоров. Низкоуровневая библиотека обработки тензоров, используемая в llama.cpp, — ggml — не может их поддерживать, поэтому, чтобы обойти это, мы сгенерировали патчи с помощью отдельной службы Python, которая вызывает сервер llama.cpp по HTTP.

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))

Чтобы применить операцию matmul вместо двух слоев conv2d в llama.cpp, мы изменили код, который строит граф 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Оценка

После внесения этих изменений мы оценили модель llama.cpp по сравнению с нашей эталонной моделью на задачах ViDoRe с использованием бенчмарка MTEB. Вы можете увидеть скрипт и инструкции для воспроизведения этих результатов в нашем форке 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 и ее квантованные варианты не сильно отличаются от эталонной модели.

Чтобы сравнить модели более подробно, мы использовали изображения из разных областей и с разным разрешением, отображая расстояние между векторными моделями патчей изображений (до объединения/нормализации). Чем краснее патч, тем больше косинусное расстояние между векторами эталонной модели и модели llama.cpp для этого конкретного патча.

Рисунок 3: Страница из jina-embeddings-v4 технического отчета, разрешение 372 × 526 (слева), разрешение 2481 × 3508 (справа)

Рисунок 4: Скриншот с веб-сайта Jina AI, разрешение 594 × 428 (слева), разрешение 1982 × 1428 (справа)

Рисунок 5: Токио, Сибуя от S K на Unsplash, разрешение 383 × 255 (слева), разрешение 5472 × 3649 (справа)

Мы стремились выявить любые закономерности, выходящие за рамки различий в числовой точности — закономерности, которые могут выявить дальнейшие ошибки или различия между нашими моделями. Однако никаких особых закономерностей не было видно, за исключением того, что количество расходящихся патчей увеличивается с разрешением изображения. Эти различия, скорее всего, возникают из-за различий в бэкенде, а не из-за каких-либо конкретных ошибок в реализации Qwen2.5-VL (базовой модели jina-embeddings-v4).

Тем не менее, необходимо повторить, что эти различия минимальны, и результаты тестов также отражают этот факт. В целом, модели llama.cpp работают так же хорошо, как и эталонная модель, хотя и выдают немного отличающиеся векторы 向量模型 (Embeddings).

tagОстающиеся проблемы

Существует несколько потенциальных областей для улучшения мультимодальных 向量模型 (Embeddings) в llama.cpp:

  • Квантование vision encoder. В настоящее время llama.cpp поддерживает квантование только для 大模型 (LLM), но для достижения лучшего масштабирования мы также хотели бы квантовать vision encoder.
💡
Примечание: llama.cpp ранее поддерживал квантование CLIP моделей с помощью llama-llava-quantize-cli, но соответствующие ресурсы были удалены после внедрения библиотеки mtmd.
  • Выделение vision encoder в отдельный сервис. Vision encoder обычно используют non-causal masking, что означает, что любое данное изображение необходимо закодировать в рамках одного forward call. Поэтому мы не можем использовать continuous batching. Однако мы могли бы рассмотреть возможность выделения vision encoder в отдельный сервис, который объединял бы несколько изображений (даже из разных источников) и кодировал бы их все за один forward pass. Это означало бы более высокие требования к vRAM, но также было бы намного быстрее, чем кодирование каждого изображения по отдельности. Это разделение также означало бы, что мы могли бы автоматически масштабировать vision encoder независимо от языковой модели.
  • Включение multi-vector 向量模型 (Embeddings). В этой статье мы работали только с single-vector 向量模型 (Embeddings). Но для полного использования jina-embeddings-v4, мы также хотели бы включить multi-vector 向量模型 (Embeddings) для достижения более высокой точности на сложных изображениях. Это было бы легким дополнением, поскольку эти 向量模型 (Embeddings) генерируются с помощью одного линейного слоя поверх базовой модели.

tagЗаключение

Несмотря на первоначальные ошибки и неудачи, интеграция мультимодальных 向量模型 (Embeddings) в llama.cpp теперь дает результаты, близко совпадающие с нашей эталонной моделью PyTorch, в том числе в ряде тестов. Исправления в attention mask и обработке патчей изображений устранили основные источники расхождений, и даже квантованные варианты достигают аналогичной точности при использовании значительно меньшего количества ресурсов. Остающиеся различия при более высоких разрешениях изображений кажутся незначительными и, вероятно, связаны с различиями в бэкенде, а не с основной реализацией модели.

В перспективе, расширение квантования на vision encoder, включение пакетной обработки через отдельный сервис и поддержка multi-vector 向量模型 (Embeddings) еще больше повысят как эффективность, так и точность. Эти дополнения сделают мультимодальные 向量模型 (Embeddings) в llama.cpp более масштабируемыми и лучше подходящими для реальных сценариев использования.

Категории:
Технический блог
rss_feed

Читать далее
март 11, 2026 • 7 минуты чтения
Бутстрэппинг аудиоэмбеддингов на базе мультимодальных LLM
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
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 Берлин, Германия
Поиск Фонда
Читатель
Вложения
Реранкер
Получить API-ключ Jina
Ограничение скорости
Статус API
Компания
О нас
Связаться с отделом продаж
отдел новостей
Стажерская программа
Загрузить логотип Jina
open_in_new
Скачать логотип Elastic
open_in_new
Условия
Безопасность
Условия использования
Конфиденциальность
Управление файлами cookie
email
Jina AI от Elastic © 2020-2026.