Noticias
Modelos
API
keyboard_arrow_down
Lector
Lea las URL y busque en la web para obtener una base más sólida para su LLM.
Incrustaciones
Integraciones multilingües y multimodales de clase mundial.
reclasificador
Recuperador neuronal de clase mundial para maximizar la relevancia de la búsqueda.
MCP terminalCLIarticlellms.txtsmart_toyAgentesdata_objectEsquemamenu_bookDocumentos



Acceso
login
Comprender la entrada de imágenes en Llama.cpp
Atención en los 词元 de imagen
Nuestras correcciones
Evaluación
Problemas pendientes
Conclusión
Blog de tecnología
septiembre 09, 2025

Vectores multimodales en Llama.cpp y GGUF

Hemos incorporado modelos de vectores multimodales a llama.cpp y GGUF, y hemos descubierto algunos problemas sorprendentes en el camino.
Andrei Ungureanu
Alex C-G
Andrei Ungureanu, Alex C-G • 11 minutos de lectura
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

Hemos introducido varias correcciones en nuestra bifurcación de llama.cpp, para que funcione con jina-embeddings-v4 en los modelos de 向量模型 multimodales.

jina-embeddings-v4 introduce modelos de 向量模型 multimodales de última generación que pueden procesar texto, imágenes y documentos visuales complejos para la búsqueda vectorial. Hace unas semanas, lanzamos los GGUF de v4 y las cuantificaciones dinámicas para tareas solo de texto, que ofrecen una menor huella de VRAM y un mejor rendimiento. Sin embargo, aún faltaba el soporte de modelos de 向量模型 multimodales en GGUF. Para completar el panorama, ahora hemos descubierto cómo generar modelos de 向量模型 multimodales con llama.cpp y GGUF. Consulta este archivo README para obtener la guía completa.

Para ser justos, llama.cpp upstream sí tiene cierto soporte para la entrada multimodal, pero dado que la mayoría de la comunidad de llama.cpp se centra en los 大模型 y la generación de texto, el soporte para la salida de modelos de 向量模型 multimodales está completamente ausente. En este artículo, explicaremos cómo implementamos los modelos de 向量模型 multimodales en llama.cpp y examinaremos cómo se desempeña (junto con dos versiones cuantificadas) en comparación con la versión PyTorch de jina-embeddings-v4. A lo largo de este artículo, nos referiremos a la versión de PyTorch como nuestro "modelo de referencia".

tagComprender la entrada de imágenes en Llama.cpp

Primero, recapitulemos cómo se realiza el procesamiento de modelos de 向量模型 multimodales con nuestro modelo de referencia. Primero, emparejas cada entrada de imagen con un 提示词 especial:

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

Luego, el modelo preprocesa la imagen, la codifica (a través de su ViT) y luego procesa toda la secuencia intercalada en una sola pasada hacia adelante.

Sin embargo, cuando se trata de llama.cpp, las cosas son más complicadas. Si bien admite entradas de imagen para la finalización de chats, no admite entradas multimodales, es decir, entradas (como las anteriores) que combinan tanto texto como una imagen. Esta es exactamente la razón por la que bifurcamos llama.cpp, cambiando el controlador de modelos de 向量模型 para aceptar imágenes codificadas en base64, lo que nos permite procesar contenido multimodal de manera similar al controlador de finalización de chats.

Así que ahora, para trabajar con entradas multimodales en llama.cpp, podemos comenzar con un 提示词 similar al utilizado por el modelo de referencia:

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

El proceso funciona de la siguiente manera:

  1. llama.cpp rodea el 词元 <__image__> con marcadores de visión <|vision_start|> y <|vision_end|>, dándonos algo como esto: <|im_start|>user\n<|vision_start|><__image__><|vision_end|>Describe the image.<|im_end|>\n
  2. El tokenizador reemplaza el 词元 especial <__image__> con -1 al tokenizar el 提示词, señalando internamente que la secuencia contiene una imagen que debe codificarse antes (de ser procesada más tarde).
  3. Los 词元 de texto antes del marcador <__image__> (es decir, <|im_start|>user\n<|vision_start|>) se decodifican a través del 大模型 y se inyectan en la KVCache.
  4. La imagen se codifica a través del componente ViT, generando una serie de 词元 de imagen que se decodifican a través del 大模型. Aunque estos 词元 se procesan por separado de los 词元 en el paso uno, las capas de atención aún pueden atender a esos 词元 de texto a través de la KVCache. Sin embargo, en este punto, las capas de atención no pueden atender a ningún 词元 de texto posterior (<|vision_end|>Describe the image.<|im_end|>\n).
  5. El 大模型 decodifica los 词元 de texto restantes (<|end_vision|>Describe the image.<|im_end|>\n). Ahora las capas de atención pueden atender a todos los 词元 anteriores (tanto de texto como de imagen) a través de la KVCache.

