Finetuning multi-vector fashions includes a number of parts: the mannequin itself, datasets, loss features, coaching arguments, evaluators, and the coach class. I am going to take a look at every of those parts, accompanied by sensible examples of how they can be utilized for finetuning robust multi-vector fashions.
Lastly, within the Analysis part, I am going to present you that my finetuned multi-vector-encoder/mLateOn-medical mannequin, skilled in 14.5 hours on a single RTX 3090 alongside this blogpost, simply outperforms each general-purpose retrieval mannequin I may discover on my medical retrieval analysis: dense, sparse, lexical, and multi-vector alike.

In the event you’re inquisitive about finetuning dense embedding fashions, sparse embedding fashions, or rerankers as an alternative, then contemplate studying by way of my prior Coaching and Finetuning Embedding Fashions, Coaching and Finetuning Sparse Embedding Fashions, and Coaching and Finetuning Reranker Fashions blogposts.
This blogpost is about coaching multi-vector fashions. If you wish to discover ways to use them, from loading and encoding to indexing in vector databases, see the companion Multi-Vector (Late Interplay) Embedding Fashions with Sentence Transformers blogpost.
Desk of Contents
What are Multi-Vector fashions?
A dense embedding mannequin compresses a complete textual content right into a single vector, and similarity is one dot product between two such summaries. A multi-vector mannequin (additionally known as a late-interaction or ColBERT-style mannequin) skips that compression. It retains one small vector per token and scores a question in opposition to a doc with the MaxSim operator, the place each question token finds its best-matching doc token and the scores are summed. Token-level matching preserves precisely the fine-grained alerts {that a} single vector has to common away, which often means stronger retrieval, at the price of a much bigger index.
The companion Multi-Vector Embedding Fashions blogpost covers the structure, encoding, scoring, and indexing intimately, so I am going to hold this part quick and get to the coaching.

