Elastic
Jina AI
Modelli
API
keyboard_arrow_down
Lettore
Leggi gli URL e cerca sul web per ottenere LLM più approfonditi.
Incorporamenti
Incorporamenti multimodali multilingue.
Riclassificazione
Strumento di riordinamento per massimizzare la pertinenza dei risultati di ricerca.
Elastic Inference Service
Esegui i modelli Jina in modo nativo all'interno di Elasticsearch.
MCP
terminal
CLI
article
llms.txt
smart_toy
Agenti
data_object
Schema
menu_book
Documenti
Login
login
Comprendere l'input di immagini in Llama.cpp
Attenzione sui Token di immagine
Le nostre correzioni
Problemi rimanenti
Conclusione
Blog tecnico
settembre 09, 2025

Modelli di Embedding Multimodali in Llama.cpp e GGUF

Abbiamo introdotto i vettori modello multimodali in llama.cpp e GGUF, e abbiamo scoperto alcune problematiche sorprendenti lungo il percorso.
Andrei Ungureanu, Alex C-G • 11 minuti letti
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

Abbiamo introdotto diverse correzioni nel nostro fork di llama.cpp, in modo che funzioni con jina-embeddings-v4 sugli embeddings multimodali.

jina-embeddings-v4 introduce embeddings multimodali all'avanguardia in grado di elaborare testo, immagini e documenti visivi complessi per la ricerca vettoriale. Qualche settimana fa, abbiamo rilasciato i GGUF e le quantizzazioni dinamiche di v4 per attività di solo testo, che offrono un ingombro VRAM inferiore e prestazioni migliorate. Tuttavia, il supporto per gli embeddings multimodali su GGUF era ancora mancante. Per completare il quadro, abbiamo ora capito come generare embeddings multimodali con llama.cpp e GGUF. Dai un'occhiata a questo file README per la guida completa.

Ad essere onesti, llama.cpp upstream ha un certo supporto per l'input multimodale, ma poiché la maggior parte della community di llama.cpp si concentra su LLM e generazione di testo, il supporto per l'output di embeddings multimodali è completamente assente. In questo articolo, spiegheremo come abbiamo implementato gli embeddings multimodali in llama.cpp ed esamineremo le sue prestazioni (insieme a due versioni quantizzate) rispetto alla versione PyTorch di jina-embeddings-v4. Nel corso di questo articolo, faremo riferimento alla versione PyTorch come al nostro "modello di riferimento".

tagComprendere l'input di immagini in Llama.cpp

Ricapitoliamo innanzitutto come vengono gestiti gli embeddings multimodali con il nostro modello di riferimento. Innanzitutto, si associa ogni input immagine a un Prompt speciale:

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

Quindi il modello preelabora l'immagine, la codifica (tramite il suo ViT) e quindi elabora l'intera sequenza interleaved in un singolo passaggio in avanti.

Quando si tratta di llama.cpp, tuttavia, le cose sono più complicate. Sebbene supporti gli input di immagini per il completamento della chat, non supporta gli input multimodali, ovvero input (come quello sopra) che combinano sia testo che un'immagine. Questo è esattamente il motivo per cui abbiamo creato un fork di llama.cpp, modificando il gestore degli embeddings per accettare immagini codificate in base64, consentendoci così di elaborare contenuti multimodali in modo simile al gestore del completamento della chat.

Quindi ora, per lavorare con input multimodali in llama.cpp, possiamo iniziare con un Prompt simile a quello utilizzato dal modello di riferimento:

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

Il processo funziona come segue:

  1. llama.cpp circonda il Token <__image__> con marcatori di visione <|vision_start|> e <|vision_end|>, dandoci qualcosa di simile a questo: <|im_start|>user\n<|vision_start|><__image__><|vision_end|>Describe the image.<|im_end|>\n
  2. Il tokenizer sostituisce il Token speciale <__image__> con -1 durante la tokenizzazione del Prompt, segnalando internamente che la sequenza contiene un'immagine che deve essere codificata prima (di essere elaborata in seguito).
  3. I Token di testo prima del marcatore <__image__> (ovvero, <|im_start|>user\n<|vision_start|>) vengono decodificati tramite l'LLM e iniettati nella KVCache.
  4. L'immagine viene codificata tramite il componente ViT, producendo una serie di Token di immagine che vengono decodificati tramite l'LLM. Sebbene questi Token vengano elaborati separatamente dai Token nel passaggio uno, i livelli di attenzione possono comunque prestare attenzione a quei Token di testo tramite la KVCache. Tuttavia, a questo punto, i livelli di attenzione non possono prestare attenzione ad alcun Token di testo successivo (<|vision_end|>Describe the image.<|im_end|>\n).
  5. L'LLM decodifica tutti i Token di testo rimanenti (<|end_vision|>Describe the image.<|im_end|>\n). Ora i livelli di attenzione possono prestare attenzione a tutti i Token precedenti (sia di testo che di immagine) tramite la KVCache.