El proceso de inferencia de modelos de 向量模型 (codificación de imágenes y decodificación de 词元 de texto/imagen) se muestra en la siguiente figura:

Figura 1: Proceso de inferencia de modelos de 向量模型 de la versión llama.cpp de jina-embeddings-v4.

tagAtención en los 词元 de imagen

Debido al mecanismo de atención, este proceso de varios pasos puede ser problemático para algunos modelos. Recapitulemos rápidamente los diferentes tipos de atención utilizados en los modelos:

  • Atención causal: el mecanismo de atención para un 词元 en la posición k atiende solo a los 词元 anteriores, en las posiciones [0:k-1].
  • Atención no causal: el mecanismo de atención para un 词元 en la posición k atiende a todos los 词元 de la secuencia [0:n].

La siguiente figura muestra los 词元 a los que atendería el mecanismo de atención al procesar img_tok_n en el segundo paso:

Figura 2: Atención causal vs. no causal

Al procesar img_tok_n, el estado del modelo es el siguiente:

  • Todos los 词元 de texto anteriores (<|im_start|>, user, \n, <|vision_start|>) ya se han procesado y guardado en la KVCache.
  • Todos los 词元 de imagen (img_tok_1 a img_tok_n) se procesan en este punto, como parte de la misma secuencia.
  • Todos los 词元 de texto siguientes (<|vision_end|>, Describe, etc.) se procesarán más adelante.

En el caso de la atención causal, solo se consideran los 词元 anteriores al calcular las puntuaciones de atención, y los 词元 pasados se recuperan a través de la KVCache.

En el caso de la atención no causal, se deben considerar todos los 词元. Sin embargo, los futuros 词元 de texto (<|vision_end|>, Describe, etc.) aún no se han procesado. Se procesarán en un paso futuro, por lo que las cosas se rompen rápidamente.

Dado que jina-embeddings-v4 utiliza la atención causal, el proceso de varios pasos funciona sin problemas, pero para otros modelos, este podría no ser el caso.

En términos de modelos de 向量模型, los estados ocultos de cada 词元 se capturan exactamente cuando se procesan y se combinan en una sola secuencia al final. Actualmente, la normalización y la agrupación se manejan en Python, pero (con un poco de trabajo adicional) esto también podría hacerse en el lado de llama.cpp.

tagNuestras correcciones

Después de habilitar las entradas de imagen para el punto final de modelos de 向量模型 en el servidor llama.cpp, comenzamos a probar la implementación con puntos de referencia y vimos diferencias sorprendentemente grandes en comparación con nuestro modelo de referencia. Sospechamos que debía haber algo mal con la implementación de llama.cpp del ViT utilizado por Qwen2.5-VL para codificar imágenes en modelos de 向量模型 de parches de imagen (representación vectorial densa de cuadrados de imagen) que el 大模型 Qwen2.5 puede procesar.

Aquí hay un ejemplo de cómo difieren las salidas de ViT entre el modelo de referencia y la implementación de 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)

Como puedes ver, las diferencias son bastante notables. Para confirmar que este era el único problema, precalculamos los 词元 de imagen en Python, luego los decodificamos usando la implementación de Qwen2.5 de llama.cpp (usando solo el 大模型), con la esperanza de que los modelos de 向量模型 resultantes coincidieran mucho más con los valores del modelo de referencia; sin embargo, este no fue el caso.

tagCorrección n.º 1: Máscara de atención causal para capas de atención

Continuamos depurando observando las capas de atención, la causa más probable de las diferencias numéricas. Notamos que la máscara de atención utilizada por las capas de atención no se calculaba correctamente para los 词元 de imagen. Para ver esto, podemos volver a nuestra secuencia de ejemplo:

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

Al procesar 词元 de imagen, el marcador <__image__> se desempaqueta en algo como img_tok_1 img_tok_2 .... img_tok_last. Entonces, la secuencia completa sería:

<|im_start|>user\n<|vision_start|> img_tok_1 img_tok_2 ... img_tok_last <|vision_end|>Describe the image.<|im_end|>\n
⚠️
Nota: en este punto de la canalización, tanto los 词元 de texto (<|im_start|>, user, etc.) como los 词元 de imagen (img_tok_1,img_tok_2, etc.), son vectores densos y no *tokens* de texto literales. Usamos esta forma de la secuencia para simplificar la explicación.

