Hace dos semanas, publicamos los formatos GGUF de jina-embeddings-v4, un modelo de vectorización universal para la recuperación multimodal multilingüe, con varias versiones cuantificadas. Nuestra motivación era simple: como modelo de 3.75B parámetros, la versión de transformador vainilla de jina-embeddings-v4 no escala bien en nuestras instancias de API de GCP G2 (GPU L4), por lo que queríamos acelerar la inferencia utilizando estas versiones GGUF más pequeñas y rápidas. Durante nuestros experimentos, descubrimos algunos hallazgos interesantes al convertir y ejecutar modelos de vectorización GGUF. Dado que la mayoría de la comunidad de llama.cpp se centra en los LLM, pensamos que sería valioso compartir esto desde la perspectiva de un proveedor de vectorizaciones.
Lo que es particularmente relevante es que los modelos de vectorización actuales son casi idénticos a los LLM; por ejemplo, jina-embeddings-v4 se basa en Qwen2.5-VL-3B-instruct y jina-reranker-m0 se basa en Qwen2-VL-2B. La única diferencia real es la salida: los LLM son generativos, las vectorizaciones y los重排器 son discriminativos. Esto crea oportunidades y desafíos: por un lado, podemos aprovechar la implementación eficiente de llama.cpp (por ejemplo, ubatch_size) para servir modelos de vectorización/重排器; por otro lado, las implementaciones de vectorización de llama.cpp se desarrollaron principalmente para arquitecturas más antiguas de solo codificador (como los modelos basados en RoBERTa) y no se han puesto al día por completo con los modelos modernos de vectorización/重排器 de solo decodificador. Este artículo comparte lo que aprendimos al adaptar los modelos de vectorización modernos para que funcionen con el formato GGUF y las herramientas de llama.cpp, por ejemplo, llama-embedding y llama-serving.
tagGGUF Base y Cuantificados
jina-embeddings-v4 se basa en Qwen2.5-VL-3B-instruct con tres adaptadores LoRA: retrieval (optimizado para tareas de recuperación), text-matching (optimizado para tareas de similitud de oraciones) y code (optimizado para tareas de recuperación de código). También está muy entrenado para la recuperación visual de documentos y la salida multi-vector de estilo de interacción tardía. Así que la idea aquí es aprovechar la implementación de gráfico existente de llama.cpp de Qwen2.5-VL-3B y usar llama-embedding para la inferencia.
Sin embargo, lo primero que notamos fue un comportamiento defectuoso en la implementación de transformador de visión o mmproj en llama.cpp, que produce diferentes vectorizaciones con respecto a la implementación torch de Qwen2.5-VL-3B dada la misma entrada de imagen. Mientras solucionamos este problema en nuestra bifurcación, decidimos excluir la torre de visión de las versiones GGUF por ahora. Puedes encontrar más detalles sobre esta discusión aquí.
La salida de vectorización multi-vector tampoco es compatible de forma predeterminada, pero no es un problema tan grande como los transformadores de visión. La salida multi-vector proviene de un MLP entrenado en el último bloque de transformador, por lo que, en el peor de los casos, siempre podemos exportar este MLP por separado a numpy y aplicarlo después de obtener las vectorizaciones a nivel de 词元 de llama.cpp: que es lo que hicimos para jina-reranker-m0-GGUF. Claro, no es muy eficiente, pero funciona sin tener que modificar y recompilar llama.cpp.
Eliminamos el transformador de visión y el proyector multi-vector y obtuvimos tres modelos GGUF base en F16.
Así que, para cumplir plenamente con la implementación de gráfico existente de Qwen2.5-VL-3B de llama.cpp, eliminamos el transformador de visión y el proyector multi-vector en el último bloque de transformador y fusionamos todos los adaptadores LoRA de nuevo en el modelo de lenguaje base. Esto nos dio tres modelos v4 específicos para cada tarea con 3.09B parámetros cada uno, por debajo de los 3.75B parámetros originales del v4:
| Repositorio de HuggingFace | Tarea |
|---|---|
jinaai/jina-embeddings-v4-text-retrieval-GGUF |
Recuperación de texto |
jinaai/jina-embeddings-v4-text-code-GGUF |
Recuperación de código |
jinaai/jina-embeddings-v4-text-matching-GGUF |
Similitud de oraciones |
Luego usamos calibration_data_v5_rc.txt (que se puede encontrar aquí y es recomendado por Unsloth) para calibrar los tres modelos GGUF base y obtuvimos tres archivos imatrix, luego usamos llama-quantize con imatrix para cuantificar los modelos de float16 de la siguiente manera:
# build imatrix
llama-imatrix -m jina-embeddings-v4-text-retrieval-F16.gguf -f calibration_data_v5_rc.txt -ngl 99 --no-ppl -o imatrix-retrieval-512.dat
# quantize
./quantize.sh jina-embeddings-v4-text-retrieval-F16.gguf retrieval-i3 imatrix-retrieval-512.dat jinaai/jina-embeddings-v4-text-retrieval-GGUFEl script quantize.sh se muestra a continuación:
#!/bin/bash
F16_MODEL_FILE="$1"
OUTPUT_DIR="$2"
IMATRIX="$3"
HF_REPO="$4"
FILENAME="$(basename "$F16_MODEL_FILE")"
BASE_NAME="${FILENAME%-F16.gguf}"
BASE_NAME="${BASE_NAME%.gguf}"
mkdir -p "$OUTPUT_DIR"
# Array of quantization types
QUANT_TYPES=("IQ1_S" "IQ1_M" "IQ2_XXS" "IQ2_M" "Q2_K" "IQ4_NL" "IQ4_XS" "IQ3_XXS" "IQ3_S" "IQ3_M" "IQ3_XS" "Q3_K_M" "Q4_K_M" "Q5_K_S" "Q5_K_M" "Q6_K" "Q8_0")
for quant_type in "${QUANT_TYPES[@]}"; do
llama-quantize --imatrix "${IMATRIX}" "$F16_MODEL_FILE" "${OUTPUT_DIR}/${BASE_NAME}-${quant_type}.gguf" $quant_type 8
doneFinalmente, subimos todas las cuantificaciones a HuggingFace.
| Cuantificación | BPW | Tamaño del archivo (GB) |
|---|---|---|
| IQ1_S | 2.04 | 0.73 |
| IQ1_M | 2.19 | 0.79 |
| IQ2_XXS | 2.44 | 0.88 |
| IQ2_M | 2.94 | 1.06 |
| Q2_K | 3.29 | 1.18 |
| IQ3_XXS | 3.31 | 1.19 |
| IQ3_XS | 3.59 | 1.29 |
| IQ3_S | 3.76 | 1.35 |
| IQ3_M | 3.84 | 1.38 |
| Q3_K_M | 4.11 | 1.48 |
| IQ4_NL | 4.72 | 1.69 |
| IQ4_XS | 4.49 | 1.61 |
| Q4_K_M | 4.99 | 1.79 |
| Q5_K_S | 5.61 | 2.02 |
| Q5_K_M | 5.75 | 2.07 |
| Q6_K | 6.56 | 2.36 |
| Q8_0 | 8.50 | 3.05 |
| F16 | 16.00 | 5.75 |
| v3 (Transformers) | 16.00 | 1.10 |
| v4 (Transformers) | 16.00 | 7.40 |

tagUso y advertencias
Ahora podemos usar llama-server y llama-embedding para servir GGUFs para la creación de **vectores modelo**. A diferencia de las bibliotecas de transformers, donde tenemos la flexibilidad de escribir código personalizado de preprocesamiento de entrada, tenemos que manejar esta parte manualmente (a menos que queramos recompilar llama-server y llama-embedding). Específicamente, para obtener resultados que sean totalmente consistentes con el uso de AutoModel.from_pretrained("jinaai/jina-embeddings-v4")..., debe tener mucho cuidado con los prefijos y agregarlos manualmente a las entradas de su modelo GGUF. Aquí hay una tabla de referencia:
| Tarea | prompt_name en la implementación de Transformer |
Entrada real al modelo |
|---|---|---|
retrieval |
query (predeterminado) |
Query: {original_text} |
retrieval |
passage |
Passage: {original_text} |
text-matching |
query (predeterminado) |
Query: {original_text} |
text-matching |
passage |
Query: {original_text} ⚠️ |
code |
query (predeterminado) |
Query: {original_text} |
code |
passage |
Passage: {original_text} |
Algunos usuarios podrían encontrar sorprendente ⚠️ que prompt_name='passage' se anule a "Query: " cuando se usa text-matching en el AutoModel.from_pretrained("jinaai/jina-embeddings-v4").... original. Pero esto en realidad tiene sentido ya que text-matching es una tarea de similitud de oraciones sin roles de izquierda/derecha: las entradas son simétricas.
tagVía llama-server
Después de instalar llama.cpp, puede ejecutar llama-server para alojar el modelo de **vector modelo** como un servidor HTTP compatible con la API de OpenAI. Por ejemplo, para usar text-matching con F16, puede hacer lo siguiente:
llama-server -hf jinaai/jina-embeddings-v4-text-matching-GGUF:F16 --embedding --pooling mean -ub 8192
--pooling mean es necesario ya que v4 son **vectores modelo** de agrupación media.
Luego envíe la solicitud a través de:
curl -X POST "http://127.0.0.1:8080/v1/embeddings" \
-H "Content-Type: application/json" \
-d '{
"input": [
"Query: A beautiful sunset over the beach",
"Query: Un beau coucher de soleil sur la plage",
"Query: 海滩上美丽的日落",
"Query: 浜辺に沈む美しい夕日"
]
}'
Cuando use los modelos retrieval y code, agregue Query: o Passage: delante de su entrada, así:
curl -X POST "http://127.0.0.1:8080/v1/embeddings" \
-H "Content-Type: application/json" \
-d '{
"input": [
"Query: A beautiful sunset over the beach",
"Query: Un beau coucher de soleil sur la plage",
"Passage: 海滩上美丽的日落",
"Passage: 浜辺に沈む美しい夕日"
]
}'
tagVía llama-embedding
Para una verificación rápida de cordura, también puede usar el llama-embedding precompilado para la creación de **vectores modelo** de un solo disparo. No recomendamos usarlo para la creación de **vectores modelo** en masa, ya que tiene algunos problemas de rendimiento que discutiremos en la siguiente sección:
llama-embedding -hf jinaai/jina-embeddings-v4-text-matching-GGUF:F16 --pooling mean -p "Query: jina is awesome" --embd-output-format json 2>/dev/null
Lea la siguiente sección para obtener una creación de **vectores modelo** en masa más eficiente con nuestra compilación de llama-embedding con algunas correcciones y mejoras.
tagResumen de las advertencias
Antes de pasar a una implementación más eficiente, resumamos las advertencias de los modelos GGUF:
- Debe agregar manualmente
Query:oPassage:delante de las entradas de texto. - No pueden manejar la entrada de imágenes en este momento porque eliminamos los transformadores de visión del modelo GGUF. Tuvimos que eliminarlos debido a errores en la implementación del transformador de visión/
mmprojdeQwen2.5-vl-3bde llama.cpp, que estamos trabajando para solucionar con upstream. - No pueden generar **vectores modelo** multivectoriales, ya que no es parte de la implementación del gráfico
Qwen2.5-vl-3bdellama.cpp. La solución más fácil sin recompilarllama.cppes exportar y ejecutar el MLP por separado después de obtener los **vectores modelo** a nivel de **token** estableciendo--pooling noneenllama-embedding. - v4 se entrena con el aprendizaje de representación de Matryoshka, y la conversión a GGUF conserva esta característica. Si obtiene **vectores modelo** con forma
NxD, simplemente puede usarembeddings[:, :truncate_dim]para obtener **vectores modelo** truncados más pequeños. Sin embargo, no todas las dimensiones están entrenadas. Para v4, entrenamostruncate_dimpara estos valores específicos:[128, 256, 512, 1024, 2048]. Esto significa que la calidad deembeddings[:, :131]no será una interpolación entre la calidad deembeddings[:, :128]yembeddings[:, :256], sino que será significativamente peor que los **vectores modelo** de 128 dimensiones o 256 dimensiones porque 131 dimensiones no están entrenadas. - La fragmentación tardía aún puede funcionar como parte del posprocesamiento después de obtener los **vectores modelo** a nivel de **token** a través de
--pooling none. Al igual que lo que hicimos al separar el MLP del gráficollama.cpp, esto no es súper eficiente, pero no requiere recompilación. Sin embargo, hay otra advertencia: dado que v4 es un modelo causal, la fragmentación tardía ya no será bidireccional: los **vectores modelo** de fragmentos anteriores no contendrán información contextual de los fragmentos posteriores. Recuerde que en v3, cada **vector modelo** de fragmento tenía información de contexto global porque usamos máscaras de atención bidireccionales en los bloques de transformadores. Internamente, discutimos si la causalidad hace que la fragmentación tardía sea obsoleta: algunos argumentan que "el contexto también es causal", lo que significa que un lector procesa el texto de izquierda a derecha, por lo que el contexto necesario para interpretar una oración debe provenir del texto anterior. Otros dicen que restringir la fragmentación tardía para que sea unidireccional bloquea el intercambio de contexto entre fragmentos. De cualquier manera, la efectividad de la fragmentación tardía en v4 sigue siendo cuestionable y necesita más estudio.
tagCreación de **vectores modelo** eficiente a través de llama-embedding
llama-embedding es un wrapper de C++ relativamente simple encima de llama.cpp para la creación de **vectores modelo** de texto con E/S muy limpia: stdin, stdout. Nos estamos centrando en mejorar esto en lugar de llama-server en este momento porque hay toneladas de otros problemas como la cola de red, el equilibrio de carga, la multiinquilinato y la serialización que creemos que están fuera del alcance en este punto. Nuestra pregunta es sencilla: ¿cuánta velocidad podemos obtener de una GPU L4 de 24 GB y cuál es el uso máximo de VRAM para la creación de **vectores modelo** de documentos largos?
¿Pero por qué L4? Principalmente porque GCP ofrece funciones de Cloud Run bastante convenientes encima, y es el tipo de GPU más ampliamente disponible y económico que puede obtener para las API de inferencia sin servidor. GCP ofrece A100 y H100 en Cloud Run a petición, y el equipo de GCP nos propone de vez en cuando usar mejores GPU. Pero nuestra filosofía es simple: si necesitamos A100/H100 para servir un modelo 3B, eso es claramente un problema de habilidad por nuestra parte.
Para algunos antecedentes, en llama.cpp, el tamaño de lote lógico (-b) representa el número máximo de tokens enviados al modelo en una sola llamada de evaluación. Cuando se procesan entradas largas, se dividen en fragmentos hasta este tamaño. El **tamaño de lote físico** (-ub) es el número real de tokens procesados simultáneamente en un pase directo a través del hardware, limitado por la memoria disponible. Las actualizaciones de la caché KV se realizan después de que se completa cada lote físico. La **Ventana de Contexto** (-c) es el límite estricto de cuántos tokens puede "ver" el modelo a la vez; para los modelos v4, esto es 32,000 tokens, lo que representa el alcance máximo de atención del modelo. Todos los tokens deben caber dentro de esta ventana de contexto para mantener una atención coherente en toda la secuencia. La siguiente figura ilustra sus relaciones.

tagNuestras correcciones
En nuestra bifurcación anterior, realizamos varias optimizaciones para que llama-embedding sea más eficiente:
- Manejo simplificado de lotes: establecimos automáticamente
-bigual a-c, lo que hace que este parámetro sea obsoleto. Los usuarios ya no necesitan especificar-b, ya que siempre aprovechamos la longitud completa del contexto del modelo para el procesamiento por lotes lógico. - Control de memoria flexible: a diferencia de la implementación original donde
-ubse veía obligado a ser igual a-b(ya que asumían que los modelos de Embedding no pueden ser causales), permitimos a los usuarios configurar-ubde forma independiente. Esto brinda un control preciso sobre el uso máximo de VRAM al codificar contextos largos; puede procesar un contexto de 32K con un pequeño lote físico de 512 tokens para permanecer dentro de los límites de VRAM gracias a la implementación de la caché KV. Tenga en cuenta que este cambio solo es correcto para los modelos de Embedding causales como jina-embeddings-v4; para las arquitecturas de solo codificador como v3, esta sería la implementación incorrecta. - Agrupación media fija: corregimos el cálculo de la agrupación media para los Embeddings cuando
ub < b, que anteriormente estaba roto en la implementación original.
Este cambio facilita mucho el trabajo con modelos de Embedding largos de solo decodificador mientras se administran las limitaciones de memoria de manera efectiva. Ahora los usuarios solo necesitan configurar dos parámetros:
-c: La longitud máxima del contexto (cuántos tokens puede procesar el modelo de Embedding)-ub: El tamaño del lote físico (cuántos tokens procesa la GPU a la vez)
Entonces, el código exacto para ejecutar nuestra bifurcación en L4 es el siguiente:
# Compile
git clone https://github.com/hanxiao/llama.cpp.git
cd llama.cpp
cmake -B build -DGGML_CUDA=ON
cmake --build build --config Release -j 8
# Run
INPUT_PREFIX="Query: " # or "Passage: "
cat big_input.txt | sed "s/^/${INPUT_PREFIX}/" | \
./llama.cpp/build/bin/llama-embedding -f /dev/stdin \
-hf "jinaai/jina-embeddings-v4-text-retrieval-GGUF:FP16" \
--pooling mean \
--no-escape \
--embd-output-format array \
--ubatch-size 512 \
--ctx-size 8192 \
--flash-attn \
-ngl 99 \
> "embeddings.txt" 2> "error.log"Cada línea en big_input.txt es una oración que se va a incorporar. --no-escape se debe establecer para evitar que \n dentro de la oración se interprete como separadores. --flash-attn y -ngl 99 se deben configurar para obtener el mejor rendimiento en la GPU L4.
tagBenchmark
Queremos comprender las siguientes preguntas a través de la evaluación comparativa:
- ¿Qué tan buena es nuestra cuantificación en comparación con la v4 Float16 original? ¿En qué punto se degrada tanto que sería mejor usar los Embeddings v3?
- ¿Qué tan rápido puede ejecutarse cada cuantificación en L4 y cuál es el uso máximo de VRAM?
- ¿Cómo afectan el tamaño del lote físico
-uby la longitud del contexto-ca la velocidad y al uso máximo de VRAM?
Los conjuntos de datos que utilizamos en la evaluación comparativa son:
| Tarea | Documentos | Consultas | Pares relevantes | Longitud promedio del documento | Longitud máxima del documento | Longitud promedio de la consulta |
|---|---|---|---|---|---|---|
| NanoHotpotQA | 5,090 | 50 | 100 | 57.3 | 345 | 14.9 |
| NanoSciFact | 2,919 | 50 | 56 | 205.8 | 1524 | 13.5 |
| NanoArguAna | 3,635 | 50 | 50 | 164.5 | 1058 | 193.0 |
| NanoNFCorpus | 2,953 | 50 | 2,518 | 223.3 | 1460 | 3.3 |
| NanoFiQA2018 | 4,598 | 50 | 123 | 159.1 | 1882 | 10.2 |
Utilizamos nuestra compilación personalizada de llama-embedding para la evaluación comparativa.
tagCalidad de las cuantificaciones
La versión cuantificada de mejor rendimiento es IQ3_M (3.84 BPW); las cuantificaciones por debajo de 2 bits funcionan peor que v3, por lo que tiene poco sentido usarlas.

| Cuantización | NanoHotpotQA | NanoFiQA2018 | NanoArguAna | NanoNFCorpus | NanoSciFact |
|---|---|---|---|---|---|
| IQ1_S | 0.6369 | 0.3178 | 0.3798 | 0.2933 | 0.5934 |
| IQ1_M | 0.6316 | 0.3313 | 0.5167 | 0.3256 | 0.6114 |
| IQ2_XXS | 0.7236 | 0.4582 | 0.4584 | 0.4067 | 0.7392 |
| IQ2_M | 0.7427 | 0.5869 | 0.5090 | 0.4468 | 0.7880 |
| Q2_K | 0.7683 | 0.5744 | 0.5168 | 0.4183 | 0.7546 |
| IQ3_XXS | 0.7780 | 0.5991 | 0.4811 | 0.4267 | 0.7610 |
| IQ3_XS | 0.7727 | 0.5615 | 0.5195 | 0.4439 | 0.7726 |
| IQ3_S | 0.8002 | 0.5505 | 0.4886 | 0.4381 | 0.7690 |
| IQ3_M | 0.8106 | 0.5387 | 0.5091 | 0.4462 | 0.7760 |
| Q3_K_M | 0.7567 | 0.5267 | 0.4486 | 0.4092 | 0.7775 |
| IQ4_NL | 0.7930 | 0.5598 | 0.4911 | 0.4285 | 0.7794 |
| IQ4_XS | 0.7979 | 0.5627 | 0.4947 | 0.4258 | 0.7789 |
| Q4_K_M | 0.8029 | 0.5569 | 0.4883 | 0.4226 | 0.7877 |
| Q5_K_S | 0.7969 | 0.5581 | 0.4721 | 0.4288 | 0.7842 |
| Q5_K_M | 0.7927 | 0.5601 | 0.4745 | 0.4247 | 0.7873 |
| Q6_K | 0.7951 | 0.5636 | 0.4822 | 0.4337 | 0.7846 |
| Q8_0 | 0.7938 | 0.5687 | 0.4784 | 0.4335 | 0.7851 |
| F16 | 0.7940 | 0.5610 | 0.4931 | 0.4343 | 0.7963 |
| v3 (Transformers) | 0.7393 | 0.5144 | 0.4600 | 0.4068 | 0.7820 |
| v4 (Transformers) | 0.7977 | 0.5571 | 0.4844 | 0.4351 | 0.7963 |
tagVelocidad y VRAM
Ahora fijamos el conjunto de datos de referencia a NanoHotpotQA y trazamos todas las cuantizaciones por sus bits por peso frente a la velocidad (medida en tokens por segundo) y el consumo de VRAM. Descubrimos que las versiones GGUF son ligeramente más rápidas que la versión original en FP16 (2023 frente a 1865 tokens/seg). La mayoría de las cuantizaciones se agrupan en torno a 2000-2100 tokens/seg. Con la atención flash habilitada, obtenemos una aceleración de ~77% en todas las cuantizaciones (3000+ frente a 2000+ tokens/seg). Sin embargo, la cuantización con mejor rendimiento Q8_0, con alrededor de 3700 tokens por segundo, sigue estando muy por detrás de la versión v3 original (572M parámetros), que alcanza los 16000 tokens/seg. Las versiones cuantificadas ahorran una considerable VRAM y casi alcanzan el nivel del modelo v3 FP16 con IQ3.