Why Finetune?
Finetuning multi-vector fashions considerably improves their retrieval efficiency in your particular area: the vocabulary, the question fashion, and the notion of relevance all differ between internet search, authorized discovery, code search, and scientific literature overview. As a result of queries and paperwork are matched token by token, multi-vector fashions decide up fine-grained area alerts that single-vector fashions are likely to common away, and so they reply very properly to even modest quantities of in-domain finetuning information.
Past that, most launched retrieval fashions have been configured for brief passages. The basic ColBERT checkpoints truncate paperwork at 180 or 300 tokens, and plenty of fashionable dense fashions at 256 or 512, as a result of their MS MARCO-style coaching information hardly ever goes past that. In case your paperwork are lengthy, these fashions silently discard most of each doc earlier than scoring it. On my medical analysis with passages averaging 941 tokens, I measured that this truncation prices as much as 0.24 NDCG@10, significantly greater than any distinction between mannequin architectures. While you practice your individual mannequin, you configure the doc size that your information wants.
LightOn bumped into this similar dynamic with code retrieval, the place normal LateOn wasn’t sufficient and so they skilled LateOn-Code. Your area, whether or not that is medical, authorized, monetary, or your organization’s inside paperwork, is just not getting an official mannequin. This blogpost reveals you the best way to construct it your self, in a matter of hours, on a single shopper GPU.
Coaching Elements
Coaching MultiVectorEncoder fashions includes the next parts:
Mannequin: The mannequin to finetune or the structure to construct recent.
Dataset: The information used for coaching and analysis.
Loss Perform: A operate that measures the mannequin’s efficiency and guides the optimization course of.
Coaching Arguments (elective): Parameters that influence coaching efficiency, monitoring, and debugging.
Evaluator (elective): A category for evaluating the mannequin earlier than, throughout, or after coaching.
Coach: Brings collectively all coaching parts.
Let’s take a better have a look at every element.
Mannequin
Multi-vector coaching offers you an actual selection of start line, and it issues greater than you would possibly count on.
Finetuning an current multi-vector mannequin
If you wish to additional finetune an current multi-vector mannequin, you do not have to fret in regards to the structure in any respect:
from sentence_transformers import MultiVectorEncoder
mannequin = MultiVectorEncoder(
“lightonai/mLateOn-unsupervised”,
model_kwargs={“torch_dtype”: “float32”},
processor_kwargs={“model_max_length”: 8192},
)
The checkpoint brings its personal recipe alongside: its question and doc marker tokens, its projection head, its scoring skiplist. For finetuning, you typically wish to hold all of that and alter solely what your information calls for. The very first thing to test is the size configuration, since many launched checkpoints cap paperwork at 180 to 512 tokens (see Why Finetune?), and my medical passages run to 1,400 tokens. The mLateOn household already serves the spine’s full 8192 token context, but when your beginning checkpoint carries caps, carry them:
mannequin[0].query_length = None
mannequin[0].document_length = None
With the per-task caps unset, truncation falls again to the tokenizer’s model_max_length, which is why I configure that restrict at load time above.
I made yet another change, including a punctuation skiplist that excludes punctuation tokens from document-side scoring and storage. In a 4-way ablation (none, punctuation, stopwords, each) it modestly gained on high quality, and it shrinks the doc index by 9.6% on this information without cost:
import string
mannequin[2].skiplist_words = record(string.punctuation)
mannequin[2].resolve_with_tokenizer(mannequin.tokenizer)
Constructing one from a base transformer
You can even level MultiVectorEncoder at any base transformer, and a recent, randomly initialized token-level projection is appended for you:
from sentence_transformers import MultiVectorEncoder
mannequin = MultiVectorEncoder(“answerdotai/ModernBERT-base”, model_kwargs={“torch_dtype”: “float32”})
That is the basic ColBERT pipeline: a Transformer producing contextualized token embeddings, a token-level Dense projecting every of them right down to 128 dimensions, a MultiVectorMask deciding which tokens depend throughout scoring, and a token-level Normalize. The projection begins random, so coaching is required earlier than this mannequin is helpful. Apparently, this works with robust dense embedding backbones too. A recent projection on Alibaba-NLP/gte-modernbert-base reached inside 0.03 of the existing-checkpoint beginning factors in my experiments, from nothing however the projection and 25k coaching pairs.
The basic ColBERT tokenization methods ([MASK] question enlargement, [Q] / [D] prefix tokens, a doc size cap, a punctuation skiplist) are all off by default and configurable. See Creating Customized Fashions for the total set. For what it is value, I examined [MASK] question enlargement in 4 configurations for my area finetune and none of them made a measurable distinction, so do not feel obliged to succeed in for the basic recipe.
Which start line do you have to decide?
I measured this straight whereas making ready this blogpost, taking six beginning factors and coaching every with the similar recipe on 25k medical question-passage pairs from MIRIAD, then evaluating on 1,000 held-out questions in opposition to a 50,000 passage corpus:
The outcome stunned me, and it replicated throughout two mannequin households. *The -unsupervised checkpoints adapt to a brand new area much better than their completed siblings, overtaking them regardless of beginning decrease. These checkpoints sit after large-scale contrastive pretraining however earlier than supervised finetuning on normal retrieval, in order that they carry all of the late-interaction construction with not one of the general-purpose tuning that area coaching then has to undo. The completed checkpoints, against this, barely moved and even regressed, at each studying charge I attempted.
So, if the mannequin household you want publishes a pre-supervised checkpoint, begin there. If not, a recent projection on a powerful retrieval-pretrained spine is a detailed runner-up. Persevering with from a completely completed checkpoint is the weakest possibility for area adaptation, regardless of being essentially the most natural-feeling one.
Dataset
The MultiVectorEncoderTrainer makes use of datasets.Dataset or datasets.DatasetDict cases for coaching and analysis. You’ll be able to load information from the Hugging Face Datasets Hub or use native information in no matter format you like (e.g. CSV, JSON, Parquet, Arrow, or SQL).
Notice: Numerous public datasets that work out of the field with Sentence Transformers have been tagged with sentence-transformers on the Hugging Face Hub, so you may simply discover them on https://huggingface.co/datasets?different=sentence-transformers. Think about shopping by way of these to search out ready-to-go datasets that is perhaps helpful on your duties, domains, or languages.
Knowledge on the Hugging Face Hub
You should use the load_dataset operate to load information from datasets on the Hub:
from datasets import load_dataset
train_dataset = load_dataset(“tomaarsen/miriad-4.4M-split”, break up=“practice”)
print(train_dataset)
“””
Dataset({
options: [‘question’, ‘passage_text’],
num_rows: 4467542
})
“””
That is the dataset I am going to practice on on this blogpost: 4.4 million medical questions from MIRIAD, every paired with the supply passage that comprises its reply (averaging 941 tokens). Easy (question, related passage) pairs like these are the best retrieval coaching information to gather on your personal area, and as you may see, they’re all you want.
Native Knowledge
You can even use load_dataset for loading native information in widespread file codecs:
from datasets import load_dataset
dataset = load_dataset(“csv”, data_files=“my_file.csv”)
dataset = load_dataset(“json”, data_files=“my_file.json”)
And in case your native information requires pre-processing, you should utilize datasets.Dataset.from_dict to initialize your dataset with a dictionary of lists:
from datasets import Dataset
queries = []
paperwork = []
dataset = Dataset.from_dict({
“question”: queries,
“doc”: paperwork,
})
Dataset Format
It will be significant that your dataset format matches your loss operate (or that you just select a loss operate that matches your dataset format). Verifying whether or not a dataset format works with a loss operate includes two steps:
In case your loss operate requires a Label based on the Loss Overview desk, then your dataset should have a column named “label” or “rating”. This column is robotically taken because the label.
All columns not named “label” or “rating” are thought of Inputs based on the Loss Overview desk. The variety of remaining columns should match the variety of legitimate inputs on your chosen loss. The names of those columns are irrelevant, solely the order issues.
There are two multi-vector particular conventions on high of this:
Positional question and doc project: the primary column is embedded because the question and all following columns as paperwork, whatever the column names. This default may be overridden per column through the usual router_mapping coaching argument.
Data distillation format: one column per candidate doc, i.e. (question, document_1, …, document_N, scores) the place scores is an inventory of N trainer scores per row. For KD datasets that retailer question and doc IDs alongside separate textual content datasets (e.g. lightonai/ms-marco-en-bge), you should utilize resolve_ids to resolve the IDs to texts on the fly.
Loss Perform
Loss features quantify how properly a mannequin performs for a given batch of information, permitting an optimizer to replace the mannequin weights to provide extra beneficial (i.e., decrease) loss values. The correct loss operate on your process is determined by the info you might have and what you are attempting to realize. You could find a full record of choices within the Loss Overview.
For the widespread case of question-answer or question-passage pairs, the workhorse is in-batch negatives coaching with MultiVectorMultipleNegativesRankingLoss, the place each different doc within the batch acts as a adverse for every question. Larger batches imply extra negatives and stronger coaching, so in apply you may need its GradCache variant, CachedMultiVectorMultipleNegativesRankingLoss, which decouples the efficient batch dimension from what matches in your GPU:
from sentence_transformers import MultiVectorEncoder
from sentence_transformers.multi_vector_encoder.losses import CachedMultiVectorMultipleNegativesRankingLoss
mannequin = MultiVectorEncoder(“lightonai/mLateOn-unsupervised”, model_kwargs={“torch_dtype”: “float32”})
loss = CachedMultiVectorMultipleNegativesRankingLoss(
mannequin=mannequin,
mini_batch_size=16,
)
The mini_batch_size parameter bounds the reminiscence by encoding paperwork in chunks of this dimension, whereas the efficient contrastive batch dimension (128 in my run beneath, and in my ablations larger batches purchased nothing additional) stays a free selection. GradCache ensures similar outcomes whatever the chunk dimension, so decrease it for smaller GPUs at solely a wall-clock value. When your doc lengths range quite a bit, contemplate its sibling mini_batch_num_tokens, which packs every chunk to a complete token finances as an alternative of a doc depend, so a bit of unusually lengthy paperwork can by no means spike your reminiscence (my mini_batch_size=16 at roughly 940 tokens per doc corresponds to mini_batch_num_tokens=15_000).
One multi-vector particular lure is that the contrastive losses default to scale=1.0, in contrast to the dense embedding equal which defaults to scale=20.0. That 20.0 exists as a result of a cosine similarity is a single worth in [-1, 1], too slender a variety for a pointy softmax. A MaxSim rating as an alternative sums one best-match similarity per question token, so it already spans roughly [0, query_length]: a 32-token question can rating as much as 32. So do not copy scale=20.0 over from a dense coaching script, since it might saturate the softmax and kill your gradients.
For distillation from a stronger trainer, which is how the strongest general-purpose late-interaction fashions are skilled, see MultiVectorDistillKLDivLoss and the Data Distillation tab within the Coaching Overview documentation.
Coaching Arguments
You’ll be able to customise the coaching course of utilizing the MultiVectorEncoderTrainingArguments class. This class allows you to modify parameters that may influence coaching pace and show you how to perceive what’s occurring throughout coaching.
For extra info on essentially the most helpful coaching arguments, try the Multi-Vector Encoder > Coaching Overview > Coaching Arguments. It is value studying to get essentially the most out of your coaching.
This is an instance, utilizing the values from my precise coaching run:
from sentence_transformers import MultiVectorEncoderTrainingArguments
from sentence_transformers.base.sampler import BatchSamplers
args = MultiVectorEncoderTrainingArguments(
output_dir=“fashions/mLateOn-medical”,
num_train_epochs=1,
per_device_train_batch_size=128,
per_device_eval_batch_size=16,
learning_rate=1e-4,
warmup_steps=0.05,
prompts={“query”: “[Q] “, “passage_text”: “[D] “},
fp16=False,
bf16=True,
batch_sampler=BatchSamplers.NO_DUPLICATES,
eval_strategy=“steps”,
eval_steps=0.1,
save_strategy=“steps”,
save_steps=0.05,
logging_steps=0.01,
run_name=“mLateOn-medical”,
)
A couple of of those deserve a remark:
prompts: coaching doesn’t robotically apply the prompts saved within the mannequin, so map them onto your coaching columns explicitly. Right here that’s the checkpoint’s [Q] marker for the query column and [D] for the passage column, holding coaching in step with inference.
max_length (intentionally not set): this argument caps tokenization throughout coaching solely, for while you need cheaper coaching than the mannequin’s full serving size. I measured what that shortcut prices on this information. Coaching at 512 tokens misplaced about 0.015 NDCG@10 for about 2x the pace, and the deficit didn’t shrink with extra information, as a result of the mannequin merely by no means sees what acquired reduce off. Depart it unset so coaching matches inference, except you want the speedup greater than the standard.
learning_rate=1e-4: after a sweep from 5e-6 to 2e-4, I had one of the best luck with this higher-than-usual studying charge.
Evaluator
To trace your mannequin’s efficiency throughout coaching, you may cross an eval_dataset to the coach for analysis loss, however concrete retrieval metrics are far more informative. Sentence Transformers contains the next built-in evaluators for multi-vector fashions:
For area finetuning, the MultiVectorInformationRetrievalEvaluator constructed from your individual held-out information is the one which issues. One tip on developing it’s that the corpus needs to be laborious sufficient that fashions may be informed aside. In my case the MIRIAD questions are generated from their very own supply passages, which makes retrieval unusually simple. In opposition to simply the 10k gold passages, almost each mannequin scored above 0.97 NDCG@10. In case your analysis saturates like that, add distractor passages (I take advantage of deduplicated passages from the coaching break up) till the scores unfold out:
from datasets import load_dataset
from sentence_transformers.multi_vector_encoder.analysis import MultiVectorInformationRetrievalEvaluator
dataset = load_dataset(“tomaarsen/miriad-4.4M-split”)
corpus = {}
queries = {}
relevant_docs = {}
passage_to_id = {}
for idx, row in enumerate(dataset[“eval”]):
if row[“passage_text”] not in passage_to_id:
passage_to_id[row[“passage_text”]] = f”p{len(passage_to_id)}“
corpus[passage_to_id[row[“passage_text”]]] = row[“passage_text”]
if idx < 1_000:
queries[f”q{idx}“] = row[“question”]
relevant_docs[f”q{idx}“] = {passage_to_id[row[“passage_text”]]}
seen = set(passage_to_id)
for row in dataset[“train”]:
if len(corpus) >= 200_000:
break
if row[“passage_text”] not in seen:
seen.add(row[“passage_text”])
corpus[f”d{len(corpus)}“] = row[“passage_text”]
evaluator = MultiVectorInformationRetrievalEvaluator(
queries=queries,
corpus=corpus,
relevant_docs=relevant_docs,
title=“miriad-dev”,
batch_size=16,
)
Coach
The MultiVectorEncoderTrainer is the place all earlier parts come collectively. Right here is the whole script that skilled multi-vector-encoder/mLateOn-medical, the mannequin from the introduction:
import logging
import string
import traceback
from datasets import load_dataset
from sentence_transformers import (
MultiVectorEncoder,
MultiVectorEncoderModelCardData,
MultiVectorEncoderTrainer,
MultiVectorEncoderTrainingArguments,
)
from sentence_transformers.base.sampler import BatchSamplers
from sentence_transformers.multi_vector_encoder.analysis import MultiVectorInformationRetrievalEvaluator
from sentence_transformers.multi_vector_encoder.losses import CachedMultiVectorMultipleNegativesRankingLoss
logging.basicConfig(format=“%(asctime)s – %(message)s”, datefmt=“%Y-%m-%d %H:%M:%S”, degree=logging.INFO)
def most important():
mannequin = MultiVectorEncoder(
“lightonai/mLateOn-unsupervised”,
model_kwargs={“torch_dtype”: “float32”},
processor_kwargs={“model_max_length”: 8192},
model_card_data=MultiVectorEncoderModelCardData(
language=“en”,
license=“apache-2.0”,
model_name=“mLateOn finetuned on MIRIAD medical retrieval”,
),
)
mannequin[0].query_length = None
mannequin[0].document_length = None
mannequin[2].skiplist_words = record(string.punctuation)
mannequin[2].resolve_with_tokenizer(mannequin.tokenizer)
train_dataset = load_dataset(“tomaarsen/miriad-4.4M-split”, break up=“practice”).choose(vary(1_000_000))
loss = CachedMultiVectorMultipleNegativesRankingLoss(mannequin=mannequin, mini_batch_size=16)
eval_split = load_dataset(“tomaarsen/miriad-4.4M-split”, break up=“eval”)
corpus, queries, relevant_docs, passage_to_id = {}, {}, {}, {}
for idx, row in enumerate(eval_split):
if row[“passage_text”] not in passage_to_id:
passage_to_id[row[“passage_text”]] = f”p{len(passage_to_id)}“
corpus[passage_to_id[row[“passage_text”]]] = row[“passage_text”]
if idx < 500:
queries[f”q{idx}“] = row[“question”]
relevant_docs[f”q{idx}“] = {passage_to_id[row[“passage_text”]]}
dev_evaluator = MultiVectorInformationRetrievalEvaluator(
queries=queries, corpus=corpus, relevant_docs=relevant_docs, title=“miriad-dev”, batch_size=16
)
run_name = “mLateOn-medical”
args = MultiVectorEncoderTrainingArguments(
output_dir=f”fashions/{run_name}“,
num_train_epochs=1,
per_device_train_batch_size=128,
per_device_eval_batch_size=16,
learning_rate=1e-4,
warmup_steps=0.05,
prompts={“query”: “[Q] “, “passage_text”: “[D] “},
fp16=False,
bf16=True,
batch_sampler=BatchSamplers.NO_DUPLICATES,
eval_strategy=“steps”,
eval_steps=0.1,
save_strategy=“steps”,
save_steps=0.05,
logging_steps=0.01,
run_name=run_name,
)
coach = MultiVectorEncoderTrainer(
mannequin=mannequin,
args=args,
train_dataset=train_dataset,
loss=loss,
evaluator=dev_evaluator,
)
coach.practice()
mannequin.save_pretrained(f”fashions/{run_name}/closing”)
attempt:
mannequin.push_to_hub(run_name)
besides Exception:
logging.error(f”Error importing mannequin to the Hugging Face Hub:n{traceback.format_exc()}“)
if __name__ == “__main__”:
most important()
That is the entire recipe: a pre-supervised checkpoint, one million area pairs, in-batch negatives, full doc size, and a higher-than-usual studying charge. The run took 14.5 hours on my single RTX 3090 at a peak of 17.5 GB VRAM, and each a kind of decisions was the winner of a measured comparability reasonably than a guess.
For readers on smaller budgets, my scaling experiments put 100k pairs (75 minutes of coaching) inside 0.012 NDCG@10 of the total million-pair run. Many of the acquire comes within the first hour.
Callbacks
The MultiVectorEncoder coach helps numerous transformers.TrainerCallback subclasses, together with:
WandbCallback for logging coaching metrics to W&B if wandb is put in
TensorBoardCallback for logging coaching metrics to TensorBoard if tensorboard is accessible
CodeCarbonCallback for monitoring carbon emissions throughout coaching if codecarbon is put in
Allow these through the report_to coaching argument, e.g. report_to=[“wandb”, “codecarbon”], with the required dependencies put in. It defaults to “none”, and report_to=”all” prompts each integration whose dependency is put in.
Check with the Transformers Callbacks documentation for extra info on these callbacks and the best way to create your individual.
Multi-Dataset Coaching
Usually, top-performing general-purpose fashions are skilled on a number of datasets concurrently. Nevertheless, this strategy may be difficult because of the various codecs of every dataset. Happily, the MultiVectorEncoderTrainer lets you practice on a number of datasets with out requiring a uniform format. Moreover, it offers the pliability to use completely different loss features to every dataset. Listed below are the steps to coach with a number of datasets without delay:
Use a dictionary of datasets.Dataset cases (or a datasets.DatasetDict) because the train_dataset (and optionally additionally eval_dataset).
(Elective) Use a dictionary of loss features mapping dataset names to losses. Solely required in the event you want to use completely different loss features for various datasets.
Every coaching/analysis batch will solely comprise samples from one of many datasets. The order wherein batches are sampled from the a number of datasets is outlined by the MultiDatasetBatchSamplers enum, which may be handed to the MultiVectorEncoderTrainingArguments through multi_dataset_batch_sampler. Legitimate choices are:
MultiDatasetBatchSamplers.ROUND_ROBIN: Spherical-robin sampling from every dataset till one is exhausted. With this technique, it is possible that not all samples from every dataset are used, however every dataset is sampled from equally.
MultiDatasetBatchSamplers.PROPORTIONAL (default): Pattern from every dataset in proportion to its dimension. With this technique, all samples from every dataset are used and bigger datasets are sampled from extra continuously.
Analysis
To seek out out the place the finetuned mannequin stands, I evaluated it in opposition to over 50 retrieval mannequin configurations throughout 4 structure households on the MIRIAD analysis set, constructed precisely as within the Evaluator part above, with 1,000 held-out medical questions looking out 200,000 distinctive passages (the 10k gold passages hidden amongst 190k deduplicated distractors from the coaching break up). This corpus is 4 occasions the scale of the 50,000-passage one from Which start line do you have to decide?, so scores usually are not comparable between the 2 tables.