Al decodificar img_tok_2, el mecanismo de atención *debería* atender a todos los *tokens* anteriores, es decir:

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

Sin embargo, un error en la máscara de atención estaba causando *en cambio* que el mecanismo atendiera a toda la secuencia de la imagen, así:

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

Después de solucionar este error, los *embeddings* de nuestro modelo llama.cpp (utilizando los *tokens* de imagen precalculados del ViT del modelo Torch) finalmente coincidieron con los *embeddings* del modelo de referencia (dentro de un pequeño margen de error).

tagCorrección #2: Procesamiento de Imágenes y *Embeddings* de Parches

El codificador ViT de llama.cpp también producía *embeddings* de imagen diferentes del modelo de referencia, con números que divergían inmediatamente después del preprocesamiento. Esto fue particularmente evidente durante el paso inicial de creación de parches, donde tanto nuestro modelo de referencia como llama.cpp dividen la imagen en bruto (valores de píxeles) en parches que se codifican a través de capas convolucionales. Las diferencias entre los parches en bruto (antes del procesamiento ViT) se pueden ver a continuación:

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

Nuestro modelo de referencia y llama.cpp procesan estos parches de diferentes maneras:

  • El modelo de referencia agrupa los valores de los píxeles utilizando operaciones de remodelación y luego utiliza una única capa conv3d para codificar los parches de píxeles pre-agrupados.
  • El modelo llama.cpp crea y codifica estos parches con dos capas conv2d

Para acercar los *embeddings* del modelo llama.cpp a los del modelo de referencia, pensamos que sería más sencillo utilizar las operaciones exactas del modelo de referencia en lugar de depurar el enfoque de llama.cpp.

Nuestro modelo de referencia genera parches de píxeles utilizando operaciones complejas de remodelación y transposición que requieren tensores de 9 dimensiones. La biblioteca de procesamiento de tensores de bajo nivel utilizada en llama.cpp — ggml — no puede soportarlos, así que para evitar esto, generamos los parches utilizando un servicio Python separado que llama al servidor llama.cpp a través de HTTP.

ggml tampoco es compatible con las capas conv3d. En nuestro modelo de referencia, la configuración de la capa conv3d se ve así:

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
)

Puede ver que stride y kernel_size son iguales, lo que significa que podemos simplemente aplanar las entradas y los pesos de la capa conv3d y realizar una simple operación de multiplicación de matrices en su lugar. Para hacer esto, modificamos el script de conversión en llama.cpp (convert_hf_to_gguf.py) para exportar una versión aplanada de los pesos conv3d para la capa de proyección de parches:

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

Para aplicar la operación matmul en lugar de las dos capas conv2d en llama.cpp, modificamos el código que construye el grafo del 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 
}

Con estos cambios finales, los *embeddings* finales de la imagen estaban dentro de un margen de error del 2% en comparación con el modelo de referencia (como se puede ver en la tabla de evaluación en la siguiente sección).

tagEvaluación

Después de realizar estos cambios, evaluamos el modelo llama.cpp con nuestro modelo de referencia en tareas ViDoRe utilizando el benchmark MTEB. Puede ver el script e instrucciones para replicar estos resultados en nuestra bifurcación de llama.cpp, así como dos versiones cuantificadas.

Tarea Modelo de referencia 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
Promedio 84.17 84.33 84.28 84.23

Observando la tabla de resultados, en promedio, el modelo llama.cpp y sus variantes cuantificadas no divergen mucho del modelo de referencia.

Para comparar los modelos con más profundidad, utilizamos imágenes de diferentes dominios y con diferentes resoluciones, trazando la distancia entre los *embeddings* de los parches de imagen (antes de la agrupación/normalización). Cuanto más rojo es el parche, mayor es la distancia coseno entre los vectores del modelo de referencia y el modelo llama.cpp para ese parche en particular.

Figura 3: Página del informe técnico jina-embeddings-v4, resolución de 372 × 526 (izquierda), resolución de 2481 × 3508 (derecha)

Figura 4: Captura de pantalla del sitio web de Jina AI, resolución de 594 × 428 (izquierda), resolución de 1982 × 1428 (derecha)

Figura 5: Tokio, Shibuya por S K en Unsplash, resolución de 383 × 255 (izquierda), resolución de 5472 × 3649 (derecha)

