Skip to main content
Blog

How vLLM Semantic Router Trains Embedding Models and Publishes Them to Hugging Face

Training embeddings for semantic routing: domain data fine-tuned with LoRA, published to Hugging Face, and consumed by vLLM Semantic Router

vLLM Semantic Router does not only forward prompts to one LLM. It classifies and routes requests using signals — keywords, classifiers, and embeddings.

Embeddings turn text into vectors. The router then:

  • matches a query to category or anchor prompts (routing signals), or
  • decides whether a new query is similar enough to reuse a semantic cache hit.

A general-purpose embedder (for example MiniLM) is good on average, but weak on specialized language (medical, law, code). So the project fine-tunes embeddings for those jobs, then publishes them on Hugging Face for everyone to download.

Training does not live inside the Go router. It lives under src/training/model_embeddings/ (Python). Maintained models and datasets live under the official vllm-sr organization.

For full technical details, see the maintained Router Embeddings & Hugging Face Guide.

End-to-end picture​

Domain data (queries / Q&A)
↓
Build training triplets (anchor, positive, negative)
↓
Fine-tune base embedding model (LoRA or full)
↓
Evaluate (margin / MRR / recall)
↓
Upload to Hugging Face (<your-hf-namespace>/... or vllm-sr/...)
↓
Users / CI download model
↓
Semantic Router uses it for routing or cache similarity

There are two main pipelines.


Pipeline A — Cache embedding LoRA​

Path: src/training/model_embeddings/cache_embeddings/

Job: Improve semantic cache accuracy so “same intent” hits and “related but different” misses.

Step 1 — Collect unlabeled queries​

Prepare a JSONL file with one query per line for a domain (for example medical questions). See domains/README.md.

Step 2 — Generate triplets with an LLM​

generate_training_data.py runs vLLM in-process on local GPUs (requiring CUDA and tensor parallelism support) to build contrastive triplets from input JSONL rows containing {"query": "..."}:

FieldMeaningExample
AnchorParaphrase (generated query variant)“How do doctors diagnose diabetes?”
PositiveSame meaning (original seed query)Original query
NegativeDistinct question / related but different intent“What are diabetes symptoms?”

That teaches: paraphrases should be close; near-misses should stay apart — exactly what a cache needs.

python3 src/training/model_embeddings/cache_embeddings/generate_training_data.py \
--input data/cache_embeddings/medical/unlabeled_queries.jsonl \
--domain medical \
--output data/cache_embeddings/medical/triplets.jsonl \
--max-queries 10000

Step 3 — Train a LoRA adapter​

lora_trainer.py fine-tunes a small LoRA on a base such as sentence-transformers/all-MiniLM-L12-v2.

  • LoRA is small (on the order of hundreds of KB), not a full retrain.
  • Training uses contrastive / MNR-style loss on triplets.
python3 src/training/model_embeddings/cache_embeddings/lora_trainer.py \
--train-data data/cache_embeddings/medical/triplets.jsonl \
--base-model sentence-transformers/all-MiniLM-L12-v2 \
--output models/cache/medical-cache-lora \
--epochs 1

You can also train a multi-domain adapter by concatenating domain triplet files and training once.

Step 4 — Evaluate​

Primary cache metric:

margin = avg(similarity to positives) − avg(similarity to negatives)

Higher margin means better separation and fewer false cache hits.

python3 src/training/model_embeddings/cache_embeddings/evaluate_multi_domain.py \
--lora-path models/cache/medical-cache-lora \
--sample-size 2000

Step 5 — Push to Hugging Face​

Authenticate using standard environment variables and upload to your Hugging Face namespace (or vllm-sr if authorized):

cd models/cache/medical-cache-lora
export HF_TOKEN="hf_your_token"
huggingface-cli login --token "$HF_TOKEN"
huggingface-cli upload <your-hf-namespace>/medical-cache-lora . --repo-type model

Step 6 — Consume​