The headline outcomes, with the total desk within the collapsible beneath:
The finetuned mannequin tops the desk, beating the strongest zero-shot mannequin of any structure by +0.062 NDCG@10. In different phrases, the strongest zero-shot mannequin returns the best passage because the very first hit for 75.8% of the queries, whereas the finetuned mannequin does so for 84.9%, chopping the rank-1 error by greater than a 3rd.
The structure sample is simply as clear, with the highest of the desk solely late interplay. On lengthy paperwork, one vector per token beats one vector per doc, even at matched coaching and matched backbones. DenseOn and LateOn share coaching information and structure apart from the pinnacle, and the late-interaction sibling wins by +0.12, with the multilingual pair (mDenseOn and mLateOn) replicating this at +0.13. Scale would not rescue single vectors both. Qwen3-Embedding-4B, the strongest dense mannequin with roughly 33x the lively (non-embedding) parameters of mine, nonetheless stops 0.13 quick, and the 8B model scores decrease than the 4B.
BM25 additionally performs surprisingly properly, beating each sparse mannequin, each truncation-capped multi-vector mannequin, and all however three dense fashions: the multi-billion Qwen3-Embedding-4B and 8B, and voyage-4-nano, which reads its full 32k token context to edge previous by simply 0.006. Do not count on that to switch to your individual information although. MIRIAD’s questions are generated from the passages, so the lexical overlap between a question and its gold passage is much bigger than in typical retrieval, and BM25’s limitless context size lets it use each a kind of overlapping phrases whereas most neural checkpoints truncate. A BM25 baseline is affordable and at all times value operating, simply do not depend on this margin.
The complete area at a look, sorted by rating and coloured by structure household.