Nuestro objetivo era detectar cualquier patrón que fuera más allá de las diferencias de precisión numérica, patrones que pudieran revelar más errores o diferencias entre nuestros modelos. Sin embargo, no se observaron patrones particulares, excepto que el número de parches divergentes aumenta con la resolución de la imagen. Estas diferencias probablemente aparecen debido a las diferencias de backend, y no debido a ningún error particular en la implementación de Qwen2.5-VL (el modelo backbone de jina-embeddings-v4).

No obstante, debemos reiterar que estas diferencias son mínimas, y los resultados de las pruebas de referencia también reflejan este hecho. En general, los modelos llama.cpp rinden tan bien como el modelo de referencia, aunque produciendo vectores de 向量模型 ligeramente diferentes.

tagProblemas pendientes

Existen varias áreas de mejora potencial para los 向量模型 multimodales en llama.cpp:

  • Cuantificar el codificador de visión. Actualmente, llama.cpp solo admite la cuantificación para 大模型, pero para lograr una mejor escalabilidad, también nos gustaría cuantificar el codificador de visión.
💡
Nota: llama.cpp solía admitir la cuantificación de modelos CLIP utilizando llama-llava-quantize-cli, pero los recursos relevantes se han eliminado desde la introducción de la biblioteca mtmd.
  • Separar el codificador de visión en un servicio dedicado. Los codificadores de visión normalmente utilizan un enmascaramiento no causal, lo que significa que cualquier imagen dada debe codificarse dentro de una única llamada directa. Por lo tanto, no podemos hacer uso del procesamiento por lotes continuo. Sin embargo, podríamos considerar separar el codificador de visión en un servicio separado, que agruparía varias imágenes (incluso de fuentes separadas) y las codificaría todas en una sola pasada directa. Esto significaría mayores requisitos de vRAM, pero también sería mucho más rápido que codificar cada imagen una por una. Esta separación también significaría que podríamos escalar automáticamente el codificador de visión independientemente del modelo de lenguaje.
  • Habilitar 向量模型 multi-vectoriales. En este artículo, solo hemos trabajado con 向量模型 de un solo vector. Pero para aprovechar al máximo jina-embeddings-v4, también nos gustaría habilitar 向量模型 multi-vectoriales para lograr una mayor precisión en imágenes complejas. Esta sería una adición fácil, ya que estos 向量模型 se generan con una sola capa lineal en la parte superior del modelo base.

tagConclusión

A pesar de los errores y contratiempos iniciales, la integración de 向量模型 multimodales en llama.cpp ahora arroja resultados que coinciden estrechamente con nuestro modelo de PyTorch de referencia, incluso en una variedad de tareas de evaluación comparativa. Las correcciones a la máscara de atención y al procesamiento de parches de imagen eliminaron las principales fuentes de divergencia, e incluso las variantes cuantificadas logran una precisión similar al tiempo que utilizan muchos menos recursos. Las diferencias restantes en resoluciones de imagen más altas parecen menores y probablemente se deban a variaciones de backend en lugar de a la implementación del modelo central.

De cara al futuro, extender la cuantificación al codificador de visión, habilitar el procesamiento por lotes a través de un servicio separado y admitir 向量模型 multi-vectoriales mejoraría aún más tanto la eficiencia como la precisión. Estas adiciones harían que los 向量模型 multimodales en llama.cpp fueran más escalables y más adecuados para casos de uso del mundo real.

Categorías:
Blog de tecnología
rss_feed

Leer más
marzo 11, 2026 • 7 minutos de lectura
Generación de embeddings de audio a partir de LLM multimodales
Han Xiao
Abstract illustration of a sound wave or heartbeat, formed by blue, orange, and gray dots on a white background.
marzo 06, 2026 • 6 minutos de lectura
Identificación de modelos de embeddings a partir de valores numéricos brutos
Han Xiao
Fingerprint illustration made from numbers, showcasing digital and high-tech design on a light background.
agosto 29, 2025 • 9 minutos de lectura
Flujo de trabajo agentic con el servidor MCP remoto de Jina
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
Oficinas
location_on
Sunnyvale, California
710 Lakeway Dr, Ste 200, Sunnyvale, CA 94085, EE. UU.
location_on
Berlín, Alemania
Prinzessinnenstraße 19-20, 10969 Berlín, Alemania
Fundación de búsqueda
Lector
Incrustaciones
reclasificador
Obtener la clave API de Jina
Límite de velocidad
Estado de la API
Compañía
Sobre nosotros
Contactar con ventas
Sala de prensa
Programa de prácticas
Descargar el logotipo de Jina
open_in_new
Descargar el logotipo de Elastic
open_in_new
Términos
Seguridad
Términos y condiciones
Privacidad
Administrar cookies
email
Jina AI de Elastic © 2020-2026.