Download the adapter, attach it to the base MiniLM with PEFT, and encode queries for cache similarity:

from sentence_transformers import SentenceTransformer
from peft import PeftModel

base_model = SentenceTransformer("sentence-transformers/all-MiniLM-L12-v2")
base_model[0].auto_model = PeftModel.from_pretrained(
base_model[0].auto_model,
"<your-hf-namespace>/medical-cache-lora",
)
embedding = base_model.encode("What are the symptoms of diabetes?")

(Note: Cache LoRA adapters can be verified through PEFT directly in Python; native Router-side LoRA loading is a separate capability, and production Semantic Router configurations bind full embedding models.)


Pipeline B — Domain-adapted full embeddings​

Path: src/training/model_embeddings/domain_adapted_embeddings/

Job: Better retrieval / domain routing vectors (for example medical), not only cache LoRA.

Inspired by Adaptation of Embedding Models to Financial Filings via LLM Distillation.

Step 1 — Prepare data​

prepare_data.py loads a Hugging Face dataset or custom Q&A JSON (question → answering passage).

python src/training/model_embeddings/domain_adapted_embeddings/prepare_data.py \
--source huggingface \
--dataset keivalya/MedQuad-MedicalQnADataset

Step 2 — Iterative hard-negative mining​

  1. Embed with the current model.
  2. Mine hard triplets (true docs ranked low; wrong docs ranked high).
  3. Keep some easy triplets so the model does not forget.
  4. Fine-tune with TripletLoss (margin ≈ 0.1).
  5. Repeat about two iterations, accumulating triplets.

Base model is often mmbert-embed-32k-2d-matryoshka.

python src/training/model_embeddings/domain_adapted_embeddings/train.py \
--data-dir data \
--output-dir models/trained \
--base-model vllm-sr/mmbert-embed-32k-2d-matryoshka \
--iterations 2 \
--learning-rate 5e-5 \
--margin 0.1

Critical knobs: learning rate 5e-5, margin 0.1, easy:hard ratio about 2:1, and accumulate triplets across iterations.

Step 3 — Measure gains​

On MedQuAD, MRR@5 improved by +71.18% over the baseline after two iterations (lr=5e-5, recorded in src/training/model_embeddings/domain_adapted_embeddings/train.py). The published model is mmbert-embed-medical.

Step 4 — Upload the full model to Hugging Face​

Same pattern: local models/trained/best → huggingface-cli upload to <your-hf-namespace>/... (or vllm-sr/... when approved for official publication).


How Hugging Face fits (publish + pull)​

PhaseWhat happens
PublishAuthenticate with HF_TOKEN and upload to your personal namespace (<your-hf-namespace>) or the official vllm-sr org
Discoverhuggingface.co/vllm-sr
PullCI, docs, and users use snapshot_download / hf_hub_download / model id in config
IterateNew domain data or a better recipe → retrain → new revision on the Hub

Training is continuous in spirit: better data or hyperparameters → new fine-tune → new Hub upload.


How this reconnects to the running router​

User prompt
→ Embedding model (from HF / local cache)
→ Vector similarity vs anchors / cache entries
→ Signal(s)
→ Decision (AND/OR rules)
→ Algorithm (confidence, fusion, …)
→ Backend LLM

We do not train the chat LLM for routing. We train small embedding models so the router’s similarity signals stay accurate for real domains, then ship those models on Hugging Face as shared community assets.


Try it yourself​

TrackStart here
Maintained GuideRouter Embeddings & Hugging Face Guide
Cache LoRAcache_embeddings/README.md
Domain full fine-tunedomain_adapted_embeddings/USAGE.md
Hub orgvllm-sr

One-line summary​

vLLM Semantic Router fine-tunes embedding models with contrastive/triplet learning on domain triplets, uploads them to Hugging Face (such as the vllm-sr organization or community repositories), and downloads them at runtime so routing and semantic cache similarity stay accurate.