11 KiB
Embeddings Module — INDEX
Embedding generation API for DIDI knowledge graph + semantic search. Serves BAAI/bge-m3 (1024-dim, multilingual, 8K context) via vLLM (or llama.cpp) behind an OpenAI-compatible FastAPI wrapper, used by didi-brain for atom storage in pgvector and for /v1/gather query embedding.
- Stack: Python 3.10+, FastAPI, uvicorn, httpx, Pydantic v2; backends via
vllm/vllm-openai:v0.8.5orghcr.io/ggml-org/llama.cpp:server - URLs:
- API wrapper:
http://10.11.10.12:14100(Prod) /http://10.11.10.12:54100(Dev) - vLLM internal:
:14101(Prod) /:54101(Dev) - llama.cpp internal:
:14110(Prod) /:54110(Dev)
- API wrapper:
- Container layout (2-tier):
didiAI-embeddings-api(FastAPI wrapper + auth + rate limiting)didiAI-embeddings-vllm(GPU embedding server) ORdidiAI-embeddings-llamacpp(CPU/lightweight server)
- Default model:
BAAI/bge-m3— 1024 dim, multilingual, 8K context
Ce face
Generate text embeddings for downstream DIDI consumers:
- didi-brain semantic atom storage — text claims/atoms get embedded once at ingestion, stored as pgvector columns.
- didi-brain
/v1/gatherquery embedding — incoming claim text gets embedded, used as the kNN probe vector. - Cross-encoder reranking — separate
rerankmodule (port 54200) handles re-scoring; this module only does dense embeddings. - Catalog / retrieval consumers — anything that needs a vector representation hits this single endpoint.
The module is OpenAI-compatible: drop-in for openai SDK code that uses client.embeddings.create().
API endpoints
All routes are mounted by src/embeddings/api/app.py via create_app(). Health is unauthenticated and excluded from rate-limiting; everything else passes through verify_bearer_token if EMB_API_TOKENS is set.
| Method | Path | Description |
|---|---|---|
POST |
/v1/embeddings |
OpenAI-compatible. Body: {input, model, encoding_format?, dimensions?, backend?}. Returns {object, data[], model, usage, backend}. |
GET |
/v1/models |
List available embedding models. Optional ?backend=vllm|llamacpp filter. |
GET |
/v1/backends |
List enabled backends as {backends: ["vllm", ...]}. |
GET |
/health |
Detailed health: per-backend status, overall healthy/degraded/unhealthy. |
GET |
/ready |
Simple readiness probe ({ready: true}) — for k8s. |
Source files:
/home/admin365/didi_mono/ai_platform/modules/embeddings/src/embeddings/api/routes/embeddings.py—POST /v1/embeddings/home/admin365/didi_mono/ai_platform/modules/embeddings/src/embeddings/api/routes/models.py—/v1/models,/v1/backends/home/admin365/didi_mono/ai_platform/modules/embeddings/src/embeddings/api/routes/health.py—/health,/ready
Errors: 400 (invalid params / backend disabled), 401 (auth), 429 (rate limit, sets Retry-After), 503 (backend connection), 504 (backend timeout), 500 (other backend failures).
Backends
Two interchangeable engines, selected per-request via the backend field or globally via EMB_DEFAULT_BACKEND:
- vLLM (production / GPU) —
vllm/vllm-openai:v0.8.5launched with--task embed, high-throughput batching, NVIDIA CUDA 12.x required. Listens internally on14101(Prod) /54101(Dev). - llama.cpp (dev / lightweight) —
ghcr.io/ggml-org/llama.cpp:server-b4769launched with--embedding, runs on CPU or GPU with GGUF-quantized models. Listens on14110/54110. - API wrapper — own FastAPI app on
14100(Prod) /54100(Dev) that proxies to whichever backend(s) are enabled.
Both backends speak OpenAI-compatible HTTP, so the wrapper just routes via httpx/openai SDK. Backend abstraction lives in src/embeddings/backends/{base,vllm_backend,llamacpp_backend,registry}.py.
Structura fisiere
embeddings/
├── INDEX.md (this file)
├── README.md quick-start + config table
├── API.md full HTTP API reference + SDK examples
├── pyproject.toml package def, optional extras: vllm, llamacpp, all, dev
├── uv.lock
├── .env.example all EMB_* env vars documented
├── deploy/
│ ├── Dockerfile multi-stage, python 3.11-slim + uv
│ ├── docker-compose.yml 3 services + 3 profiles (api/vllm/llamacpp)
│ └── deploy.sh wrapper around `docker compose --profile`
├── src/embeddings/
│ ├── __init__.py re-exports EmbeddingClient
│ ├── cli.py `python -m embeddings.cli` entry point
│ ├── client.py EmbeddingClient (Python library API)
│ ├── config.py EmbeddingSettings (pydantic-settings, EMB_ prefix)
│ ├── schemas.py EmbeddingRequest/Response/Data, base64 encoder
│ ├── types.py BackendType enum, ModelInfo
│ ├── exceptions.py BackendNotAvailable/Enabled, RateLimit, Timeout, etc.
│ ├── logging.py structured logging (text or JSON)
│ ├── api/
│ │ ├── app.py create_app() factory + lifespan
│ │ ├── dependencies.py FastAPI deps (auth, concurrency limiter, client)
│ │ ├── middleware.py RateLimitMiddleware, RequestIdMiddleware
│ │ └── routes/
│ │ ├── embeddings.py POST /v1/embeddings
│ │ ├── models.py /v1/models, /v1/backends
│ │ └── health.py /health, /ready
│ └── backends/
│ ├── base.py abstract Backend interface
│ ├── vllm_backend.py vLLM HTTP client
│ ├── llamacpp_backend.py llama.cpp HTTP client
│ └── registry.py BackendRegistry (selects + caches backends)
└── tests/
├── conftest.py
├── test_config.py
├── test_schemas.py
└── test_types.py
How didi-brain uses it
didi-brain consumes this service through shared/embedding_client.py (a thin wrapper around the openai SDK pointed at this API):
- Ingestion path — when atoms/claims are written, brain calls
EmbeddingClient.embed(text)to get a 1024-dim vector and stores it in pgvector alongside the row. One round-trip per batch. - Query path — for
/v1/gather, the incoming claim text is embedded the same way, then the resulting vector is used as the probe in a pgvector<=>(cosine) kNN search to retrieve candidate atoms. - Reranking — top-k candidates from the dense search are forwarded to the separate
rerankmodule (cross-encoder, port 54200) for fine-grained scoring. That module does not call this one — they're parallel concerns. - Catalog API — also retrieves via the same embedding pipeline (text → vector → kNN), reusing this single endpoint.
Because the wrapper is OpenAI-compatible, EmbeddingClient can be a vanilla openai.OpenAI(base_url=..., api_key=...) instance — no DIDI-specific client code needed in brain.
Configuration
All env vars use the EMB_ prefix. Required vars have no defaults — the app refuses to start without them.
Required
| Variable | Description |
|---|---|
EMB_DEFAULT_BACKEND |
vllm or llamacpp |
EMB_ENABLE_VLLM |
true / false |
EMB_ENABLE_LLAMACPP |
true / false |
EMB_EXTERNAL_URL |
Public URL exposed in the OpenAPI spec |
Common optional
| Variable | Default | Description |
|---|---|---|
EMB_PORT |
14100 (Prod) / 54100 (Dev) |
API server port |
EMB_HOST |
0.0.0.0 |
API bind address |
EMB_API_TOKENS |
unset | Comma-separated bearer tokens; auth disabled if unset |
EMB_VLLM_BASE_URL |
http://localhost:54101 |
Where vLLM listens |
EMB_VLLM_MODEL |
BAAI/bge-m3 |
HF model id loaded by vLLM |
EMB_VLLM_GPU |
0 |
CUDA_VISIBLE_DEVICES for vLLM |
EMB_VLLM_GPU_UTIL |
0.50 |
vLLM --gpu-memory-utilization |
EMB_VLLM_MAX_LEN |
8192 |
vLLM --max-model-len |
EMB_LLAMACPP_BASE_URL |
http://localhost:54110 |
Where llama.cpp listens |
EMB_LLAMACPP_MODEL |
bge-m3-q4_k_m.gguf |
GGUF filename inside MODELS_DIR |
EMB_LLAMACPP_CTX |
8192 |
llama.cpp context size |
EMB_LLAMACPP_THREADS |
4 |
llama.cpp threads |
EMB_LLAMACPP_PARALLEL |
4 |
llama.cpp parallel slots |
EMB_REQUEST_TIMEOUT |
120.0 |
Per-request backend timeout (s) |
EMB_CONNECT_TIMEOUT |
10.0 |
TCP connect timeout (s) |
EMB_RATE_LIMIT_RPS |
20.0 |
Token-bucket rate (req/s) |
EMB_RATE_LIMIT_BURST |
40 |
Token-bucket burst capacity |
EMB_MAX_CONCURRENT_REQUESTS |
20 |
In-flight cap |
EMB_LOG_LEVEL |
INFO |
DEBUG / INFO / WARNING / ERROR |
EMB_LOG_JSON |
false |
Emit JSON-formatted log lines |
HF_CACHE_DIR |
/cai2_ds_storage/hf_cache |
Mounted into vLLM container |
HF_TOKEN |
unset | Forwarded as HUGGING_FACE_HUB_TOKEN |
MODELS_DIR |
/cai2_ds_storage/models |
GGUF model directory for llama.cpp |
See /home/admin365/didi_mono/ai_platform/modules/embeddings/.env.example for the canonical, fully commented list.
Deployment
GPU host required for the production profile (e.g. 10.11.10.17). All three services live on the shared didi-network Docker network.
cd /home/admin365/didi_mono/ai_platform/modules/embeddings/deploy
# Configure
cp ../.env.example .env
$EDITOR .env
# Production: API + vLLM (GPU)
./deploy.sh --profile vllm -d
# Lightweight: API + llama.cpp
./deploy.sh --profile llamacpp -d
# API only (use external embedding servers via EMB_*_BASE_URL)
./deploy.sh --profile api -d
# Logs / shutdown
./deploy.sh --profile vllm --logs
./deploy.sh --profile vllm --down
Compose profiles:
api— only the FastAPI wrappervllm— wrapper + vLLM (GPU)llamacpp— wrapper + llama.cpp (CPU/GPU)
Performance / model
- Model:
BAAI/bge-m3— 1024 dim, multilingual (100+ languages), 8K token context, supports dense + sparse + multi-vector (this module uses dense only). - Throughput: ~100–200 embeddings/s on H100 with vLLM batching (depends on input length).
- Latency P95: ~50–100 ms per request (single text, warm GPU). Larger batches amortize well — keep request batches at 16–64 inputs for best throughput.
- Concurrency knobs:
EMB_MAX_CONCURRENT_REQUESTS(in-flight at the wrapper) andEMB_RATE_LIMIT_RPS(token-bucket) bound the load reaching the GPU. - GPU memory: bge-m3 fits comfortably in <4 GB;
EMB_VLLM_GPU_UTIL=0.50is intentionally low to allow GPU sharing with reranker / other workloads.
Related
didi-brainconsumes viashared/embedding_client.pyfor both ingestion (text → pgvector storage) and/v1/gatherquery embedding.rerankmodule (separate, port 54200) handles cross-encoder scoring on top of dense kNN candidates from this module — they're complementary, not chained inside this service.- Catalog API uses the same endpoint for retrieval-side embeddings.
- pgvector in the brain Postgres stores the resulting 1024-dim vectors (cosine distance index).
- Local Python use:
from embeddings import EmbeddingClient(works without the HTTP wrapper if you want in-process inference and have thevllm/llamacppextras installed).