Sign inSign up

aivanovitch/bge-m3-service

By aivanovitch

•Updated about 12 hours ago

BGE-M3 embeddings (dense, sparse, ColBERT) and reranking REST API. CUDA, CPU/ONNX, ROCm, Jetson.

Image
0

279

aivanovitch/bge-m3-service repository overview

⁠BGE-M3 Embeddings Service

Servizio embeddings + reranking ad alte prestazioni basato su BGE-M3.

  • Embedder: BAAI/bge-m3 — produce embeddings dense (1024-d), sparse (lexical weights) e ColBERT (multi-vector token-level).
  • Reranker (default v3.0): 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.


⁠🚀 Quick Start (Docker)

⁠NVIDIA GPU
docker run -d --gpus all -p 8004:8004 aivanovitch/bge-m3-service:cuda
⁠AMD GPU (ROCm)
docker run -d --device=/dev/kfd --device=/dev/dri --group-add video \
  -p 8004:8004 aivanovitch/bge-m3-service:rocm
⁠CPU (x86 / ARM)
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/huggingface per non riscaricarli a ogni restart. L'immagine CPU contiene solo ONNX Runtime: niente PyTorch né FlagEmbedding, quindi M3_BACKEND=flagembedding qui non è disponibile.

⁠NVIDIA Jetson (Orin / JetPack 6)

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

⁠🖥️ Piattaforme supportate

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

⁠✨ Caratteristiche

  • Embeddings multi-modalità: dense, sparse, ColBERT — in una singola request (filtrabili via return_dense / return_sparse / return_colbert)
  • Reranking dual-mode:
    • Cross-encoder (bge-reranker-v2-m3, default 3.0) — single-logit, qualità tipicamente superiore
    • Ensemble BGE-M3 (bge-m3, legacy) — score combinato dense_weight · dense + sparse_weight · sparse + colbert_weight · colbert
  • Batching automatico: la finestra di accumulazione parte all'arrivo della prima richiesta, fonde input compatibili in una singola chiamata al modello
  • Device auto-detect: cuda → mps → cpu, FP16 attivo solo su CUDA/ROCm
  • OpenAPI + Swagger su /docs

⁠🧩 Modelli

RuoloDefault v3.0Override env
EmbedderBAAI/bge-m3M3_MODEL_ID
Reranker (encoder)bge-reranker-v2-m3M3_RERANK_ENCODER (valori: bge-reranker-v2-m3 | bge-m3)
Reranker (model id)BAAI/bge-reranker-v2-m3M3_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).


⁠🌐 API Endpoints

⁠Health Check
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.)

⁠Embeddings
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, "..."]
        }
      }
    }
  ]
}
⁠Reranking
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-m3 con M3_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 combinati dense_w·dense + sparse_w·sparse + colbert_w·colbert, scala diversa ancora.

Se cambi M3_RERANK_ENCODER (o M3_RERANK_NORMALIZE) i valori assoluti cambiano — gli ordinamenti restano comparabili.


⁠💻 Esempi

⁠Python
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
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"]
  }'

⁠📚 Documentazione API interattiva

Quando il servizio è in esecuzione:


⁠📏 Limiti

  • Max input per request: 1024 testi
  • Max documenti per rerank: 1024
  • Lunghezza massima embedding: 1024 token (M3_MAX_LENGTH)
  • Lunghezza massima rerank (coppia query+documento): 2048 token (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.


⁠⚡ Performance

Il servizio implementa batching automatico del seguente tipo:

  • Una asyncio.Queue raccoglie richieste in arrivo
  • All'arrivo della prima richiesta parte una finestra di M3_FLUSH_TIMEOUT (default 0.01s)
  • Entro quella finestra, fino a M3_MAX_REQUESTS (default 16) richieste vengono accorpate
  • Le frasi delle richieste embed compatibili vengono concatenate e passate al modello in una sola chiamata
  • Per i rerank: se il reranker è ensemble (bge-m3) si raggruppa per weights identici; se è cross-encoder si collassa tutto in una sola chiamata

Un singolo ThreadPoolExecutor(max_workers=1) su GPU serializza le chiamate al modello (aumentarlo non aiuta — la GPU è serializzata comunque).


⁠⚙️ Variabili d'ambiente

VariabileDefaultDescrizione
M3_PORT8004Porta HTTP
M3_DEVICEautocuda / cpu / mps / rocm / auto. Se richiesto un device non disponibile, fallback a CPU con warning
M3_BACKENDautoBackend inferenza: onnx (default su CPU) | flagembedding (default su GPU) | auto
M3_ONNX_REPO_EMBED / _RERANKa-ivanovitch/bge-m3-onnx / a-ivanovitch/bge-reranker-v2-m3-onnxRepo HF dei modelli ONNX (solo backend onnx). Usa M3_ONNX_DIR per caricarli da un path locale
M3_MODEL_IDBAAI/bge-m3Embedder (deve essere BGE-M3-compatibile)
M3_RERANK_ENCODERbge-reranker-v2-m3Tipo 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_NORMALIZEtrueApplica sigmoid sui logits del cross-encoder → score in [0, 1]. Solo per bge-reranker-v2-m3, ignorato per bge-m3
M3_DEFAULT_RETURN_COLBERTfalseDefault del campo return_colbert nelle EmbedRequest
BGE_MODEL_MEMORY0.30Frazione VRAM riservata al processo (per GPU < 8 GB potresti dover usare bge-m3 ensemble per evitare OOM)
M3_BATCH_SIZE8Batch interno passato al modello
M3_MAX_REQUESTS16Max richieste accorpate per batch
M3_FLUSH_TIMEOUT0.01Finestra di accumulo batch (secondi) dal primo arrivo
M3_MAX_LENGTH1024Lunghezza max per gli embedding (token)
M3_RERANK_MAX_LENGTH2048Lunghezza 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_WEIGHTS0.35,0.35,0.30Pesi dense, sparse, colbert (usati solo con M3_RERANK_ENCODER=bge-m3)
M3_REQUEST_TIMEOUT30Timeout HTTP end-to-end
M3_MAX_QUEUE512Max dimensione queue del processor (backpressure)
M3_CPU_WORKERS0Worker pool su CPU (0 = auto-sizing in base ai core)
M3_DEBUGfalseLog dettagliato (weights, score per modalità, ecc.)

⁠🐞 Troubleshooting

⁠GPU non rilevata in container
docker run --rm --gpus all nvidia/cuda:12.8.0-base-ubuntu22.04 nvidia-smi
⁠Out of Memory su GPU piccola

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 ...
⁠Timeout su richieste lunghe
docker run -e M3_REQUEST_TIMEOUT=60 ...
⁠AttributeError: ... has no attribute 'prepare_for_model' durante warmup reranker

FlagEmbedding 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.


⁠📦 Storia versioni rilevanti

  • 3.2.1 — backend ONNX (flavor CPU): inferenza a sotto-batch di 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)
  • 3.2.0 — 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+doc
  • 3.1.0 — backend ONNX Runtime come default per il flavor CPU (M3_BACKEND), torch/FlagEmbedding resi lazy, modelli ONNX fp32 pubblicati su HF (a-ivanovitch/bge-m3-onnx, a-ivanovitch/bge-reranker-v2-m3-onnx)
  • 3.0.0 — reranker switchabile (cross-encoder bge-reranker-v2-m3 default), M3_RERANK_ENCODER + M3_RERANK_MODEL_ID, M3_DEFAULT_RETURN_COLBERT, bump BGE_MODEL_MEMORY 0.20 → 0.30
  • 2.9.0 — fix accumulation window del batching, warning su device fallback, .tolist() su numpy arrays, refactor processor in due metodi, model + processor nel lifespan, alias /healthz, rimosso M3_GPU_TIMEOUT (non funzionava davvero)
  • 2.8.x — versione di partenza

⁠📄 Licenza

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.

Tag summary

Content type

Image

Digest

sha256:f4acf294f…

Size

5.6 GB

Last updated

about 12 hours ago

docker pull aivanovitch/bge-m3-service:rocm