Hugging Face · 官方博客

推出 Ettin Reranker 系列

Introducing the Ettin Reranker Family

二〇二六年五月十九日 · 英文原文

Tom Aarsen 发布了六个基于 Ettin ModernBERT 编码器的 Sentence Transformers CrossEncoder 重排序模型(17M 至 1B 参数),在各自参数量级上达到 SOTA。模型采用 pointwise MSE 蒸馏方案,以 mixedbread-ai/mxbai-rerank-large-v2 为教师,在约 143M 个 (query, document, score) 三元组上训练。在 MTEB(eng, v2) Retrieval 上,1B 模型 NDCG@10 达 0.6114,接近教师模型(0.6115);150M 模型为 600M 以下最强(0.5994)。配合 Flash Attention 2 与 unpadded 输入,17M 模型吞吐量达 7517 对/秒。所有模型及训练数据均以 Apache 2.0 许可证发布。

](https://huggingface.co/tomaarsen)

TL;DR

今天,我发布了六款新的 Sentence Transformers CrossEncoder 重排序模型(reranker),它们在各自参数量级上均达到 SOTA 水平。这些模型基于 Ettin ModernBERT 编码器构建,同时公开了训练所用的数据及完整的训练方案:

这些模型采用蒸馏方案训练:在 cross-encoder/ettin-reranker-v1-data 上,以 mixedbread-ai/mxbai-rerank-large-v2 的得分为目标,进行 pointwise MSE 蒸馏。该数据集是 lightonai/embeddings-pre-training 的一个子集,并与 lightonai/embeddings-fine-tuning 的一个重排序子集混合而成。

Image 2: MTEB(eng, v2) Retrieval with embeddinggemma-300m + reranker

我们的六款重排序模型与 google/embeddinggemma-300m 在 MTEB(eng, v2) Retrieval 上的表现。更多嵌入模型配对结果请见 结果 部分。

如果你刚接触重排序模型,想先了解“为什么”,请跳转到 什么是重排序模型,为什么要与嵌入模型搭配使用?。如果你只想直接使用模型,请跳转到 使用。如果你想自己训练,请跳转到 训练

我使用 Sentence Transformers v5.5.0 中附带的新 train-sentence-transformers Agent Skill 引导了下面的训练方案。通过 hf skills add train-sentence-transformers [--global] [--claude] 安装,然后让你的 AI 编码 agent(Claude Code、Codex、Cursor、Gemini CLI 等)在你的数据上微调 SentenceTransformerCrossEncoderSparseEncoder 模型。

目录

什么是重排序模型,为什么要与嵌入模型搭配使用?

重排序模型(reranker,又称 pointwise cross-encoder)是一种神经网络模型,它接收一个 (query, document) 对,并输出一个单一的相关性分数。与嵌入模型(embedding model)不同——嵌入模型会分别对查询和文档进行编码,然后从两个嵌入向量计算它们的相似度——重排序模型允许两个文本在每个 Transformer 层中相互关注。这种联合编码更准确,但代价也更高:模型必须为每个 (query, document) 对运行一次,而不是为每个文本运行一次。

由于 cross-encoder 在整个语料库上运行成本过高,常见的生产模式是检索-重排序:一个快速的嵌入模型检索出 top-K 候选(廉价),然后一个 cross-encoder 仅对这些 K 个候选进行高精度重排序。这样总成本可控,而最终排序结果更接近穷举式 cross-encoder 处理的结果。

Image 3: Embedding vs Reranker Models

在本博文中,我将互换使用“reranker”和“cross-encoder”。

使用

发布的模型是标准的 Sentence Transformers CrossEncoder 模型,因此只需 3 行代码即可使用:

from sentence_transformers import CrossEncoder

model = CrossEncoder("cross-encoder/ettin-reranker-32m-v1")
scores = model.predict([
    ("Where was Apple founded?", "Apple Inc. was founded in Cupertino, California in 1976 by Steve Jobs, Steve Wozniak, and Ronald Wayne."),
    ("Where was Apple founded?", "The Fuji apple is an apple cultivar developed in the late 1930s and brought to market in 1962."),
])
print(scores)
# [11.393298  2.968891]   <- 值越大表示越相关

对于查询和候选列表,你也可以使用 rank 来获取排序后的索引和分数:

ranked = model.rank(
    query="Which planet is known as the Red Planet?",
    documents=[
        "Venus is often called Earth's twin because of its similar size and proximity.",
        "Mars, known for its reddish appearance, is often referred to as the Red Planet.",
        "Jupiter, the largest planet in our solar system, has a prominent red spot.",
        "Saturn, famous for its rings, is sometimes mistaken for the Red Planet.",
    ],
    top_k=4,
    return_documents=True,
)
for r in ranked:
    print(f"({r['score']:.2f}): {r['text']}")
# (10.82): Mars, known for its reddish appearance, is often referred to as the Red Planet.
# (9.86): Saturn, famous for its rings, is sometimes mistaken for the Red Planet.
# (8.55): Jupiter, the largest planet in our solar system, has a prominent red spot.
# (6.21): Venus is often called Earth's twin because of its similar size and proximity.

你可以将 cross-encoder/ettin-reranker-32m-v1 替换为任何其他尺寸,以在质量与速度之间进行权衡。得益于 ModernBERT 的长上下文预训练,所有六个模型都支持最多 8K token 的上下文(适用于长文档重排序)。

建议安装 kernels 并设置 model_kwargs={"dtype": "bfloat16", "attn_implementation": "flash_attention_2"} 以获得最高吞吐量。更多详情请参见下面的 速度 部分,但一般来说,根据模型大小和序列长度,你可以预期比默认加载方式快 1.7 倍到 8.3 倍。

from sentence_transformers import CrossEncoder

model = CrossEncoder(
    "cross-encoder/ettin-reranker-32m-v1",
    model_kwargs={"dtype": "bfloat16", "attn_implementation": "flash_attention_2"},
)

端到端检索-重排序流水线

一个完整的示例,使用快速嵌入模型进行检索,并使用重排序模型进行最终排序:

from sentence_transformers import SentenceTransformer, CrossEncoder

# 使用静态嵌入模型进行快速检索(CPU 上每个查询亚毫秒级)
embedder = SentenceTransformer("sentence-transformers/static-retrieval-mrl-en-v1")
reranker = CrossEncoder("cross-encoder/ettin-reranker-68m-v1")

corpus = [
    "Apple Inc. was founded in Cupertino, California in 1976 by Steve Jobs, Steve Wozniak, and Ronald Wayne.",
    "The Fuji apple is an apple cultivar developed in the late 1930s.",
    "Steve Jobs introduced the iPhone in 2007 at Macworld.",
    "Macintosh computers were sold by Apple from 1984 onward.",
    # ... 生产环境中可能有数千或数百万条
]
query = "Where was Apple founded?"

# 步骤 1:编码 + 检索 top-100
query_emb = embedder.encode_query(query, convert_to_tensor=True)
corpus_emb = embedder.encode_document(corpus, convert_to_tensor=True)
scores = embedder.similarity(query_emb, corpus_emb)[0]
top_k_idx = scores.topk(min(100, len(corpus))).indices.tolist()

# 步骤 2:重排序
top_k_docs = [corpus[i] for i in top_k_idx]
ranked = reranker.rank(query, top_k_docs, top_k=5, return_documents=True)
for r in ranked:
    print(f"({r['score']:.2f}): {r['text']}")
# (11.63): Apple Inc. was founded in Cupertino, California in 1976 by Steve Jobs, Steve Wozniak, and Ronald Wayne.
# (4.71): Steve Jobs introduced the iPhone in 2007 at Macworld.
# (1.96): The Fuji apple is an apple cultivar developed in the late 1930s.
# (1.49): Macintosh computers were sold by Apple from 1984 onward.

这与大多数现代搜索系统使用的模式相同。检索器决定哪些内容进入漏斗,重排序模型决定最终胜出者。

架构细节

所有六个重排序模型共享相同的架构,仅在骨干网络大小上有所不同。骨干网络是约翰霍普金斯大学 Ettin 套件中的六个 Ettin 编码器 之一。这些是 ModernBERT 风格的模型,具有 unpadded attention、RoPE 位置编码、GeGLU 和 2T token 的开放许可预训练数据,支持最多 8192 个 token 的上下文。

在每个编码器之上,重排序模型使用一个 4 模块的分类头,该分类头模仿 ModernBertForSequenceClassification,但由 Sentence Transformers 的模块化组件构建。底层的 Transformer 是一个普通的 AutoModel,而不是 AutoModelForSequenceClassification,这使我们能够对可变长度输入使用序列 unpadding,以支持 Flash Attention 2。在中等文档序列长度下,这比 fp32+SDPA 快 1.7 倍到 8.3 倍,具体取决于模型大小(完整基准测试请参见 速度):

1. Transformer(FA2)
2. Pooling(cls)
3. Dense(H, H, bias=False, GELU)
4. LayerNorm(H)
5. Dense(H, 1, scores)

在我的消融实验中,CLS pooling 优于 mean pooling。这有点令人惊讶。ModernBERT 仅每三层使用一次全局 attention,其余三分之二使用局部窗口 attention,无法从远处位置到达 CLS。经验上,那少数几个全局层携带了足够的信号,使 CLS 成为更好的 pooling 选择。

模型 骨干网络 隐藏层大小 层数 参数量(含分类头)
cross-encoder/ettin-reranker-17m-v1 jhu-clsp/ettin-encoder-17m 256 7 17.6M
cross-encoder/ettin-reranker-32m-v1 jhu-clsp/ettin-encoder-32m 384 10 32.8M
cross-encoder/ettin-reranker-68m-v1 jhu-clsp/ettin-encoder-68m 512 19 68.6M
cross-encoder/ettin-reranker-150m-v1 jhu-clsp/ettin-encoder-150m 768 22 150.9M
cross-encoder/ettin-reranker-400m-v1 jhu-clsp/ettin-encoder-400m 1024 28 401.6M
cross-encoder/ettin-reranker-1b-v1 jhu-clsp/ettin-encoder-1b 1792 28 1.00B

所有六个模型均以 Apache 2.0 许可证发布,与 Ettin 编码器一致。

结果

MTEB(eng, v2) Retrieval

我使用 MTEB 的 两阶段重排序流程,将每个发布的模型在完整的 MTEB(eng, v2) Retrieval 基准测试(10 个任务,top-100 重排序)上进行了评估,并将每个重排序模型与六个覆盖速度/质量谱系的嵌入模型配对:

嵌入模型 激活参数量 仅检索器 NDCG@10
sentence-transformers/static-retrieval-mrl-en-v1 0M 0.3495
sentence-transformers/all-MiniLM-L6-v2 23M 0.4292
BAAI/bge-small-en-v1.5 33M 0.5149
nomic-ai/nomic-embed-text-v1.5 137M 0.5226
google/embeddinggemma-300m 308M 0.5463
jinaai/jina-embeddings-v5-text-small-retrieval 596M 0.5980

下面每个图表中的虚线仅检索器基线是需要超越的关键数字。低于该线意味着重排序模型平均而言会损害流水线性能:

Image 4: MTEB(eng, v2) Retrieval with static-retrieval-mrl-en-v1 + reranker Image 5: MTEB(eng, v2) Retrieval with all-MiniLM-L6-v2 + reranker
Image 6: MTEB(eng, v2) Retrieval with bge-small-en-v1.5 + reranker Image 7: MTEB(eng, v2) Retrieval with nomic-embed-text-v1.5 + reranker
Image 8: MTEB(eng, v2) Retrieval with embeddinggemma-300m + reranker Image 9: MTEB(eng, v2) Retrieval with jina-embeddings-v5-text-small-retrieval + reranker

完整结果表格(点击展开) 6 个嵌入模型配对的平均 NDCG@10,降序排列。我们的六个模型以粗体显示,教师模型 mixedbread-ai/mxbai-rerank-large-v2 带下划线。

重排序模型 参数量 MTEB(eng, v2) Retrieval NDCG@10
Qwen/Qwen3-Reranker-4B 4.02B 0.6367
mixedbread-ai/mxbai-rerank-large-v2 1.54B 0.6115
cross-encoder/ettin-reranker-1b-v1 1.00B 0.6114
cross-encoder/ettin-reranker-400m-v1 401M 0.6091
cross-encoder/ettin-reranker-150m-v1 151M 0.5994
Qwen/Qwen3-Reranker-0.6B 596M 0.5940
mixedbread-ai/mxbai-rerank-base-v2 494M 0.5920
cross-encoder/ettin-reranker-68m-v1 68.6M 0.5915
jinaai/jina-reranker-m0 2.44B 0.5856
Alibaba-NLP/gte-reranker-modernbert-base 150M 0.5843
cross-encoder/ettin-reranker-32m-v1 32.8M 0.5779
ibm-granite/granite-embedding-reranker-english-r2 150M 0.5656
cross-encoder/ettin-reranker-17m-v1 17.6M 0.5576
BAAI/bge-reranker-v2-m3 568M 0.5526
zeroentropy/zerank-2-reranker 4.02B 0.5300
BAAI/bge-reranker-large 560M 0.5098
cross-encoder/ms-marco-MiniLM-L6-v2 22.7M 0.5082
cross-encoder/ms-marco-MiniLM-L12-v2 33.4M 0.5066
mixedbread-ai/mxbai-rerank-large-v1 435M 0.5063
cross-encoder/ms-marco-MiniLM-L4-v2 19.2M 0.4979
mixedbread-ai/mxbai-rerank-xsmall-v1 70.8M 0.4968
BAAI/bge-reranker-base 278M 0.4890
mixedbread-ai/mxbai-rerank-base-v1 184M 0.4863

† 限制为 max_seq_length=8192(基于 4B Qwen3 的重排序模型在原生上下文下无法装入单个 H100 80GB)。原生上下文评估分数可能更高。

完整 NanoBEIR 结果表格(点击展开) NanoBEIRBEIR 的一个快速 13 数据集子集,每个数据集使用 50 个查询,每个查询最多对应 5000 个文档。NanoBEIR 是训练期间 metric_for_best_model 的设置目标(参见 评估),也是我用来指导实验的指标。

重排序模型 参数量 NanoBEIR 平均 NDCG@10
mixedbread-ai/mxbai-rerank-large-v2 1.54B 0.7318
cross-encoder/ettin-reranker-1b-v1 1.00B 0.7237
jinaai/jina-reranker-m0 2.44B 0.7197
cross-encoder/ettin-reranker-400m-v1 401M 0.7193
mixedbread-ai/mxbai-rerank-base-v2 494M 0.7162
cross-encoder/ettin-reranker-150m-v1 151M 0.7086
Alibaba-NLP/gte-reranker-modernbert-base 150M 0.7017
BAAI/bge-reranker-v2-m3 568M 0.6971
cross-encoder/ettin-reranker-68m-v1 68.6M 0.6915
ibm-granite/granite-embedding-reranker-english-r2 150M 0.6909
cross-encoder/ettin-reranker-32m-v1 32.8M 0.6825
cross-encoder/ettin-reranker-17m-v1 17.6M 0.6746
mixedbread-ai/mxbai-rerank-large-v1 435M 0.6488
BAAI/bge-reranker-large 560M 0.6379
cross-encoder/ms-marco-MiniLM-L12-v2 33.4M 0.6369
cross-encoder/ms-marco-MiniLM-L6-v2 22.7M 0.6312
cross-encoder/ms-marco-MiniLM-L4-v2 19.2M 0.6298
mixedbread-ai/mxbai-rerank-base-v1 184M 0.6231
mixedbread-ai/mxbai-rerank-xsmall-v1 70.8M 0.6136
BAAI/bge-reranker-base 278M 0.6027

我发布的最小模型,我们的 17M,在 MTEB 上以大约一半的参数量,比 33M 的 ms-marco-MiniLM-L12-v2 高出 +0.051 NDCG@10(0.5576 vs 0.5066),在 NanoBEIR 上高出 +0.038(0.6746 vs 0.6369)。32M 在 MTEB 上以 17 倍的参数量差距,比 568M 的 BAAI/bge-reranker-v2-m3 高出 +0.025(0.5779 vs 0.5526)。如果你一直在检索-重排序栈中使用传统的 MiniLM 重排序模型作为默认选项,那么换用我们的 17M(或 32M)是一个低风险的直接替换,在两个基准测试上都能带来明显的质量提升。

向上看表格,我们的 150M 是我在 MTEB 上测试的 600M 以下范围内最强的重排序模型,以 +0.005(0.5994 vs 0.5940)的优势略胜于最近的 Qwen/Qwen3-Reranker-0.6B(596M),并比所有 BAAI bge-reranker 变体高出 0.03 到 0.05。68M 也值得一提:其 0.5915 的分数几乎与 Qwen3-Reranker-0.6B(0.5940)持平,而参数量仅为后者的九分之一。

在发布范围的顶端,我们的 1B 模型紧密跟随其教师模型。它在 MTEB 上与 1.54B 的 mxbai-rerank-large-v2 相差仅 0.0001(0.6114 vs 0.6115),在 NanoBEIR 上相差 0.008,尽管它是从一个比自身大 54% 的模型中蒸馏出来的。蒸馏有效地缩小了与教师模型的差距,这正是我在发布前希望看到的结果。

比较中整体最强的重排序模型是 Qwen/Qwen3-Reranker-4B,MTEB 得分为 0.6367,比我们的 1B 模型高出 +0.025。要使用当前方案缩小这一差距,可能需要从一个更强的教师模型(我们的教师模型本身低于 Qwen3-Reranker-4B)进行蒸馏。对于大多数检索-重排序工作负载,我们的 1B 模型以其四分之一的参数量(参见 速度)是更实用的选择。

速度

对于重排序模型,质量数据只是其重要性的一半。另一半是它的延迟是否适合你在检索和向用户展示结果之间的预算。让我带你看看我测量的结果。

我在单个 NVIDIA H100 80GB 上,对我发布的全部六个模型与十三个公开重排序模型(强基线,参数量最高约 1B)进行了基准测试。查询和文档来自 sentence-transformers/natural-questions,采用其自然的文档长度分布:大多数 NQ 答案很短,有些很长。文档被截断为 max_length=512,以避免给较旧的模型带来不公平的优势。每个模型使用其最佳支持的 attention 实现:架构支持的地方使用 Flash Attention 2(BERT、XLM-RoBERTa、ModernBERT、Qwen2),不支持的地方使用 SDPA,DeBERTa-v2 使用 eager(目前在 transformers 中既不支持 FA2 也不支持 SDPA)。

对于每个模型,自动批处理搜索从批大小 8 开始,并加倍直到 GPU 内存耗尽。在每个批大小下,我运行三次计时并取中位数吞吐量,这样单次不幸运的运行不会影响数字。报告的吞吐量是获胜批大小下的值。

表 1. 吞吐量(对/秒),全部使用 bfloat16。我们的六个重排序模型以粗体显示。

模型 参数量 Attention 对/秒
cross-encoder/ettin-reranker-17m-v1 17M FA2 7517
cross-encoder/ettin-reranker-32m-v1 32M FA2 6602
cross-encoder/ettin-reranker-68m-v1 68M FA2 4913
cross-encoder/ms-marco-MiniLM-L4-v2 19M FA2 4029
cross-encoder/ms-marco-MiniLM-L6-v2 22M FA2 3817
cross-encoder/ms-marco-MiniLM-L12-v2 33M FA2 3311
cross-encoder/ettin-reranker-150m-v1 150M FA2 3237
BAAI/bge-reranker-base 278M FA2 2858
mixedbread-ai/mxbai-rerank-xsmall-v1 70M eager 2636
mixedbread-ai/mxbai-rerank-base-v1 184M eager 1953
cross-encoder/ettin-reranker-400m-v1 400M FA2 1738
BAAI/bge-reranker-large 560M FA2 1659
BAAI/bge-reranker-v2-m3 568M FA2 1569
Alibaba-NLP/gte-reranker-modernbert-base 150M FA2 1418
ibm-granite/granite-embedding-reranker-english-r2 150M FA2 1404
cross-encoder/ettin-reranker-1b-v1 1B FA2 928
mixedbread-ai/mxbai-rerank-large-v1 435M eager 867
mixedbread-ai/mxbai-rerank-base-v2 494M FA2 809
mixedbread-ai/mxbai-rerank-large-v2 1.5B FA2 387

我们的 17M 是整个比较中最快的重排序模型,达到 7517 对/秒。这几乎是 ms-marco-MiniLM-L6-v2(3817)的两倍吞吐量,甚至比更小的 ms-marco-MiniLM-L4-v2(4029)还要快。正如你在前面的 MTEB 表格中看到的,我们的 17M 也比所有 MiniLM 变体更准确。如果你当前正在运行 MiniLM cross-encoder,换用我们的 17M 只需一行代码更改,即可同时改善延迟和搜索质量。

我们的 150M 是一个更有趣的比较,因为恰好有两个架构对等的模型,参数量正好是 150M:Alibaba-NLP/gte-reranker-modernbert-baseibm-granite/granite-embedding-reranker-english-r2。两者都基于相同的 ModernBERT-base 骨干网络。我们的 150M 运行速度为 3237 对/秒,而两个对等模型分别为 1418 和 1404 对/秒,存在 2.3 倍的速度差距。

所有三个 150M 模型都使用 Flash Attention 2,但两个对等模型通过 AutoModelForSequenceClassification 加载,这保持了输入的填充状态。因此,attention 本身运行了 FA2 内核,但模型的其余部分仍在填充 token 上进行密集计算,而这些 token 没有任何贡献。我们的模块化 Transformer 模块(参见上面的 架构细节)将 unpadded 输入一直传播到整个模型,因此每一层只在实际 token 上花费计算。这就是获得 FA2 部分好处与获得全部好处之间的区别。

在表格底部,我们的 1B 模型达到 928 对/秒,比 1.54B 的教师模型 mxbai-rerank-large-v2(387 对/秒)快 2.4 倍,同时 MTEB 分数相差仅 0.0001。教师模型基于 Qwen2,每个对都有 prompt-template 开销,因此蒸馏后的学生模型继承了教师模型的校准和判断力,但跳过了所有运行时负担。老实说,这是整个发布中我最满意的一个数字。

一个不幸的说明:基于 DeBERTa-v2 的 mxbai-rerank-{xsmall,base,large}-v1 系列最终比表格中的其他模型慢得多,因为 DeBERTa-v2 目前在 transformers 中既不支持 Flash Attention 2 也不支持 SDPA。70M 的 mxbai-rerank-xsmall-v1 运行速度为 2636 对/秒,大约是参数量几乎相同的我们 68M 模型吞吐量的一半。这些模型本身完全没问题,只是无法使用现代 attention 内核。

在消费级 GPU(RTX 3090,24 GB)上的相同基准测试 如果你在消费级显卡而非数据中心 GPU 上自托管,以下是 RTX 3090 上的相同吞吐量扫描。基准测试设置与表 1 相同:bfloat16,每个模型使用最佳支持的 attention,三次试验中位数吞吐量,取能容纳的最大批大小。

模型 参数量 最佳 Attention 对/秒
cross-encoder/ettin-reranker-17m-v1 17M FA2 9008
cross-encoder/ms-marco-MiniLM-L4-v2 19M FA2 5071
cross-encoder/ettin-reranker-32m-v1 32M FA2 4497
cross-encoder/ms-marco-MiniLM-L6-v2 22M FA2 4234
cross-encoder/ms-marco-MiniLM-L12-v2 33M FA2 2847
cross-encoder/ettin-reranker-68m-v1 68M FA2 1916
mixedbread-ai/mxbai-rerank-xsmall-v1 70M eager 1677
BAAI/bge-reranker-base 278M FA2 1329
cross-encoder/ettin-reranker-150m-v1 150M FA2 982
mixedbread-ai/mxbai-rerank-base-v1 184M eager 772
ibm-granite/granite-embedding-reranker-english-r2 150M FA2 598
Alibaba-NLP/gte-reranker-modernbert-base 150M FA2 586
BAAI/bge-reranker-large 560M FA2 448
BAAI/bge-reranker-v2-m3 568M FA2 436
cross-encoder/ettin-reranker-400m-v1 400M FA2 429
mixedbread-ai/mxbai-rerank-large-v1 435M eager 266
mixedbread-ai/mxbai-rerank-base-v2 494M FA2 221
cross-encoder/ettin-reranker-1b-v1 1B FA2 189
mixedbread-ai/mxbai-rerank-large-v2 1.5B FA2 69

我们的 17M 仍然是表格中最快的模型,达到 9008 对/秒,实际上高于其在 H100 上的数字,这表明在极小尺寸下,原始计算不是瓶颈,H100 的额外算力无法体现。表格中间部分略有重新洗牌,MiniLM 重排序模型超过了我们的 32M 和 68M,而 1B 模型落后于 mxbai-rerank-base-v2(189 vs 221 对/秒)。我们的 150M 模型仍然对两个 150M 的 ModernBERT 对等模型保持明显领先,教师模型替代的故事仍然成立,我们的 1B 模型吞吐量是 1.5B mxbai-rerank-large-v2 的 2.7 倍(189 vs 69 对/秒)。

在 CPU(Intel Core i7-13700K)上的相同基准测试

模型 参数量 最佳 Attention 对/秒
cross-encoder/ettin-reranker-17m-v1 17M SDPA 267.4
cross-encoder/ms-marco-MiniLM-L4-v2 19M SDPA 206.2
cross-encoder/ms-marco-MiniLM-L6-v2 22M SDPA 143.9
cross-encoder/ettin-reranker-32m-v1 32M SDPA 92.5
cross-encoder/ms-marco-MiniLM-L12-v2 33M SDPA 75.9
mixedbread-ai/mxbai-rerank-xsmall-v1 70M eager 38.9
cross-encoder/ettin-reranker-68m-v1 68M SDPA 31.2
BAAI/bge-reranker-base 278M SDPA 19.2
Alibaba-NLP/gte-reranker-modernbert-base 150M SDPA 14.7
ibm-granite/granite-embedding-reranker-english-r2 150M SDPA 14.5
cross-encoder/ettin-reranker-150m-v1 150M SDPA 14.0
mixedbread-ai/mxbai-rerank-base-v1 184M eager 13.4
BAAI/bge-reranker-large 560M SDPA 6.2
BAAI/bge-reranker-v2-m3 568M SDPA 6.0
cross-encoder/ettin-reranker-400m-v1 400M SDPA 5.2
mixedbread-ai/mxbai-rerank-large-v1 435M eager 4.3
mixedbread-ai/mxbai-rerank-base-v2 494M SDPA 3.5
cross-encoder/ettin-reranker-1b-v1 1B SDPA 2.1

在 CPU 上,我们无法利用 bf16、Flash Attention 2 或 unpadding,因此延迟情况更简单:参数量越高,模型越慢。17M 模型明显快于 ms-marco-MiniLM-L6-v2(267.4 vs 143.9 对/秒),甚至快于更小的 ms-marco-MiniLM-L4-v2(206.2)。正如预期,我们的 150M 模型与两个 150M 对等模型处于同一水平(14.0 vs 14.5 和 14.7 对/秒),因为 unpadding 不再适用。如果你受限于 CPU,我们的 17M 和 32M 是实用的选择。

为了解释速度的来源,下表使用相同的基准配置,对我们的六个模型进行了 fp32+SDPAbf16+SDPAbf16+FA2 的扫描。FA2 列分为两部分:输入仍为 padded(包装模型会看到的情况)和输入为 unpadded(我们的模块化 Transformer 实际执行的情况)。最右列是我们的模型在启用 FA2 时默认使用的配置。

表 2. 六个发布尺寸在 max_length=512 下对自然 NQ 文档的精度和 attention 消融实验。每个单元格显示对/秒,括号内为相对于 fp32+SDPA 的倍数,第二行为峰值 GPU 内存。最右列(粗体)是我们的模型在启用 FA2 时默认使用的配置。

模型 参数量 fp32+SDPA bf16+SDPA bf16+FA2 w. padding bf16+FA2 w.o. padding
cross-encoder/ettin-reranker-17m-v1 17M 4402 (1.00x) 0.8 GB 4523 (1.03x) 2.2 GB 3744 (0.85x) 1.9 GB 7517 (1.71x) 1.4 GB
cross-encoder/ettin-reranker-32m-v1 32M 3307 (1.00x) 1.2 GB 4357 (1.32x) 1.6 GB 3040 (0.92x) 2.9 GB 6602 (2.00x) 1.1 GB
cross-encoder/ettin-reranker-68m-v1 68M 1364 (1.00x) 1.0 GB 2861 (2.10x) 2.2 GB 2003 (1.47x) 2.0 GB 4913 (3.60x) 1.5 GB
cross-encoder/ettin-reranker-150m-v1 150M 671 (1.00x) 1.6 GB 1942 (2.90x) 1.8 GB 1396 (2.08x) 3.1 GB 3237 (4.83x) 1.4 GB
cross-encoder/ettin-reranker-400m-v1 400M 266 (1.00x) 2.5 GB 1113 (4.18x) 1.8 GB 864 (3.25x) 2.7 GB 1738 (6.53x) 2.2 GB
cross-encoder/ettin-reranker-1b-v1 1B 112 (1.00x) 4.6 GB 630 (5.60x) 2.8 GB 522 (4.64x) 3.6 GB 928 (8.26x) 4.5 GB

bf16+FA2 w.o. padding 相对于 fp32+SDPA 基线的总加速比随模型大小急剧增长,从 17M 的 1.71 倍到 1B 的 8.26 倍。这种增长大部分来自 bf16 本身:从 fp32+SDPAbf16+SDPA 这一步,17M 仅获得 1.03 倍加速,而 1B 获得了完整的 5.60 倍加速,这也是由于降低的内存成本允许使用更大的批大小。简而言之,bfloat16 是整个加速的最大单一贡献者。

出乎意料的是,在输入仍为 padded 的情况下启用 FA2,实际上在所有发布尺寸上都比 bf16+SDPA 慢。FA2 内核更喜欢 unpadded 格式,当你向其提供 padded 输入时,你需要在格式之间转换的记账开销,同时仍然在填充 token 本身上花费计算。因此,bf16+FA2 w. padding 列大致相当于你在 model_kwargs 中将 sdpa 替换为 flash_attention_2 而不更改模型加载器其他任何内容时测量到的结果。这就是表 1 中 gte-reranker-modernbert-basegranite-embedding-reranker-english-r2 所处的情况。

最后,从 bf16+FA2 w. paddingbf16+FA2 w.o. padding 带来了 1.78 倍(1B)到 2.45 倍(68M)的额外吞吐量提升,并且还显著降低了峰值内存,从而允许更高的批大小。

因此,我的建议很简单:同时启用 bf16 和 FA2。六个 Ettin 重排序模型默认将使用 unpadded 输入,因为 架构细节 部分中的模块化 Transformer 模块就是为此设置的。完整代码片段与上面的 使用 部分相同:

from sentence_transformers import CrossEncoder

model = CrossEncoder(
    "cross-encoder/ettin-reranker-150m-v1",
    model_kwargs={
        "dtype": "bfloat16",
        "attn_implementation": "flash_attention_2",  // 参见下面的提示
    },
)

使用 pip install kernels 安装 FA2。它为广泛的 GPU 架构、CUDA 版本和操作系统提供了预构建的内核。

其他 CrossEncoder 的一个注意事项:完整的加速仅适用于使用模块化 Transformer 构建的模型,例如 Ettin 重排序模型。将相同的两个标志应用于通过 AutoModelForSequenceClassification 加载的 CrossEncoder,将导致你进入表 2 中较慢的 bf16+FA2 w. padding 列。

训练

下面的训练脚本最初是 Sentence Transformers v5.5.0 中附带的新 train-sentence-transformers Agent Skill 的输出。如果你使用 AI 编码 agent(Claude Code、Codex、Cursor、Gemini CLI 等),你可以安装该 skill 并要求它在你的数据上微调 SentenceTransformerCrossEncoderSparseEncoder 模型。该 skill 包含针对基础模型选择、损失和评估器选择、难负例挖掘、蒸馏、LoRA、Matryoshka、多语言训练和静态嵌入的版本感知指导,以及每种模型类型的模板脚本。

hf skills add train-sentence-transformers --claude   # 符号链接到 .claude/skills/
hf skills add train-sentence-transformers --global   # 在 ~/.agents/skills/ 下

"Fine-tune a cross-encoder reranker on (query, document) pairs from my dataset, mine hard negatives, and push to my Hub repo" 这样的提示将生成一个可运行的脚本,你可以在此基础上进行迭代。我就是这样开始研究下面的方案的。

所有六个重排序模型都使用相同的单阶段方案进行训练。只有学习率和每个设备的批大小因模型大小而异。完整的训练脚本大约 150 行,并使用一个已发布的数据集。

该方案在一次跨模型大小的扫描后收敛。每个大小的学习率通过在最终训练数据约 15% 的子集上进行小规模网格搜索来调整,得到的 LR 可以干净地迁移到全数据运行,无需重新调整。除了 LR 之外,不需要针对每个大小进行调整。

蒸馏方案

大多数已发布的重排序方案在人工标注的相关性三元组(一个查询、一个正文档,以及可选的难负例)上训练,使用对比、pointwise、pairwise 或 listwise 损失,例如 MultipleNegativesRankingLossBinaryCrossEntropyLossRankNetLossLambdaLoss。例如,请参见我之前的博文 Training and Finetuning Reranker Models with Sentence Transformers

但这种方法有一些实际和理论上的缺点。首先,正例需要人工标注,这在跨多个领域扩展时既昂贵又缓慢。其次,模型只看到有人处理过的那个小部分 (query, document) 对的标签。特别是在难负例挖掘之后,你最终会得到很多假负例,例如 Hard Negatives, Hard Lessons 所示。第三,这种标注的二元性质与现实不符,现实中有些文档只是比其他文档更相关。

我在这里走了一条不同的路线:从现有的强教师重排序模型进行 pointwise MSE 蒸馏。设置简单到可以用三行描述:

数据集

我已将训练数据作为一个单一的 Hugging Face 数据集 cross-encoder/ettin-reranker-v1-data 发布,该数据集由两个来源组成。每个来源都保留为自己的 split,以便来源透明:

  1. LightOn 预训练数据(lightonai/embeddings-pre-training,非策划):32 个 split,涵盖广泛领域的文本相似性信号(MTP、FW-EDU、Reddit、PAQ、S2ORC、Amazon、Wikipedia、MS MARCO 等)。我限制了某些 split 的样本数量,总共产生约 110M 个 (query, document, similarity) 三元组。
  2. 来自 lightonai/embeddings-fine-tuning 的重新评分检索数据:7 个 split(msmarcohotpotqatrivianqsquadv2fiqafever)。源数据集每个查询最多有 2048 个候选文档(最初使用 Alibaba-NLP/gte-modernbert-base 评分),我使用 mixedbread-ai/mxbai-rerank-large-v2 对其重新评分,并上传为 cross-encoder/lightonai-embeddings-fine-tuning-reranked-v1。该数据集使用 Jang et al. 的分位数锚点方案,将每个查询的 2048 个候选子采样到 256 个(所有正例 + top-16 难例 + 约 239 个分位数锚点分层样本)。对于训练,我从每个查询的 256 个候选中挑选 64 个:32 个来自分数排序的头部(正例加上最难的负例),32 个中等难度的负例,从教师模型排名中更靠后的一个区间采样。确切的排名位置请参见 数据集卡片

总计:约 143M 个 (query, document, score) 三元组,外加一个保留的 5K 行评估 split(quora 的尾部),用于驱动训练中的评估损失。

训练参数

大多数超参数在不同模型大小之间是恒定的:

CrossEncoderTrainingArguments(
    num_train_epochs=1,                    # 我选择更多数据而不是更多 epoch
    per_device_train_batch_size=...,       # global_batch_size // world_size(见下表)
    gradient_accumulation_steps=1,
    learning_rate=...,                     # 按大小调整,见下表
    warmup_ratio=0.03,                     # 约 3% 线性预热,然后线性衰减(默认)
    bf16=True,                             # 全程使用 FA2 + bf16
    eval_strategy="steps",
    eval_steps=0.05,                       # 每训练 5% 评估一次 NanoBEIR
    save_strategy="steps",
    save_steps=0.05,
    save_total_limit=5,
    load_best_model_at_end=True,
    metric_for_best_model="eval_NanoBEIR_R100_mean_ndcg@10",
    seed=12,
)

只有学习率和全局批大小因模型大小而异。

大小 学习率 全局批大小
17m 2.4e-4 1024
32m 1.2e-4 512
68m 3e-5 256
150m 1.5e-5 192
400m 7e-6 256
1b 3e-6 512

global_batch_sizeper_device_batch_size x world_size x gradient_accumulation_steps。在单个 8-GPU 节点上,17m 的 1024 全局批大小意味着 per_device=128。在 8 个节点上,意味着 per_device=8。训练脚本从 global_batch_size // world_size 计算 per_device_batch_size,因此同一脚本可以在任何节点数量下工作。全局批大小本可以更一致,但我发现上述值效果很好,并且不想仅仅为了追求一致性而重新调整它们。

评估

我在训练期间监控 NanoBEIR 平均 NDCG@10(每 5% 的步骤评估一次),并将其用作 load_best_model_at_endmetric_for_best_model。NanoBEIR 速度很快,因此我可以在每次训练运行中负担 20 次评估。训练后,我在完整的 MTEB(eng, v2) Retrieval 基准测试上评估了最佳检查点(根据 NanoBEIR)和最后一个检查点。最终发布的检查点是 MTEB 上表现最好的那个。对于除 68m 之外的所有大小,NanoBEIR 偏好的检查点获胜,而 68m 的最后一个检查点略强。

完整训练脚本

完整的脚本(每个发布模型都使用它进行训练)是一个单一文件。每次运行只需更改 ENCODER_SIZE,其他一切自动完成:

from __future__ import annotations

import logging
import os
from pathlib import Path

import torch
import torch.nn as nn
from datasets import concatenate_datasets, get_dataset_config_names, load_dataset

from sentence_transformers import CrossEncoder
from sentence_transformers.base.modules import Dense
from sentence_transformers.cross_encoder import (
    CrossEncoderModelCardData,
    CrossEncoderTrainer,
    CrossEncoderTrainingArguments,
)
from sentence_transformers.cross_encoder.evaluation import CrossEncoderNanoBEIREvaluator
from sentence_transformers.cross_encoder.losses import MSELoss
from sentence_transformers.sentence_transformer.modules import LayerNorm, Pooling, Transformer

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s", datefmt="%H:%M:%S")
logging.getLogger("httpx").setLevel(logging.WARNING)

# 按大小配置。我使用这些全局(有效)批大小扫描了学习率,
# 也结合了 accum_steps
CONFIGS: dict[str, dict] = {
    "17m":  {"base_model_name": "jhu-clsp/ettin-encoder-17m",  "learning_rate": 2.4e-4, "global_batch_size": 1024},
    "32m":  {"base_model_name": "jhu-clsp/ettin-encoder-32m",  "learning_rate": 1.2e-4, "global_batch_size": 512},
    "68m":  {"base_model_name": "jhu-clsp/ettin-encoder-68m",  "learning_rate": 3e-5,   "global_batch_size": 256},
    "150m": {"base_model_name": "jhu-clsp/ettin-encoder-150m", "learning_rate": 1.5e-5, "global_batch_size": 192},
    "400m": {"base_model_name": "jhu-clsp/ettin-encoder-400m", "learning_rate": 7e-6,   "global_batch_size": 256},
    "1b":   {"base_model_name": "jhu-clsp/ettin-encoder-1b",   "learning_rate": 3e-6,   "global_batch_size": 512},
}
ENCODER_SIZE = "17m"

def main() -> None:
    config = CONFIGS[ENCODER_SIZE]
    encoder_id = config["base_model_name"]
    learning_rate = config["learning_rate"]
    global_batch_size = config["global_batch_size"]

    world_size = int(os.environ.get("WORLD_SIZE", 1))
    per_device_batch_size = global_batch_size // world_size
    dataloader_workers = 0 if world_size > 8 else 4
    run_name = f"ettin-reranker-{ENCODER_SIZE}-lr{learning_rate:.0e}"

    # 1. 加载一个待微调的模型,附带模型卡片数据
    # 该模型模仿 ModernBertForSequenceClassification,但使用一个'无头'的 Transformer,仅加载
    # AutoModel。这允许与 FA2 一起使用 unpadding,而 AutoModelForSequenceClassification 无法实现。
    # 这大大加快了训练速度,同时大幅减少了内存使用。
    torch.manual_seed(12)
    transformer = Transformer(encoder_id, model_kwargs={"attn_implementation": "flash_attention_2"})
    transformer.model.config.num_labels = 1
    embedding_dimension = transformer.get_embedding_dimension()
    pooling = Pooling(embedding_dimension=embedding_dimension, pooling_mode="cls")
    dense_inner = Dense(
        in_features=embedding_dimension, out_features=embedding_dimension, bias=False,
        activation_function=nn.GELU(),
        module_input_name="sentence_embedding", module_output_name="sentence_embedding",
    )
    norm = LayerNorm(dimension=embedding_dimension)
    dense_score = Dense(
        in_features=embedding_dimension, out_features=1, bias=True,
        activation_function=nn.Identity(),
        module_input_name="sentence_embedding", module_output_name="scores",
    )
    model = CrossEncoder(
        modules=[transformer, pooling, dense_inner, norm, dense_score],
        num_labels=1,
        activation_fn=nn.Identity(),
        model_card_data=CrossEncoderModelCardData(
            model_name=f"Ettin Reranker {ENCODER_SIZE} distilled from mxbai-rerank-large-v2",
            language="en",
            license="apache-2.0",
        ),
    )
    actual_attn = getattr(model[0].model.config, "_attn_implementation", None)
    if not (actual_attn and "flash" in actual_attn.lower()):
        logging.warning(f"FA2 may not be active (attn_impl={actual_attn!r}); training will be slower.")

    # 2. 加载数据集。每个配置是一个源子集(32 个 lighton + 7 个重排序检索
    # 领域)。保留的评估行作为 'quora' 配置的 'validation' split 存在。
    dataset_repo = "cross-encoder/ettin-reranker-v1-data"
    train_pieces = []
    eval_dataset = None
    for config_name in get_dataset_config_names(dataset_repo):
        dataset = load_dataset(dataset_repo, config_name)
        train_pieces.append(dataset["train"])
        if "validation" in dataset:
            eval_dataset = dataset["validation"]
    train_dataset = concatenate_datasets(train_pieces)
    print(train_dataset)

    # 3. 定义损失函数
    loss = MSELoss(model)

    # 4. 指定训练参数
    args = CrossEncoderTrainingArguments(
        output_dir=f"models/{run_name}",
        num_train_epochs=1,
        per_device_train_batch_size=per_device_batch_size,
        per_device_eval_batch_size=per_device_batch_size,
        gradient_accumulation_steps=1,
        learning_rate=learning_rate,
        warmup_ratio=0.03,
        bf16=True,
        eval_strategy="steps",
        eval_steps=0.05,
        save_strategy="steps",
        save_steps=0.05,
        save_total_limit=5,
        logging_steps=0.025,
        logging_first_step=True,
        load_best_model_at_end=True,
        metric_for_best_model="eval_NanoBEIR_R100_mean_ndcg@10",
        dataloader_num_workers=dataloader_workers,
        run_name=run_name,
        seed=12,
    )

    # 5. 创建评估器
    evaluator = CrossEncoderNanoBEIREvaluator(
        dataset_names=["msmarco", "nfcorpus", "nq", "fiqa2018", "touche2020", "scifact",
                       "hotpotqa", "arguana", "fever", "dbpedia", "climatefever", "scidocs",
                       "quoraretrieval"],
        batch_size=per_device_batch_size,
        always_rerank_positives=False,
        show_progress_bar=False,
    )

    # 6. 创建训练器
    trainer = CrossEncoderTrainer(
        model=model,
        args=args,
        train_dataset=train_dataset,
        eval_dataset=eval_dataset,
        loss=loss,
        evaluator=evaluator,
    )

    # 7. 训练前评估
    if trainer.is_world_process_zero():
        with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
            evaluator(model)

    # 8. 训练
    trainer.train()

    # 9. 评估最终模型
    if trainer.is_world_process_zero():
        with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
            evaluator(model)

    # 10. 保存最终模型
    final_dir = f"models/{run_name}/final"
    model.save_pretrained(final_dir)

if __name__ == "__main__":
    main()

对于多节点训练(超过 17m/32m 的任何模型),使用 torchrun 启动相同的脚本:

# 单节点 (17m, 32m):默认即可
python train.py

# 150m 的多节点 4n 设置,保持 global_batch_size=192:
torchrun --nproc_per_node=8 --nnodes=4 ... train.py

结论

ettin-reranker-v1 系列,使用单一简单方案训练,在发布的所有尺寸(最高 1B 参数)上均达到 SOTA 水平。从强教师模型到广泛领域和检索特定混合数据的 pointwise MSE 蒸馏,可以干净地从 17M 扩展到 1B 参数,不同大小之间仅需更改学习率和每个设备的批大小。

每个 ettin-reranker-v1 模型在 MTEB 和 NanoBEIR 上都以显著优势击败了 ms-marco-MiniLM-L*-v2 系列。cross-encoder/ettin-reranker-150m-v1 是我在 600M 以下范围内测试的最强中端重排序模型,cross-encoder/ettin-reranker-400m-v1 的 MTEB 分数与 1.54B 教师模型相差仅 0.0024,而 cross-encoder/ettin-reranker-1b-v1 与该教师模型的差距仅为 0.0001。

所有内容集中在一处:

如果你在这些模型的基础上构建了什么,请告诉我!我真的很想看看人们用它们做了什么,如果你能使用发布的数据训练出更好的重排序模型,那就更好了。该方案有意保持简单,部分原因是为了给其他人留出足够的改进空间。训练一个更强的教师模型,同样的脚本可以继续产生更好的学生模型。

致谢

我要感谢 Ettin 团队(Orion Weller、Kathryn Ricci、Marc Marone、Antoine Chaffin、Dawn Lawrie 和 Benjamin Van Durme)构建了这些重排序模型所基于的基础编码器,感谢 LightOn 团队(Antoine Chaffin、Raphael Sourty、Paulo Moura 和 Amélie Chatelain)在训练数据收集方面的工作,以及感谢 Mixedbread AI 团队(Xianming Li、Aamir Shakir、Rui Huang、Tsz-fung Andrew Lee、Julius Lipp、Benjamin Clavié 和 Jing Li)在教师模型方面的工作

引用

如果你使用 ettin-reranker-v1 系列或任何已发布的工件,请引用此博文:

@misc{aarsen2026ettin-reranker,
    title = "Introducing the Ettin Reranker Family",
    author = "Aarsen, Tom",
    year = "2026",
    publisher = "Hugging Face",
    url = "https://huggingface.co/blog/ettin-reranker",
}
译自 Hugging Face · 官方博客 · 录于 二〇二六年五月十九日