BGE-M3 embeddings (dense, sparse, ColBERT) and reranking REST API. CUDA, CPU/ONNX, ROCm, Jetson.
279
Servizio embeddings + reranking ad alte prestazioni basato su BGE-M3.
BAAI/bge-m3 — produce embeddings dense (1024-d), sparse (lexical weights) e ColBERT (multi-vector token-level).BAAI/bge-reranker-v2-m3 — cross-encoder dedicato, un singolo logit per coppia query/doc. Switchabile via env all'ensemble pesato di BGE-M3.API REST in FastAPI, batching automatico delle richieste verso il modello, Docker pronto per CPU / CUDA / ROCm.
docker run -d --gpus all -p 8004:8004 aivanovitch/bge-m3-service:cuda
docker run -d --device=/dev/kfd --device=/dev/dri --group-add video \
-p 8004:8004 aivanovitch/bge-m3-service:rocm
docker run -d -p 8004:8004 aivanovitch/bge-m3-service:cpu
Il flavor CPU usa ONNX Runtime di default (più veloce di PyTorch su CPU, output identico). I modelli ONNX si scaricano da HuggingFace al primo avvio — a-ivanovitch/bge-m3-onnx e a-ivanovitch/bge-reranker-v2-m3-onnx. Monta un volume su
/root/.cache/huggingfaceper non riscaricarli a ogni restart. L'immagine CPU contiene solo ONNX Runtime: niente PyTorch né FlagEmbedding, quindiM3_BACKEND=flagembeddingqui non è disponibile.
Immagine non ancora pubblicata su questo account: per ora si builda da
Dockerfile.jetson, meglio direttamente sulla Jetson.
docker run -d --runtime=nvidia -p 8004:8004 aivanovitch/bge-m3-service:jetson
| Piattaforma | Stato | Image |
|---|---|---|
| CPU x86_64 / ARM64 | ✅ | :cpu (multiarch) |
| NVIDIA CUDA | ✅ | :cuda |
| AMD ROCm | ✅ | :rocm |
| NVIDIA Jetson (JetPack 6 / r36.4.0) | 🔜 in arrivo su aivanovitch/bge-m3-service | :jetson (arm64, L4T) |
return_dense / return_sparse / return_colbert)bge-reranker-v2-m3, default 3.0) — single-logit, qualità tipicamente superiorebge-m3, legacy) — score combinato dense_weight · dense + sparse_weight · sparse + colbert_weight · colbertcuda → mps → cpu, FP16 attivo solo su CUDA/ROCm/docs| Ruolo | Default v3.0 | Override env |
|---|---|---|
| Embedder | BAAI/bge-m3 | M3_MODEL_ID |
| Reranker (encoder) | bge-reranker-v2-m3 | M3_RERANK_ENCODER (valori: bge-reranker-v2-m3 | bge-m3) |
| Reranker (model id) | BAAI/bge-reranker-v2-m3 | M3_RERANK_MODEL_ID (override esplicito, utile per modelli fine-tuned) |
Se M3_RERANK_ENCODER=bge-m3 e M3_RERANK_MODEL_ID coincide con M3_MODEL_ID (default), il reranker riusa la stessa istanza dell'embedder → un solo modello in VRAM.
Altrimenti i due modelli vengono caricati separatamente (~2.2 GB FP16 totali su GPU).
GET /healthz # alias di /health
Risposta:
{
"status": "ok",
"device": "cuda",
"backend": "flagembedding",
"version": "3.2.1",
"model": "BAAI/bge-m3",
"embed_model": "BAAI/bge-m3",
"rerank_encoder": "bge-reranker-v2-m3",
"rerank_model": "BAAI/bge-reranker-v2-m3",
"rerank_normalize": true
}
(rerank_normalize è null quando M3_RERANK_ENCODER=bge-m3 — il flag non si applica all'ensemble.)
POST /v1/embeddings
Request:
{
"input": "Testo da embeddare",
"return_dense": true,
"return_sparse": true,
"return_colbert": false
}
Anche con array:
{
"input": ["Primo testo", "Secondo testo"],
"return_dense": true,
"return_sparse": false,
"return_colbert": false
}
I default dei tre flag sono return_dense=true, return_sparse=true, return_colbert=DEFAULT_RETURN_COLBERT (configurabile via M3_DEFAULT_RETURN_COLBERT, default false — colbert produce payload pesanti).
Risposta:
{
"results": [
{
"index": 0,
"embeddings": {
"dense": [0.123, -0.456, "..."],
"sparse": {
"indices": [101, 2023, "..."],
"values": [0.89, 0.76, "..."]
}
}
}
]
}
POST /v1/rerank
Request:
{
"query": "Come funziona il machine learning?",
"documents": [
"Il machine learning è una branca dell'IA che impara dai dati",
"La pasta carbonara è un primo romano",
"Le reti neurali sono il cuore del deep learning"
]
}
Weights opzionali (usati solo con M3_RERANK_ENCODER=bge-m3; col cross-encoder vengono accettati ma ignorati):
{
"query": "...",
"documents": ["..."],
"weights": {
"dense_weight": 0.35,
"sparse_weight": 0.35,
"colbert_weight": 0.30
}
}
Risposta (ordinata per relevance_score decrescente):
{
"results": [
{
"index": 0,
"document": {"text": "Il machine learning è una branca dell'IA che impara dai dati"},
"relevance_score": 0.94
},
{
"index": 2,
"document": {"text": "Le reti neurali sono il cuore del deep learning"},
"relevance_score": 0.81
},
{
"index": 1,
"document": {"text": "La pasta carbonara è un primo romano"},
"relevance_score": 0.04
}
]
}
Nota sui range di score:
- Cross-encoder
bge-reranker-v2-m3conM3_RERANK_NORMALIZE=true(default) → score in [0, 1] (sigmoid sui logits). Match buoni tipicamente > 0.9, irrilevanti vicini a 0.- Cross-encoder con
M3_RERANK_NORMALIZE=false→ logits raw (range circa −15 / +10): più informativi per chi taraturare threshold custom.- Ensemble
bge-m3→ score combinatidense_w·dense + sparse_w·sparse + colbert_w·colbert, scala diversa ancora.Se cambi
M3_RERANK_ENCODER(oM3_RERANK_NORMALIZE) i valori assoluti cambiano — gli ordinamenti restano comparabili.
import requests
# Embeddings
r = requests.post(
"http://localhost:8004/v1/embeddings",
json={
"input": "Testo da embeddare",
"return_dense": True,
"return_sparse": True,
},
)
emb = r.json()["results"][0]["embeddings"]
print("dense dim:", len(emb["dense"]))
print("sparse tokens:", len(emb["sparse"]["indices"]))
# Reranking
r = requests.post(
"http://localhost:8004/v1/rerank",
json={
"query": "machine learning",
"documents": [
"Deep learning è una tecnica di ML",
"La pasta al pomodoro",
"Neural networks e AI",
],
},
)
for item in r.json()["results"]:
print(f"{item['relevance_score']:.3f} {item['document']['text']}")
curl http://localhost:8004/healthz
curl -X POST http://localhost:8004/v1/embeddings \
-H 'content-type: application/json' \
-d '{"input": "Testo di esempio", "return_dense": true, "return_sparse": true}'
curl -X POST http://localhost:8004/v1/rerank \
-H 'content-type: application/json' \
-d '{
"query": "machine learning",
"documents": ["Deep learning è ML", "Pasta al pomodoro", "Neural networks"]
}'
Quando il servizio è in esecuzione:
M3_MAX_LENGTH)M3_RERANK_MAX_LENGTH)I testi oltre il limite vengono troncati: l'API risponde comunque 200 con il risultato calcolato sulla parte iniziale, senza segnalarlo nel body. Il server scrive nel log una riga per chiamata, per esempio rerank pair: 13/49 truncated to 2048 tokens (longest 8197). Se la vedi spesso, spezza i documenti in chunk più piccoli.
Il servizio implementa batching automatico del seguente tipo:
asyncio.Queue raccoglie richieste in arrivoM3_FLUSH_TIMEOUT (default 0.01s)M3_MAX_REQUESTS (default 16) richieste vengono accorpatebge-m3) si raggruppa per weights identici; se è cross-encoder si collassa tutto in una sola chiamataUn singolo ThreadPoolExecutor(max_workers=1) su GPU serializza le chiamate al modello (aumentarlo non aiuta — la GPU è serializzata comunque).
| Variabile | Default | Descrizione |
|---|---|---|
M3_PORT | 8004 | Porta HTTP |
M3_DEVICE | auto | cuda / cpu / mps / rocm / auto. Se richiesto un device non disponibile, fallback a CPU con warning |
M3_BACKEND | auto | Backend inferenza: onnx (default su CPU) | flagembedding (default su GPU) | auto |
M3_ONNX_REPO_EMBED / _RERANK | a-ivanovitch/bge-m3-onnx / a-ivanovitch/bge-reranker-v2-m3-onnx | Repo HF dei modelli ONNX (solo backend onnx). Usa M3_ONNX_DIR per caricarli da un path locale |
M3_MODEL_ID | BAAI/bge-m3 | Embedder (deve essere BGE-M3-compatibile) |
M3_RERANK_ENCODER | bge-reranker-v2-m3 | Tipo di reranker: bge-reranker-v2-m3 (cross-encoder) | bge-m3 (ensemble legacy) |
M3_RERANK_MODEL_ID | "" | Override esplicito del nome HF del reranker (per fine-tuned) |
M3_RERANK_NORMALIZE | true | Applica sigmoid sui logits del cross-encoder → score in [0, 1]. Solo per bge-reranker-v2-m3, ignorato per bge-m3 |
M3_DEFAULT_RETURN_COLBERT | false | Default del campo return_colbert nelle EmbedRequest |
BGE_MODEL_MEMORY | 0.30 | Frazione VRAM riservata al processo (per GPU < 8 GB potresti dover usare bge-m3 ensemble per evitare OOM) |
M3_BATCH_SIZE | 8 | Batch interno passato al modello |
M3_MAX_REQUESTS | 16 | Max richieste accorpate per batch |
M3_FLUSH_TIMEOUT | 0.01 | Finestra di accumulo batch (secondi) dal primo arrivo |
M3_MAX_LENGTH | 1024 | Lunghezza max per gli embedding (token) |
M3_RERANK_MAX_LENGTH | 2048 | Lunghezza max per il rerank: cross-encoder = coppia query+doc concatenata; ensemble bge-m3 = query e doc separati. Indipendente da M3_MAX_LENGTH (compito diverso) |
M3_RERANK_WEIGHTS | 0.35,0.35,0.30 | Pesi dense, sparse, colbert (usati solo con M3_RERANK_ENCODER=bge-m3) |
M3_REQUEST_TIMEOUT | 30 | Timeout HTTP end-to-end |
M3_MAX_QUEUE | 512 | Max dimensione queue del processor (backpressure) |
M3_CPU_WORKERS | 0 | Worker pool su CPU (0 = auto-sizing in base ai core) |
M3_DEBUG | false | Log dettagliato (weights, score per modalità, ecc.) |
docker run --rm --gpus all nvidia/cuda:12.8.0-base-ubuntu22.04 nvidia-smi
Tre leve in ordine di impatto:
# 1. Riusa la stessa istanza BGE-M3 anche per il rerank (un solo modello in VRAM)
docker run -e M3_RERANK_ENCODER=bge-m3 ...
# 2. Riduci la frazione VRAM riservata
docker run -e BGE_MODEL_MEMORY=0.15 ...
# 3. Batch più piccolo
docker run -e M3_BATCH_SIZE=4 -e M3_MAX_REQUESTS=8 ...
docker run -e M3_REQUEST_TIMEOUT=60 ...
AttributeError: ... has no attribute 'prepare_for_model' durante warmup rerankerFlagEmbedding 1.4.0 non è compatibile con transformers 5.x (rimosso prepare_for_model). Pin transformers a 4.57.6. Il run-dev.sh lo fa già; nelle install manuali ricordati il pin.
M3_BATCH_SIZE testi ordinati per lunghezza, come FlagEmbedding (prima un'unica inferenza su tutto il batch → OOM con pochi testi lunghi); warning di troncamento anche su CPU; ensemble bge-m3 su CPU normalizzato per la somma dei pesi come su GPU. Warning di troncamento accorpati in una riga per chiamata. LICENSE e NOTICE dentro le immagini + label OCI. Immagini spostate su aivanovitch/bge-m3-service, modelli ONNX su a-ivanovitch/* (HF)M3_RERANK_MAX_LENGTH (default 2048): lunghezza max del rerank separata da M3_MAX_LENGTH (embedding, 1024) — compiti diversi, alzare il rerank non invalida l'indice. Rimossa M3_MAX_Q_LENGTH (residuo pre-3.0.0 che col cross-encoder non troncava nulla, viveva solo come soglia di un warning). Warning di troncamento del cross-encoder ora misurato sulla coppia query+docM3_BACKEND), torch/FlagEmbedding resi lazy, modelli ONNX fp32 pubblicati su HF (a-ivanovitch/bge-m3-onnx, a-ivanovitch/bge-reranker-v2-m3-onnx)bge-reranker-v2-m3 default), M3_RERANK_ENCODER + M3_RERANK_MODEL_ID, M3_DEFAULT_RETURN_COLBERT, bump BGE_MODEL_MEMORY 0.20 → 0.30.tolist() su numpy arrays, refactor processor in due metodi, model + processor nel lifespan, alias /healthz, rimosso M3_GPU_TIMEOUT (non funzionava davvero)Il codice di questo servizio è rilasciato sotto PolyForm Noncommercial License 1.0.0 — testo completo anche su https://polyformproject.org/licenses/noncommercial/1.0.0.
L'uso non commerciale è consentito nei termini della licenza. Qualsiasi uso commerciale richiede una licenza separata.
Copyright © 2026 Aleksandr Ivanovitch. I diritti di proprietà intellettuale (IP) e ogni altro diritto correlato sul codice (codice sorgente, architettura, design delle API e logica di orchestrazione) appartengono ad Aleksandr Ivanovitch — vedi il file /app/NOTICE dentro l'immagine (insieme a /app/LICENSE). Per licenze commerciali e questioni di IP: @AIvanovitch su GitHub.
I modelli BAAI/bge-m3 (MIT License) e BAAI/bge-reranker-v2-m3 (Apache License 2.0) non sono coperti dalla licenza di questo servizio; lo stesso vale per le loro versioni ONNX derivate.
Content type
Image
Digest
sha256:f4acf294f…
Size
5.6 GB
Last updated
about 12 hours ago
docker pull aivanovitch/bge-m3-service:rocm