Il processo di inferenza degli embeddings (codifica dell'immagine e decodifica dei Token di testo/immagine) è mostrato nella figura seguente:

Figura 1: processo di inferenza degli embeddings della versione llama.cpp di jina-embeddings-v4.

tagAttenzione sui Token di immagine

A causa del meccanismo di attenzione, questo processo in più fasi può essere problematico per alcuni modelli. Ricapitoliamo rapidamente i diversi tipi di attenzione utilizzati nei modelli:

  • Attenzione causale: il meccanismo di attenzione per un Token in posizione k presta attenzione solo ai Token precedenti, nelle posizioni [0:k-1].
  • Attenzione non causale: il meccanismo di attenzione per un Token in posizione k presta attenzione a tutti i Token nella sequenza [0:n].

La figura seguente mostra i Token a cui il meccanismo di attenzione presterebbe attenzione durante l'elaborazione di img_tok_n nel secondo passaggio:

Figura 2: attenzione causale vs. non causale

Durante l'elaborazione di img_tok_n, lo stato del modello è il seguente:

  • Tutti i Token di testo precedenti (<|im_start|>, user, \n, <|vision_start|> ) sono già stati elaborati e salvati nella KVCache.
  • Tutti i Token di immagine (img_tok_1 a img_tok_n) vengono elaborati a questo punto, come parte della stessa sequenza.
  • Tutti i Token di testo successivi (<|vision_end|> , Describe , ecc. ) verranno elaborati in seguito.

Nel caso dell'attenzione causale, vengono presi in considerazione solo i Token precedenti quando si calcolano i punteggi di attenzione, con i Token passati recuperati tramite la KVCache .

Nel caso dell'attenzione non causale, tutti i Token dovrebbero essere presi in considerazione. Tuttavia, i futuri Token di testo (<|vision_end|> , Describe , ecc.) non sono ancora stati elaborati. Verranno elaborati in un passaggio futuro, quindi le cose si rompono velocemente.

Poiché jina-embeddings-v4 utilizza l'attenzione causale, il processo in più fasi funziona senza problemi, ma per altri modelli potrebbe non essere così.

In termini di embeddings, gli stati nascosti di ogni Token vengono catturati esattamente quando vengono elaborati e combinati in una singola sequenza alla fine. Attualmente, la normalizzazione e il pooling vengono gestiti in Python, ma (con un po' di lavoro extra) questo potrebbe essere fatto anche lato llama.cpp.

tagLe nostre correzioni

Dopo aver abilitato gli input di immagini per l'endpoint degli embeddings nel server llama.cpp, abbiamo iniziato a testare l'implementazione con benchmark e abbiamo notato differenze sorprendentemente grandi rispetto al nostro modello di riferimento. Sospettavamo che ci fosse qualcosa di sbagliato nell'implementazione di llama.cpp del ViT utilizzato da Qwen2.5-VL per codificare le immagini in embeddings di patch di immagine (rappresentazione vettoriale densa dei quadrati dell'immagine) che Qwen2.5 LLM può elaborare.

Ecco un esempio di come gli output ViT differiscono tra il modello di riferimento e l'implementazione 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)

Come puoi vedere, le differenze sono piuttosto evidenti. Per confermare che questo fosse l'unico problema, abbiamo precalcolato i Token di immagine in Python, quindi li abbiamo decodificati utilizzando l'implementazione di Qwen2.5 di llama.cpp (utilizzando solo l'LLM), sperando che gli embeddings risultanti corrispondessero molto più da vicino ai valori del modello di riferimento: tuttavia, questo non è stato il caso.

tagCorrezione n. 1: maschera di attenzione causale per i livelli di attenzione

Abbiamo continuato il debug esaminando i livelli di attenzione, la causa più probabile per le differenze numeriche. Abbiamo notato che la maschera di attenzione utilizzata dai livelli di attenzione non veniva calcolata correttamente per i Token di immagine. Per vederlo, possiamo tornare alla nostra sequenza di esempio:

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

Durante l'elaborazione dei Token di immagine, il marcatore <__image__> viene spacchettato in qualcosa come img_tok_1 img_tok_2 .... img_tok_last. Quindi la sequenza completa sarebbe:

<|im_start|>user\n<|vision_start|> img_tok_1 img_tok_2 ... img_tok_last <|vision_end|>Describe the image.<|im_end|>\n
⚠️
Nota: a questo punto della pipeline, sia i Token di testo (<|im_start|>, user , ecc.) che i Token di immagine ( img_tok_1,img_tok_2 ecc.), sono vettori densi e non token di testo letterali. Utilizziamo questa forma della sequenza per semplificare la spiegazione. Quando si decodifica img_tok_2, il meccanismo di attenzione *dovrebbe* fare attenzione a tutti i token precedenti, ovvero: ``` <|im_start|>user <|vision_start|> img_tok_1 ``` Tuttavia, un bug nella maschera di attenzione stava *invece* facendo sì che il meccanismo prestasse attenzione all'intera sequenza di immagini, come segue: ``` <|im_start|>user <|vision_start|> img_tok_1 img_tok_2 ... img_tok_last ``` Dopo aver corretto questo bug, gli incorporamenti del nostro modello llama.cpp (utilizzando i token di immagine pre-calcolati dal ViT del modello Torch) corrispondevano finalmente agli incorporamenti del modello di riferimento (entro un piccolo margine di errore). ### Correzione n. 2: Elaborazione delle immagini e incorporamenti di patch Anche il codificatore ViT di llama.cpp produceva diversi incorporamenti di immagini rispetto al modello di riferimento, con numeri che divergevano immediatamente dopo la pre-elaborazione. Ciò era particolarmente evidente durante la fase iniziale di creazione delle patch, in cui sia il nostro modello di riferimento che llama.cpp suddividono l'immagine grezza (valori dei pixel) in patch che vengono codificate tramite livelli convoluzionali. Le differenze tra le patch grezze (prima dell'elaborazione ViT) possono essere viste di seguito: ```text === 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) ``` ```text === 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) ``` Il nostro modello di riferimento e llama.cpp elaborano queste patch in modi diversi: * Il modello di riferimento raggruppa i valori dei pixel utilizzando operazioni di rimodellamento e quindi utilizza un singolo livello conv3d per codificare le patch di pixel pre-raggruppate. * Il modello llama.cpp crea e codifica queste patch con due livelli conv2d Per avvicinare gli **embeddings** del modello llama.cpp a quelli del modello di riferimento, abbiamo pensato che sarebbe stato più semplice utilizzare le operazioni esatte del modello di riferimento piuttosto che eseguire il debug dell'approccio di llama.cpp. Il nostro modello di riferimento genera patch di pixel utilizzando complesse operazioni di rimodellamento e trasposizione che richiedono tensori a 9 dimensioni. La libreria di elaborazione dei tensori di basso livello utilizzata in llama.cpp — ggml — non può supportarli, quindi per aggirare questo problema, abbiamo generato le patch utilizzando un servizio Python separato che chiama il server llama.cpp tramite HTTP. ggml manca anche del supporto per i livelli conv3d. Nel nostro modello di riferimento, la configurazione del livello conv3d è simile a questa: ```python 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 ) ``` Puoi vedere che stride e kernel_size sono gli stessi, il che significa che possiamo semplicemente appiattire gli input e i pesi del livello conv3d ed eseguire invece una semplice operazione di moltiplicazione di matrici. Per fare ciò, abbiamo modificato lo script di conversione in llama.cpp (convert_hf_to_gguf.py) per esportare una versione appiattita dei pesi conv3d per il livello di proiezione della patch: ```python 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)) ``` Per applicare l'operazione matmul invece dei due livelli conv2d in llama.cpp, abbiamo modificato il codice che costruisce il grafico del ViT Qwen2.5-VL: ```cpp 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 queste modifiche finali, gli **embeddings** finali delle immagini rientravano in un margine di errore del 2% rispetto al modello di riferimento (come si può vedere nella tabella di valutazione nella sezione successiva). ## Valutazione Dopo aver apportato queste modifiche, abbiamo valutato il modello llama.cpp rispetto al nostro modello di riferimento sui compiti ViDoRe utilizzando il benchmark MTEB. Puoi vedere lo script e le istruzioni per replicare questi risultati nel nostro fork llama.cpp e anche due versioni quantizzate.
Task Modello di riferimento 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
Media 84.17 84.33 84.28 84.23
Osservando la tabella dei risultati, in media, il modello llama.cpp e le sue varianti quantizzate non divergono molto dal modello di riferimento. Per confrontare i modelli in modo più approfondito, abbiamo utilizzato immagini provenienti da diversi domini e con diverse risoluzioni, tracciando la distanza tra gli **embeddings** delle patch di immagine (prima del pooling/normalizzazione). Più rossa è la patch, maggiore è la distanza coseno tra i vettori del modello di riferimento e il modello llama.cpp per quella particolare patch.

Figura 3: Pagina del jina-embeddings-v4 rapporto tecnico, risoluzione 372 × 526 (a sinistra), risoluzione 2481 × 3508 (a destra)

Figura 4: Screenshot dal sito web di Jina AI, risoluzione 594 × 428 (a sinistra), risoluzione 1982 × 1428 (a destra)

Figura 5: Tokyo, Shibuya di S K su Unsplash, risoluzione 383 × 255 (a sinistra), risoluzione 5472 × 3649 (a destra)

Miravamo a individuare eventuali schemi che andassero oltre le differenze di precisione numerica, schemi che potrebbero rivelare ulteriori bug o differenze tra i nostri modelli. Tuttavia, non erano visibili schemi particolari, tranne per il fatto che il numero di patch divergenti aumenta con la risoluzione dell'immagine. Queste differenze molto probabilmente appaiono a causa delle differenze di backend e non a causa di particolari bug nell'implementazione di Qwen2.5-VL (il modello backbone di jina-embeddings-v4).

Ciononostante, dobbiamo ribadire che queste differenze sono minime e i risultati del benchmark lo riflettono. Nel complesso, i modelli llama.cpp funzionano bene quanto il modello di riferimento, anche se producono vettori di 向量模型 leggermente diversi.

tagProblemi rimanenti

Ci sono diverse potenziali aree di miglioramento per gli 向量模型 multimodali in llama.cpp:

  • Quantizzazione dell'encoder di visione. Attualmente, llama.cpp supporta solo la quantizzazione per gli LLM, ma per ottenere una scalabilità migliore vorremmo quantizzare anche l'encoder di visione.
💡
Nota: llama.cpp supportava la quantizzazione dei modelli CLIP utilizzando llama-llava-quantize-cli, ma le risorse pertinenti sono state rimosse dall'introduzione della libreria mtmd.
  • Separazione dell'encoder di visione in un servizio dedicato. Gli encoder di visione utilizzano in genere maschere non causali, il che significa che ogni immagine deve essere codificata all'interno di una singola chiamata forward. Pertanto, non possiamo utilizzare il batching continuo. Tuttavia, potremmo considerare di separare l'encoder di visione in un servizio separato, che raggrupperebbe più immagini (anche da fonti separate) e le codificherebbe tutte in un singolo passaggio forward. Ciò significherebbe requisiti di vRAM più elevati, ma sarebbe anche molto più veloce della codifica di ogni immagine una per una. Questa separazione significherebbe anche che potremmo scalare automaticamente l'encoder di visione indipendentemente dal modello linguistico.
  • Abilitazione di 向量模型 multi-vettore. In questo articolo, abbiamo lavorato solo con 向量模型 a vettore singolo. Ma per sfruttare appieno jina-embeddings-v4, vorremmo anche abilitare 向量模型 multi-vettore per ottenere una maggiore precisione su immagini complesse. Questa sarebbe un'aggiunta facile poiché questi 向量模型 vengono generati con un singolo livello lineare sopra il modello base.

tagConclusione

Nonostante i bug e le battute d'arresto iniziali, l'integrazione di 向量模型 multimodali in llama.cpp produce ora risultati che corrispondono strettamente al nostro modello PyTorch di riferimento, anche su una serie di attività di benchmark. Le correzioni alla maschera di attenzione e all'elaborazione delle patch di immagine hanno rimosso le principali fonti di divergenza e anche le varianti quantizzate raggiungono una precisione simile pur utilizzando molte meno risorse. Le restanti differenze a risoluzioni di immagine più elevate appaiono minime e sono probabilmente dovute a variazioni del backend piuttosto che all'implementazione del modello principale.

Guardando al futuro, l'estensione della quantizzazione all'encoder di visione, l'abilitazione dell'elaborazione in batch tramite un servizio separato e il supporto di 向量模型 multi-vettore migliorerebbero ulteriormente sia l'efficienza che la precisione. Queste aggiunte renderebbero gli 向量模型 multimodali in llama.cpp più scalabili e più adatti ai casi d'uso reali.

Categorie:
Blog tecnico
rss_feed

Per saperne di più
marzo 11, 2026 • 7 minuti letti
Bootstrapping Audio Embeddings from Multimodal LLMs
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 minuti letti
Identifying Embedding Models from Raw Numerical Values
Han Xiao
Fingerprint illustration made from numbers, showcasing digital and high-tech design on a light background.
settembre 09, 2025 • 11 minuti letti
Multimodal Embeddings in Llama.cpp and GGUF
Andrei Ungureanu
Alex C-G
Cartoon llama in the center of a white background, emitting laser-like beams from its eyes. The illustration creates a playfu
Lingua / tema attuale
Search Foundation
Lettore
Incorporamenti
Riclassificazione
Ottieni la chiave API Jina
Limite di velocità
Chi siamo
Notizia
Scarica il logo Jina
open_in_new
Scarica il logo Elastic
open_in_new
Stato dell'API
Elastic © 2026.SicurezzaTermini & CondizioniPrivacyGestisci i cookieNon vendere né condividere le mie informazioni personali
Questo sito web e tutti i contenuti, software, prodotti e servizi ad esso associati sono destinati esclusivamente all'uso professionale. Non è previsto né raccomandato alcun utilizzo da parte dei consumatori.