Due settimane fa, abbiamo rilasciato i formati GGUF di jina-embeddings-v4, un modello di embedding universale per il recupero multimodale multilingue, con varie versioni quantizzate. La nostra motivazione era semplice: come modello con parametri da 3,75B, la versione transformer vanilla di jina-embeddings-v4 non scala bene sulle nostre istanze API GCP G2 (GPU L4), quindi volevamo accelerare l'inferenza utilizzando queste versioni GGUF più piccole e veloci. Durante i nostri esperimenti, abbiamo scoperto alcune scoperte interessanti durante la conversione e l'esecuzione di modelli di embedding GGUF. Poiché la maggior parte della community di llama.cpp
si concentra sugli LLM, abbiamo pensato che sarebbe stato utile condividere questo punto di vista dal punto di vista di un fornitore di embedding.
Ciò che è particolarmente rilevante è che i modelli di embedding odierni sono quasi identici agli LLM: ad esempio, jina-embeddings-v4 è basato su Qwen2.5-VL-3B-instruct
e jina-reranker-m0 è basato su Qwen2-VL-2B
. L'unica vera differenza è l'output: gli LLM sono generativi, gli embedding e i reranker sono discriminativi. Questo crea sia opportunità che sfide: da un lato, possiamo sfruttare l'implementazione efficiente di llama.cpp
(ad esempio, ubatch_size
) per servire modelli di embedding/reranker; dall'altro, le implementazioni di embedding di llama.cpp
sono state sviluppate principalmente per architetture encoder-only più vecchie (come i modelli basati su RoBERTa) e non si sono completamente allineate con i moderni modelli di embedding/reranker decoder-only. Questo articolo condivide ciò che abbiamo imparato adattando i moderni modelli di embedding per funzionare con il formato GGUF e gli strumenti di llama.cpp
, ad esempio llama-embedding
e llama-serving
.
tagGGUF di base e quantizzati
jina-embeddings-v4 è basato su Qwen2.5-VL-3B-instruct
con tre adattatori LoRA: retrieval
(ottimizzato per attività di recupero), text-matching
(ottimizzato per attività di similarità di frasi) e code
(ottimizzato per attività di recupero di codice). È anche ampiamente addestrato per il recupero di documenti visivi e l'output multi-vettore in stile late-interaction. Quindi l'idea qui è di sfruttare l'implementazione del grafo esistente di llama.cpp
di Qwen2.5-VL-3B
e utilizzare llama-embedding
per l'inferenza.
Tuttavia, la prima cosa che abbiamo notato è stato un comportamento difettoso nell'implementazione di mmproj
o vision transformer in llama.cpp, che produce embedding diversi rispetto all'implementazione torch di Qwen2.5-VL-3B
dato lo stesso input di immagine. Mentre stiamo risolvendo questo problema nel nostro fork, abbiamo deciso di escludere la vision tower dalle versioni GGUF per ora. Puoi trovare maggiori dettagli su questa discussione qui.
L'output di embedding multi-vettore non è supportato immediatamente, ma non è un problema così grande come i vision transformer. L'output multi-vettore proviene da un MLP addestrato all'ultimo blocco transformer, quindi nel peggiore dei casi possiamo sempre esportare questo MLP separatamente in numpy e applicarlo dopo aver ottenuto gli embedding a livello di token da llama.cpp
- che è quello che abbiamo fatto per jina-reranker-m0-GGUF
. Certo, non è molto efficiente, ma funziona senza dover modificare e ricompilare llama.cpp
.
Abbiamo rimosso il vision transformer e il proiettore multi-vettore e abbiamo ottenuto tre modelli GGUF di base in F16.
Quindi, per conformarsi pienamente all'implementazione del grafo esistente di llama.cpp
di Qwen2.5-VL-3B
, abbiamo rimosso il vision transformer e il proiettore multi-vettore all'ultimo blocco transformer e abbiamo unito tutti gli adattatori LoRA di nuovo nel modello linguistico di base. Questo ci ha dato tre modelli v4 specifici per attività a 3,09B di parametri ciascuno, rispetto ai 3,75B parametri originali di v4:
Repository HuggingFace | Attività |
---|---|
jinaai/jina-embeddings-v4-text-retrieval-GGUF |
Recupero di testo |
jinaai/jina-embeddings-v4-text-code-GGUF |
Recupero di codice |
jinaai/jina-embeddings-v4-text-matching-GGUF |
Similarità di frasi |
Quindi abbiamo usato calibration_data_v5_rc.txt
(che può essere trovato qui ed è raccomandato da Unsloth) per calibrare tutti e tre i modelli GGUF di base e abbiamo ottenuto tre file imatrix
, quindi abbiamo usato llama-quantize
con imatrix
per quantizzare i modelli da float16 come segue:
# 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-GGUF
Lo script quantize.sh
è mostrato di seguito:
#!/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
done
Alla fine, abbiamo caricato tutte le quantizzazioni su HuggingFace.
Quantizzazione | BPW | Dimensione File (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 |

tagUtilizzo e avvertenze
Ora possiamo usare llama-server
e llama-embedding
per servire i GGUF per l'incorporamento. A differenza delle librerie transformer dove abbiamo la flessibilità di scrivere codice di pre-elaborazione dell'input personalizzato, dobbiamo gestire questa parte manualmente (a meno che non vogliamo ricompilare llama-server
e llama-embedding
). In particolare, per ottenere risultati che siano completamente coerenti con l'utilizzo di AutoModel.from_pretrained("jinaai/jina-embeddings-v4")...
, devi essere molto attento ai prefissi e aggiungerli manualmente ai tuoi input del modello GGUF. Ecco una tabella di riferimento:
Compito | prompt_name nell'implementazione Transformer |
Input effettivo al modello |
---|---|---|
retrieval |
query (predefinito) |
Query: {original_text} |
retrieval |
passage |
Passage: {original_text} |
text-matching |
query (predefinito) |
Query: {original_text} |
text-matching |
passage |
Query: {original_text} ⚠️ |
code |
query (predefinito) |
Query: {original_text} |
code |
passage |
Passage: {original_text} |
Alcuni utenti potrebbero trovare sorprendente che ⚠️ prompt_name='passage'
venga sovrascritto in "Query: "
quando si utilizza text-matching
nell'originale AutoModel.from_pretrained("jinaai/jina-embeddings-v4")....
Ma questo ha effettivamente senso poiché text-matching
è un compito di similarità di frase senza ruoli di sinistra/destra: gli input sono simmetrici.
tagTramite llama-server
Dopo aver installato llama.cpp
, puoi eseguire llama-server
per ospitare il modello di embedding come un server HTTP compatibile con l'API OpenAI. Ad esempio, per usare text-matching
con F16
, puoi fare:
llama-server -hf jinaai/jina-embeddings-v4-text-matching-GGUF:F16 --embedding --pooling mean -ub 8192
--pooling mean
è richiesto poiché v4 è un embedding di mean-pooling.
Quindi invia la richiesta tramite:
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: 浜辺に沈む美しい夕日"
]
}'
Quando si utilizzano i modelli retrieval
e code
, aggiungi Query:
o Passage:
davanti all'input, in questo modo:
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: 浜辺に沈む美しい夕日"
]
}'
tagTramite llama-embedding
Per un rapido controllo di integrità, puoi anche usare il llama-embedding
precompilato per l'embedding one-shot. Non raccomandiamo di usarlo per l'embedding di massa poiché ha alcuni problemi di prestazioni che discuteremo nella prossima sezione:
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
Leggi la prossima sezione per un embedding di massa più performante con la nostra build di llama-embedding
con alcune correzioni e miglioramenti.
tagRiepilogo delle avvertenze
Prima di passare a un'implementazione più performante, riassumiamo le avvertenze dei modelli GGUF:
- Devi aggiungere manualmente
Query:
oPassage:
davanti agli input di testo. - Non possono gestire l'input di immagini in questo momento perché abbiamo rimosso i transformer di visione dal modello GGUF. Abbiamo dovuto rimuoverli a causa di bug nell'implementazione del transformer di visione/
mmproj
diQwen2.5-vl-3b
di llama.cpp, che stiamo lavorando per risolvere con l'upstream. - Non possono emettere embedding multi-vettore poiché non fa parte dell'implementazione del grafico
Qwen2.5-vl-3b
dillama.cpp
. La soluzione più semplice senza ricompilarellama.cpp
è esportare ed eseguire l'MLP separatamente dopo aver ottenuto gli embedding a livello di token impostando--pooling none
inllama-embedding
. - v4 è addestrato con l'apprendimento della rappresentazione Matryoshka e la conversione in GGUF preserva questa caratteristica. Se ottieni embedding con forma
NxD
, puoi semplicemente usareembeddings[:, :truncate_dim]
per ottenere embedding troncati più piccoli. Tuttavia, non ogni dimensione è addestrata. Per v4, abbiamo addestratotruncate_dim
per questi valori specifici:[128, 256, 512, 1024, 2048]
. Ciò significa che la qualità diembeddings[:, :131]
non sarà una sorta di interpolazione tra la qualità diembeddings[:, :128]
eembeddings[:, :256]
, ma sarà significativamente peggiore degli embedding a 128 dimensioni o a 256 dimensioni perché 131 dimensioni non è addestrato. - La chunking tardiva può ancora funzionare come parte della post-elaborazione dopo aver ottenuto gli embedding a livello di token tramite
--pooling none
. Proprio come abbiamo fatto con la separazione dell'MLP dal graficollama.cpp
, questo non è super efficiente ma non richiede la ricompilazione. Tuttavia, c'è un'altra avvertenza: poiché v4 è un modello causale, la chunking tardiva non sarà più bidirezionale: gli embedding di chunk precedenti non conterranno informazioni contestuali dai chunk successivi. Ricorda che in v3, ogni embedding di chunk aveva informazioni di contesto globale perché abbiamo usato maschere di attenzione bidirezionali nei blocchi transformer. Internamente, abbiamo discusso se la causalità renda obsoleta la chunking tardiva: alcuni sostengono che "il contesto è anche causale", il che significa che un lettore elabora il testo da sinistra a destra, quindi il contesto necessario per interpretare una frase dovrebbe provenire dal testo precedente. Altri dicono che limitare la chunking tardiva per essere unidirezionale blocca la condivisione del contesto tra i chunk. In ogni caso, l'efficacia della chunking tardiva in v4 rimane discutibile e necessita di ulteriori studi.
tagEmbedding efficiente tramite llama-embedding
llama-embedding
è un wrapper C++ relativamente semplice sopra llama.cpp
per incorporare testo con I/O molto pulito: stdin, stdout. Ci stiamo concentrando sul miglioramento di questo piuttosto che su llama-server
in questo momento perché ci sono un sacco di altri problemi come la coda di rete, il bilanciamento del carico, il multi-tenancy e la serializzazione che riteniamo siano fuori dal campo di applicazione a questo punto. La nostra domanda è semplice: quanta velocità possiamo ottenere da una GPU L4 da 24 GB e qual è l'utilizzo massimo di VRAM per incorporare documenti lunghi?
Ma perché L4? Principalmente perché GCP offre funzioni Cloud Run piuttosto convenienti sopra di esso, ed è il tipo di GPU più ampiamente disponibile ed economico che puoi ottenere per le API di inferenza serverless. GCP offre A100 e H100 su Cloud Run su richiesta, e il team di GCP ci propone di tanto in tanto di usare GPU migliori. Ma la nostra filosofia è semplice: se abbiamo bisogno di A100/H100 per servire un modello 3B, questo è chiaramente un problema di abilità da parte nostra.
Per un po' di background, in llama.cpp, la dimensione del batch logico (-b
) rappresenta il numero massimo di token inviati al modello in una singola chiamata di valutazione. Quando si elaborano input lunghi, questi vengono suddivisi in blocchi fino a questa dimensione. La dimensione fisica del batch (-ub
) è il numero effettivo di token elaborati simultaneamente in un singolo passaggio attraverso l'hardware, vincolato dalla memoria disponibile. La cache KV si aggiorna al completamento di ogni batch fisico. La Finestra di Contesto (-c
) è il limite massimo per il numero di token che il modello può "vedere" contemporaneamente: per i modelli v4 è di 32.000 token, che rappresenta la massima estensione dell'attenzione del modello. Tutti i token devono rientrare in questa finestra di contesto per mantenere un'attenzione coerente sull'intera sequenza. La figura seguente illustra le loro relazioni.

tagLe nostre correzioni
Nella nostra fork di cui sopra, abbiamo apportato diverse ottimizzazioni per rendere llama-embedding
più efficiente:
- Gestione semplificata dei batch: abbiamo impostato automaticamente
-b
uguale a-c
, rendendo di fatto obsoleto questo parametro. Gli utenti non devono più specificare-b
poiché sfruttiamo sempre la lunghezza del contesto completo del modello per il batch logico. - Controllo flessibile della memoria: a differenza dell'implementazione originale in cui
-ub
era forzato a essere uguale a-b
(poiché si presumeva che i modelli di embedding non potessero essere causali), consentiamo agli utenti di impostare-ub
in modo indipendente. Ciò offre un controllo preciso sull'utilizzo massimo della VRAM durante la codifica di contesti lunghi: è possibile elaborare un contesto di 32K con un piccolo batch fisico di 512 token per rimanere entro i limiti della VRAM grazie all'implementazione della cache KV. Si noti che questa modifica è corretta solo per i modelli di embedding causali come jina-embeddings-v4; per le architetture solo encoder come v3, questa sarebbe l'implementazione errata. - Pooling medio fisso: abbiamo corretto il calcolo del pooling medio per gli embedding quando
ub < b
, che in precedenza era errato nell'implementazione originale.
Questa modifica semplifica notevolmente l'utilizzo di modelli di embedding solo decoder con contesto lungo, gestendo efficacemente i vincoli di memoria. Gli utenti devono ora configurare solo due parametri:
-c
: la lunghezza massima del contesto (quanti token può elaborare il modello di embedding)-ub
: la dimensione fisica del batch (quanti token vengono elaborati contemporaneamente dalla GPU)
Quindi, il codice esatto per eseguire la nostra fork su L4 è il seguente:
# 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"
Ogni riga in big_input.txt
è una frase da incorporare. --no-escape
deve essere impostato per impedire che \n
all'interno della frase venga interpretato come separatore. --flash-attn
e -ngl 99
devono essere impostati per ottenere le migliori prestazioni sulla GPU L4.
tagBenchmark
Vogliamo capire le seguenti domande attraverso il benchmarking:
- Quanto è buona la nostra quantizzazione rispetto alla v4 Float16 originale? A che punto si degrada così tanto che sarebbe meglio usare gli embedding v3?
- Quanto velocemente può essere eseguita ogni quantizzazione su L4 e qual è l'utilizzo massimo della VRAM?
- In che modo la dimensione fisica del batch
-ub
e la lunghezza del contesto-c
influiscono sulla velocità e sull'utilizzo massimo della VRAM?
I set di dati che abbiamo utilizzato nel benchmarking sono:
Task | Documents | Queries | Relevant Pairs | Avg Doc Length | Max Doc Length | Avg Query Length |
---|---|---|---|---|---|---|
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 |
Abbiamo utilizzato la nostra build personalizzata di llama-embedding
per il benchmarking.
tagQualità delle quantizzazioni
La versione quantizzata con le migliori prestazioni è IQ3_M
(3,84 BPW): le quantizzazioni inferiori a 2 bit hanno prestazioni peggiori della v3, quindi è inutile utilizzarle.

Quantization | 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 |
tagVelocità e VRAM
Ora fissiamo il dataset di benchmark a NanoHotpotQA e tracciamo tutte le quantizzazioni in base ai loro bit per peso rispetto alla velocità (misurata in token al secondo) e al consumo di VRAM. Abbiamo scoperto che le versioni GGUF sono leggermente più veloci della versione vanilla a FP16 (2023 vs 1865 token/sec). La maggior parte delle quantizzazioni si raggruppa intorno a 2000-2100 token/sec. Con l'abilitazione di flash attention, otteniamo un aumento di velocità di circa il 77% su tutte le quantizzazioni (3000+ vs 2000+ token/sec). Tuttavia, la quantizzazione con le migliori prestazioni Q8_0
a circa 3700 token al secondo è ancora molto indietro rispetto alla v3 vanilla (572M di parametri), che raggiunge i 16000 token/sec. Le versioni quantizzate risparmiano notevolmente VRAM e si avvicinano quasi al livello del modello v3 FP16 con IQ3.

Quantization | BPW | File Size (GB) | Peak VRAM (GB) | Token/s w FA | Token/s w/o 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 |
Fare clic per espandere le informazioni di 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
tagDimensione ottimale del batch fisico e dimensione del contesto
Ora fissiamo il tipo di quantizzazione a IQ3_S
ed esaminiamo come la dimensione del batch fisico (-ub
) e la dimensione del contesto (-c
) influiscano su velocità e VRAM. I risultati sulla GPU L4 mostrano che -ub=512
con -c=2048
fornisce la configurazione ottimale, offrendo 4.143 token/sec utilizzando 2.025 MB di VRAM. La conclusione è intuitiva: quando si conosce la lunghezza massima di un singolo documento nell'input, utilizzare una dimensione del contesto più piccola, sufficiente a coprirlo. Per quanto riguarda la dimensione del batch fisico, 512 token sembra essere il punto ideale sulla GPU L4.

Prestazioni in termini di token al secondo
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 |
Utilizzo massimo di 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 |
tagConclusione
Per gli utenti di v4 che desiderano eseguire GGUF quantizzato in modo efficiente su GPU economiche, scegliete IQ3_S
o IQ3_M
con la nostra build personalizzata di llama-embedding
: questo dovrebbe fornire 4000 token/sec su dataset normali (dove la lunghezza della frase è <2048 token). Per incorporare documenti più lunghi, aumentate la dimensione del contesto -c
e controllate la dimensione del batch fisico -ub
per ridurre l'impronta di VRAM. Con la nostra build personalizzata, potete codificare documenti super lunghi (>32K token) utilizzando solo 3 GB di VRAM impostando -ub
su un numero piccolo come 1024, cosa che non era possibile con l'implementazione originale o con i transformer vanilla.
La ricerca dell'ottimizzazione della velocità non finisce mai. C'è sempre spazio per implementazioni più veloci e snelle con una produttività maggiore. 4000 token/sec probabilmente non è il nostro limite massimo: c'è ancora molto lavoro da fare. Oltre a correggere l'implementazione di mmproj/vision transformer di qwen2.5-vl-3b
in llama.cpp
, stiamo anche esplorando ottimizzazioni più approfondite a livello di grafo di llama e cache KV, migliorando la logica di batching di llama-serving
e aggiungendo opzioni di streaming alle API di embedding. Il nostro obiettivo è far sì che llama.cpp
supporti nativamente embedding multimodali moderni solo decoder per le nostre versioni future e attuali di Reranker.