Click on to see the total analysis desk
Fashions marked @N are evaluated with their doc size cap lifted to N tokens, since their native caps (180 to 512 tokens) would in any other case truncate the 941-token common passages. For each multi-vector mannequin this carry was value +0.08 to +0.24 NDCG@10 over the as-served row, and even the dense DenseOn gained +0.03 from the identical remedy.
Notice that this doesn’t imply that multi-vector-encoder/mLateOn-medical is the strongest mannequin on all domains. It is merely the strongest in my area. That is completely wonderful, as I simply want this mannequin to work properly on my information.
Do not underestimate the ability of finetuning multi-vector fashions in your area. Fourteen and a half hours on a single shopper GPU produced a mannequin that no general-purpose retriever comes near on this information, and the recipe is a single script with no trainer mannequin and no mined negatives!
Optimizing the index
The truthful objection to multi-vector retrieval is index dimension, and this area is near the worst case for it. Storing one vector per token, my mannequin wants about 878 vectors per passage, so the 200,000-passage corpus takes roughly 45 GB at fp16, the place a dense mannequin wants properly underneath 1 GB. Doc size is what makes that hole so vast. The Pure Questions passages within the companion publish common about 125 token vectors every, seven occasions fewer, so a corpus of quick passages begins from a much smaller index than this one does. The HierarchicalTokenPooling module compresses precisely this by clustering every doc’s token embeddings and storing the cluster means, holding roughly 1 / pool_factor of the vectors:
from sentence_transformers.multi_vector_encoder.modules import HierarchicalTokenPooling
pooling = HierarchicalTokenPooling(pool_factor=4)
document_embeddings = mannequin.encode_document(passages, token_pooling=pooling)
I measured it post-hoc on the completed mannequin, with no pooling-aware coaching, and on lengthy paperwork it’s remarkably low cost.