| Cuantización | BPW | Tamaño del archivo (GB) | VRAM máxima (GB) | Token/s con FA | Token/s sin FA |
|---|---|---|---|---|---|
| IQ1_S | 2.04 | 0.73 | 4.04 | 3625 | 2050 |
| IQ1_M | 2.19 | 0.79 | 4.09 | 3349 | 1997 |
| IQ2_XXS | 2.44 | 0.88 | 4.19 | 3701 | 2071 |
| IQ2_M | 2.94 | 1.06 | 4.37 | 3407 | 1989 |
| Q2_K | 3.29 | 1.18 | 4.49 | 3173 | 1905 |
| IQ3_XXS | 3.31 | 1.19 | 4.50 | 3668 | 2067 |
| IQ3_XS | 3.59 | 1.29 | 4.60 | 3604 | 2053 |
| IQ3_S | 3.76 | 1.35 | 4.66 | 3599 | 2049 |
| IQ3_M | 3.84 | 1.38 | 4.69 | 3603 | 2053 |
| Q3_K_M | 4.11 | 1.48 | 4.78 | 3450 | 2008 |
| IQ4_NL | 4.72 | 1.69 | 5.00 | 3571 | 2039 |
| IQ4_XS | 4.49 | 1.61 | 4.92 | 3585 | 2046 |
| Q4_K_M | 4.99 | 1.79 | 5.10 | 3558 | 2045 |
| Q5_K_S | 5.61 | 2.02 | 5.32 | 3567 | 2044 |
| Q5_K_M | 5.75 | 2.07 | 5.38 | 3528 | 2034 |
| Q6_K | 6.56 | 2.36 | 5.66 | 3334 | 1981 |
| Q8_0 | 8.50 | 3.05 | 6.36 | 3767 | 2101 |
| F16 | 16.00 | 5.75 | 9.70 | 3399 | 2023 |
| v3 (Transformers) | 16.00 | 1.10 | 2.82 | 16505 | |
| v4 (Transformers) | 16.00 | 7.40 | 14.45 | 1865 |
Haz clic para expandir la información del sistema
load_tensors: loading model tensors, this can take a while... (mmap = true)
load_tensors: offloading 36 repeating layers to GPU
load_tensors: offloading output layer to GPU
load_tensors: offloaded 37/37 layers to GPU
load_tensors: CUDA0 model buffer size = 3127.61 MiB
load_tensors: CPU_Mapped model buffer size = 315.30 MiB
...................................................................................
llama_context: constructing llama_context
llama_context: n_seq_max = 1
llama_context: n_ctx = 4096
llama_context: n_ctx_per_seq = 4096
llama_context: n_batch = 4096
llama_context: n_ubatch = 4096
llama_context: causal_attn = 1
llama_context: flash_attn = 1 // 1 for w/ FA in the table; 0 for w/o FA
llama_context: kv_unified = true
llama_context: freq_base = 1000000.0
llama_context: freq_scale = 1
llama_context: n_ctx_per_seq (4096) < n_ctx_train (128000) -- the full capacity of the model will not be utilized
llama_context: CUDA_Host output buffer size = 0.59 MiB
llama_kv_cache_unified: CUDA0 KV buffer size = 144.00 MiB
llama_kv_cache_unified: size = 144.00 MiB ( 4096 cells, 36 layers, 1/1 seqs), K (f16): 72.00 MiB, V (f16): 72.00 MiB
llama_context: CUDA0 compute buffer size = 2470.16 MiB
llama_context: CUDA_Host compute buffer size = 96.17 MiB
llama_context: graph nodes = 1234
llama_context: graph splits = 2
common_init_from_params: added <|endoftext|> logit bias = -inf
common_init_from_params: added <|im_end|> logit bias = -inf
common_init_from_params: added <|fim_pad|> logit bias = -inf
common_init_from_params: added <|repo_name|> logit bias = -inf
common_init_from_params: added <|file_sep|> logit bias = -inf
common_init_from_params: setting dry_penalty_last_n to ctx_size = 4096
common_init_from_params: warming up the model with an empty run - please wait ... (--no-warmup to disable)
system_info: n_threads = 4 (n_threads_batch = 4) / 8 | CUDA : ARCHS = 890 | USE_GRAPHS = 1 | PEER_MAX_BATCH_SIZE = 128 | CPU : SSE3 = 1 | SSSE3 = 1 | AVX = 1 | AVX2 = 1 | F16C = 1 | FMA = 1 | BMI2 = 1 | AVX512 = 1 | AVX512_VNNI = 1 | LLAMAFILE = 1 | OPENMP = 1 | REPACK = 1 |
main: n_tokens in batch = 0
main: number of embeddings = 5090
tagTamaño óptimo del lote físico y del contexto
Ahora fijamos el tipo de cuantificación a IQ3_S y examinamos cómo el tamaño del lote físico (-ub) y el tamaño del contexto (-c) afectan a la velocidad y a la VRAM. Los resultados en la GPU L4 muestran que -ub=512 con -c=2048 proporciona la configuración óptima, ofreciendo 4.143 tokens/seg mientras se utilizan 2.025MB de VRAM. La conclusión es intuitiva: cuando se conoce la longitud máxima de un único documento en la entrada, se debe utilizar un tamaño de contexto más pequeño que sea justo el necesario para cubrirlo. Para el tamaño del lote físico, 512 tokens parece ser el punto óptimo en la GPU L4.

Rendimiento de tokens por segundo
| ubatch_size | ctx_size=64 | ctx_size=128 | ctx_size=256 | ctx_size=512 |
|---|---|---|---|---|
| 64 | 2233 | 2093 | 2128 | 2125 |
| 128 | N/A | 2866 | 2821 | 2877 |
| 256 | N/A | N/A | 3287 | 3349 |
| 512 | N/A | N/A | N/A | 3469 |
| ubatch_size | ctx_size=2048 | ctx_size=4096 | ctx_size=8192 | ctx_size=16384 |
|---|---|---|---|---|
| 256 | 3971 | 3630 | 3593 | 2766 |
| 512 | 4143 | 3797 | 3758 | 2852 |
| 1024 | 4059 | 3742 | 3707 | 2822 |
| 2048 | 3957 | 3631 | 3603 | 2762 |
| 4096 | N/A | 3450 | 3410 | 2625 |
Uso máximo de VRAM (MB)
| ubatch_size | ctx_size=64 | ctx_size=128 | ctx_size=256 | ctx_size=512 |
|---|---|---|---|---|
| 64 | 1691 | 1689 | 1689 | 1697 |
| 128 | N/A | 1729 | 1727 | 1737 |
| 256 | N/A | N/A | 1803 | 1811 |
| 512 | N/A | N/A | N/A | 1963 |
| ubatch_size | ctx_size=2048 | ctx_size=4096 | ctx_size=8192 | ctx_size=16384 |
|---|---|---|---|---|
| 256 | 1885 | 1947 | 2099 | 2409 |
| 512 | 2025 | 2101 | 2257 | 2577 |
| 1024 | 2329 | 2407 | 2571 | 2917 |
| 2048 | 2933 | 3025 | 3203 | 3597 |
| 4096 | N/A | 4285 | 4497 | 4985 |
tagConclusión
Para los usuarios de v4 que quieran ejecutar GGUF cuantizado de forma eficiente en GPUs económicas, elijan IQ3_S o IQ3_M con nuestra compilación personalizada de llama-embedding - esto debería darles 4000 tokens/seg en conjuntos de datos regulares (donde la longitud de la frase es <2048 tokens). Para incrustar documentos más largos, aumente el tamaño del contexto -c y controle el tamaño del lote físico -ub para reducir la huella de VRAM. Con nuestra compilación personalizada, puede codificar documentos súper largos (>32K tokens) utilizando sólo 3GB de VRAM configurando -ub a un número pequeño como 1024 - algo que no era posible con la implementación original o los transformadores vainilla.
La búsqueda de la optimización de la velocidad nunca termina. Siempre hay espacio para implementaciones más rápidas y ágiles con mayor rendimiento. 4000 tokens/seg probablemente no sea nuestro límite máximo - hay mucho más trabajo por hacer. Más allá de arreglar la implementación del transformador qwen2.5-vl-3b mmproj/vision en llama.cpp, también estamos explorando optimizaciones más profundas a nivel de llama.graph y KV-cache, mejorando la lógica de procesamiento por lotes de llama-serving y añadiendo opciones de streaming a las APIs de incrustación. Nuestro objetivo es que llama.cpp soporte de forma nativa las incrustaciones multimodales modernas de sólo decodificador para nuestras versiones actuales y futuras de Reranker.