The stable factors are uncompressed embeddings, so that each household is counted the identical manner and scored with actual search. You wouldn’t deploy any of them like that, although. Dense indexes routinely use int8 or binary quantization with rescoring, sparse indexes compress their postings, and multi-vector indexes use PLAID-style residual compression. Do not learn these factors because the disk you could purchase, however as relative storage value.
Token pooling is the stable line. Halving the vector depend prices 0.0033 NDCG@10 and leaves rank-1 accuracy untouched, and holding solely 1 / 4 of them, at 11.2 GB, nonetheless scores 0.8991. The curve retains going (I measured out to a tenth of the vectors, nonetheless at 0.8765) however there may be little cause to push pooling that far as soon as quantization is on the desk, which is what the dashed line beneath is about.
The dashed line is what an actual deployment would possibly appear to be. I gave Omar Khattab early entry to the mannequin and the benchmark, and he measured these configurations with fast-plaid at 1-bit residual quantization, utilizing compact 17-bit centroid ids and 18-bit doc ids as an alternative of its extraordinary unpacked 64-bit integers, plus document-side pruning:
configuration
vectors saved
index
NDCG@10
1-bit PLAID, all vectors
100%
3.37 GB
0.8984
1-bit PLAID + pruning
65%
2.23 GB
0.8830
1-bit PLAID + pruning
42%
1.45 GB
0.8642
That first row is 13x smaller than the uncooked embeddings, for 0.0155 NDCG@10. That could be a much better commerce than wherever on the pooling curve. Quantization shrinks every vector whereas pooling and pruning reduce what number of you retain, in order that they compose, and quantization is the one to succeed in for first. Push additional and the final row lands at 1.45 GB, smaller than the fp16 embeddings of Qwen3-Embedding-8B (1.64 GB), whereas scoring 0.0895 greater. The objection that multi-vector indexes are too massive doesn’t survive a correctly configured index.
The pruning right here is naive, meant solely to ascertain that token discount works on high of quantization, so learn the underside two rows as a flooring reasonably than the frontier. In the event you would reasonably not hand-tune quantization in any respect, the Indexing part of the companion publish covers fast-plaid, Qdrant, Weaviate, and Vespa.
Multi-vector retrieval is barely as costly as its index. The uncooked embeddings for this corpus are 45 GB, and a correctly configured index is at the least 7x smaller at almost the identical accuracy. The index deserves as a lot of your consideration because the checkpoint.
Acknowledgements
Because of Omar Khattab for measuring the quantized and pruned index configurations in Optimizing the index, and for the discussions round late-interaction index prices.
Further Assets
Coaching Examples
These pages have coaching examples with explanations in addition to hyperlinks to coaching scripts. You should use them to get acquainted with the multi-vector coaching loop:
MIRIAD: domain-specific coaching on medical retrieval, an earlier and easier cousin of this blogpost’s recipe
MS MARCO: contrastive and information distillation recipes
Multimodal: ColPali-style visible doc retrieval coaching
PEFT Adapters: parameter-efficient finetuning with LoRA
Documentation
For additional studying, you may additionally wish to discover the next assets on Sentence Transformers:
And right here is a complicated web page that may curiosity you:
And the companion blogpost, overlaying every thing about utilizing these fashions:
