Hugging Face 发布六款 Ettin 重排序模型

Hugging Face Blog··作者 Tom Aarsen

关键信息

这些模型采用蒸馏式训练方案,使用 mixedbread-ai/mxbai-rerank-large-v2 的分数进行点式 MSE 训练,数据集为 cross-encoder/ettin-reranker-v1-data,该数据集由 lightonai/embeddings-pre-training 的子集与 lightonai/embeddings-fine-tuning 的重排序子集混合而成。它们是标准的 Sentence Transformers CrossEncoder 模型,可直接通过库中的 predict 和 rank 接口进行成对打分或排序。

资讯摘要

Hugging Face 发布了六个基于 Ettin ModernBERT 编码器的新 Sentence Transformers CrossEncoder 重排序模型。六个模型的规模分别是 17M、32M、68M、150M、400M 和 1B 参数。作者表示,这些模型在各自的参数规模上都达到了当前最优水平。除了模型本身,这次发布还公开了生成这些模型所用的数据和完整训练方案。训练方法采用蒸馏思路,在 cross-encoder/ettin-reranker-v1-data 数据集上,对 mixedbread-ai/mxbai-rerank-large-v2 的分数进行点式 MSE 学习。该数据集被描述为 lightonai/embeddings-pre-training 的一个子集与 lightonai/embeddings-fine-tuning 的一个重排序子集混合而成。

文章解释了 reranker,也就是点式 cross-encoder,是一种对查询和文档成对输入并输出相关性分数的模型。它与 embedding 模型不同,后者分别编码查询和文档,再通过向量相似度计算结果;cross-encoder 会让两段文本在每一层 Transformer 中相互注意,因此准确度更高,但计算成本也更高。由于它不能高效地遍历整个语料库,实际生产中通常采用“先召回、再重排”的流程:先用快速 embedding 模型找出候选,再用 cross-encoder 对前 K 个结果重新排序。博客还提到,这六个重排序模型与 google/embeddinggemma-300m 一起在 MTEB(eng, v2) Retrieval 上进行了评估,并且还展示了另外五种 embedding 模型搭配的结果。发布的模型是标准的 Sentence Transformers CrossEncoder,可以通过很少的代码调用 predict 或 rank 接口直接使用。作者最后说明,训练方案还借助了 Sentence Transformers v5.5.0 新增的 train-sentence-transformers Agent Skill,可通过 AI 编程代理来微调 SentenceTransformer、CrossEncoder 或 SparseEncoder 模型。

Hugging Face 发布六款 Ettin 重排序模型

资讯正文

今天我发布了六个新的 Sentence Transformers CrossEncoder reranker,它们在各自规模上都达到了最先进水平,基于 Ettin ModernBERT 编码器构建,并附带生成它们的数据集和完整训练方案:

cross-encoder/ettin-reranker-17m-v1

cross-encoder/ettin-reranker-32m-v1

cross-encoder/ettin-reranker-68m-v1

cross-encoder/ettin-reranker-150m-v1

cross-encoder/ettin-reranker-400m-v1

cross-encoder/ettin-reranker-1b-v1

这些模型采用蒸馏方案训练:在 cross-encoder/ettin-reranker-v1-data 上,对 mixedbread-ai/mxbai-rerank-large-v2 的分数做逐点 MSE;该数据集是 lightonai/embeddings-pre-training 的一个子集,并混合了 lightonai/embeddings-fine-tuning 的一个重排序子集。

我们的六个 reranker 与 google/embeddinggemma-300m 搭配后,在 MTEB(eng, v2) Retrieval 上表现出色。有关另外五种 embedding 模型搭配,请参见 Results。

如果你是 reranker 新手,并且想先了解“为什么要用”,请跳到 What is a reranker, and why pair one with an embedder?。如果你只是想直接接入一个模型,请跳到 Usage。如果你想自己训练,请跳到 Training。

我借助 Sentence Transformers v5.5.0 中新增的 train-sentence-transformers Agent Skill 搭建了下面的训练方案。你可以使用 hf skills add train-sentence-transformers [--global] [--claude] 安装它,并让你的 AI 编程代理(Claude Code、Codex、Cursor、Gemini CLI 等)在你的数据上微调 SentenceTransformer、CrossEncoder 或 SparseEncoder 模型。

- 什么是 reranker,为什么要把它和 embedder 搭配使用?

- 用法

- 架构细节

- 结果

- 训练

- 结论

- 致谢

reranker(也叫 pointwise cross-encoder)是一种神经模型,它接收一个(query, document)对并输出一个单一的相关性分数。与 embedding 模型不同,embedding 模型是分别对 query 和 document 进行编码,再通过两个 embedding 向量计算相似度;reranker 则让两段文本在每一层 transformer 中彼此注意。这样的联合编码更准确,但代价也更高:模型必须针对每一个(query, document)对都运行一次,而不是每段文本只运行一次。

由于 cross-encoder 对整个语料库逐一运行的成本过高,生产环境中常见的模式是先检索再重排:先用快速的 embedding 模型检索出前 K 个候选项(成本低),再用 cross-encoder 以更高精度对这 K 个结果重新排序。这样总成本保持可控,同时最终排序会比穷举式的 cross-encoder 扫描结果更接近理想效果。

在整篇博客中,我会交替使用“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 tokens 的上下文(这对长文档重排序很有用)。

建议安装 kernels,并设置 model_kwargs={"dtype": "bfloat16", "attn_implementation": "flash_attention_2"},以获得最高吞吐量。更多细节请参见下面的 Speed 部分,但总体上,取决于模型大小和序列长度,你可以预期相较于默认加载获得 1.7x-8.3x 的速度提升。

model = CrossEncoder(

"cross-encoder/ettin-reranker-32m-v1",

model_kwargs={"dtype": "bfloat16", "attn_implementation": "flash_attention_2"},

下面是一个完整示例:使用快速 embedder 进行检索,再用 reranker 做最终排序:

from sentence_transformers import SentenceTransformer, CrossEncoder

# 使用静态 embedder 进行快速检索(在 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.",

# ... production 中还有成千上万甚至数百万条更多内容

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)

# (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.

这与大多数现代搜索系统使用的结构相同。检索器决定哪些内容进入漏斗,重排序器决定谁胜出。

这六个 reranker 共享同一架构,只是在 backbone 大小上不同。backbone 是约翰斯·霍普金斯大学 Ettin 套件中的六个 Ettin encoder 之一。这些模型采用 ModernBERT 风格,使用未填充注意力、RoPE 位置编码、GeGLU,以及 2T tokens 的开放许可预训练,支持最高 8192 tokens 的上下文。

在每个 encoder 之上,reranker 使用一个 4 模块分类头,结构与 ModernBertForSequenceClassification

相呼应,但由 Sentence Transformers 的模块化组件构建而成。底层 Transformer

是普通的 AutoModel

,而不是 AutoModelForSequenceClassification

,这使我们能够对变长输入使用 sequence unpadding,以配合 Flash Attention 2。在中等文档序列长度下,与 fp32+SDPA 相比,这会带来 1.7x-8.3x 的速度提升,具体取决于模型大小(完整基准请见 Speed):

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 每三层才使用一次全局注意力,其余三分之二的层使用局部窗口注意力,远处位置无法到达 CLS。经验上,那几层全局层携带了足够的信号,使 CLS 成为更好的 pooling 选择。

这六个模型都以 Apache 2.0 许可证发布,与 Ettin encoders 保持一致。

我将每个已发布模型都在完整的 MTEB(eng, v2)

Retrieval 基准上进行了测试(10 个任务,重排序前 100),使用 MTEB 的两阶段 reranking 流程,并让每个 reranker 与六个覆盖速度/质量光谱的 embedding 模型配对:

下方每张图中的虚线检索器-only 曲线,是需要超越的基线数字。任何低于它的结果都意味着 reranker 平均而言在伤害整个 pipeline:

完整结果表(点击展开)

6 组 embedder 配对下的平均 NDCG@10,按降序排序。我们的六个模型以粗体显示,teacher mixedbread-ai/mxbai-rerank-large-v2

以下划线标出。

† 限制为 max_seq_length=8192

(基于 Qwen3 的 4B reranker 在原生上下文下无法装入单张 H100 80GB)。原生上下文评估的结果很可能更高。

NanoBEIR 完整结果表(点击展开)

NanoBEIR 是一个快速的 BEIR 子集,包含 13 个数据集,每个数据集使用 50 个 query,针对最多 5000 篇文档。NanoBEIR 是训练期间 metric_for_best_model

所设置的指标(见 Evaluation),也是我用来指导实验的指标。

我发布的最小模型 17M,在 MTEB 上以大约一半的参数量,超过了 33M 的 ms-marco-MiniLM-L12-v2

,NDCG@10 提升 +0.051(0.5576 对 0.5066);在 NanoBEIR 上提升 +0.038(0.6746 对 0.6369)。32M 则以 17x 的参数差距,在 MTEB 上超过了 568M 的 BAAI/bge-reranker-v2-m3

,提升 +0.025(0.5779 对 0.5526)。如果你一直在 retrieve-then-rerank 栈中把某个传统的 MiniLM reranker 作为默认选项,那么用我们的 17M(或 32M)替换它,是一种低风险的直接替换方案,并且在这两个基准上都能带来明显的质量提升。

在表格中继续往上看,我们的 150M 是我在 600M 以下范围内于 MTEB 上测试过的最强 reranker,略微超过了最近的 Qwen/Qwen3-Reranker-0.6B(0.5940),而参数量只用了它的九分之一。

对比中整体最强的 reranker 是 Qwen/Qwen3-Reranker-4B,在 MTEB 上得分 0.6367,比我们的 1B 模型高 0.025。要用当前的配方缩小这段差距,可能需要从更强的教师模型进行蒸馏(我们的教师本身也低于 Qwen3-Reranker-4B)。对于大多数先检索再重排的工作负载来说,我们的 1B 以四分之一的参数量(见 Speed)是更实用的选择。

质量分数只是 reranker 需要关注的一半。另一半是,它的延迟是否能装进你在检索和向用户展示结果之间留出的预算里。下面我来讲讲我的测量结果。

我在单张 NVIDIA H100 80GB 上,将全部六个已发布模型与十三个公开 reranker(强基线,规模最高约 1B 参数)进行了基准测试。查询和文档来自 sentence-transformers/natural-questions,保持其自然文档长度分布:大多数 NQ 答案都很短,也有一些很长。为避免让较旧的模型占便宜,文档都被截断到 max_length=512。每个模型都使用其支持的最佳注意力实现:凡是架构支持的地方都使用 Flash Attention 2(BERT、XLM-RoBERTa、ModernBERT、Qwen2),不支持的地方则使用 SDPA,而 DeBERTa-v2 使用 eager(它目前在 transformers 中既不支持 FA2,也不支持 SDPA)。

对于每个模型,自动批量搜索从 batch size 8 开始,逐步翻倍,直到 GPU 内存耗尽。每个 batch size 我都会跑三次计时,并保留中位数吞吐量,这样某一次不走运的运行不会把结果拉偏。报告中的吞吐量取的是最终胜出的那个 batch size。

表 1. 吞吐量,单位为每秒 pairs,全部使用 bfloat16。我们的六个 reranker 以粗体显示。

我们的 17M 是整个对比中最快的 reranker,达到每秒 7517 pairs。这个速度几乎是 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-base 和 ibm-granite/granite-embedding-reranker-english-r2。它们都建立在同一个 ModernBERT-base 主干之上。我们的 150M 运行速度为每秒 3237 pairs,而这两个对手分别只有 1418 和 1404,速度差距达到 2.3 倍。

这三款 150M 模型都使用 Flash Attention 2,但那两款同类模型是通过 AutoModelForSequenceClassification 加载的,

这会让输入保持填充状态。因此,注意力机制本身确实运行了 FA2 内核,但模型其余部分仍然在对不会贡献任何内容的填充 token 进行密集计算。我们的模块化 Transformer

模块(见上面的 Architecture Details)会将未填充输入一路传递到整个模型,因此每一层只会把算力花在真实 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 模型的一半左右。模型本身没有问题,只是它们无法使用现代注意力内核。

消费级 GPU 上的相同基准(RTX 3090,24 GB)

如果你是在消费级显卡而不是数据中心 GPU 上自托管,这里是 RTX 3090 上的相同吞吐量测试。基准设置与表 1 相同:bfloat16

、每个模型使用支持情况最好的 attention、在能装下的最大 batch 下取三次测试的中位吞吐量。

我们的 17M 仍然是表中最快的模型,每秒 9008 对,甚至高于它在 H100 上的结果,这说明在极小模型规模下,原始计算并不是瓶颈,而 H100 的额外算力也无法转化为更高吞吐量。表格中间部分的排序发生了一些变化,MiniLM reranker 超过了我们的 32M 和 68M,而 1B 则落后于 mxbai-rerank-base-v2

(每秒 189 对对比 221 对)。我们的 150M 模型仍然明显领先于那两款基于 ModernBERT 的 150M 同类模型,并且“替代教师模型”的故事依然成立:我们的 1B 速度是 1.5B mxbai-rerank-large-v2

的 2.7 倍(每秒 189 对对比 69 对)。

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

在 CPU 上,我们无法利用 bf16、Flash Attention 2 或 unpadding,因此延迟方面的情况要简单一些:参数量越高,模型越慢。17M 模型明显快于 ms-marco-MiniLM-L6-v2

(每秒 267.4 对对比 143.9 对),甚至比更小的 ms-marco-MiniLM-L4-v2

(206.2)还快。正如预期的那样,由于不再适用 unpadding,我们的 150M 模型与那两款 150M 同类模型处于相近水平(每秒 14.0 对,对比 14.5 和 14.7 对)。如果你的瓶颈在 CPU 上,我们的 17M 和 32M 是更实用的选择。

为了说明速度来自哪里,下一张表会分别测试 fp32+SDPA

、bf16+SDPA

以及 bf16+FA2

对于我们六个模型,使用相同的基准配置。FA2 一栏分成两部分:一部分是输入仍然带填充的情况(也就是一个被包装后的模型会看到的样子),另一部分是未填充输入(也就是我们的模块化 Transformer 实际所做的)。最右侧一栏是当启用 FA2 时我们的模型默认使用的配置。

表 2. 在 max_length=512 时,六个已发布尺寸的精度与注意力消融实验

用于自然的 NQ 文档。每个单元格显示的是每秒处理的 pair 数,括号中给出相对于 fp32+SDPA 的倍数,第二行显示峰值 GPU 内存。最右侧一栏(加粗)是当启用 FA2 时我们的模型默认使用的配置。

出乎意料的是,在输入仍然带填充的情况下开启 FA2,在发布的所有尺寸上都比 bf16+SDPA 更慢。FA2 内核更偏好未填充格式,而当你把带填充的输入喂给它时,你既要为格式转换付出簿记开销,同时还要在填充 token 上消耗计算。因此,带填充的 bf16+FA2 这一列,大致就是如果你在 model_kwargs 中把 sdpa 换成 flash_attention_2、但不改变模型加载器其他任何内容时会测到的结果。Table 1 中的 gte-reranker-modernbert-base 和 granite-embedding-reranker-english-r2 就属于这种情况。

最后,从带填充的 bf16+FA2 到不带填充的 bf16+FA2,还能额外带来 1.78x(1B)到 2.45x(68M)之间的吞吐提升,同时峰值内存也会显著下降,从而允许使用更大的 batch size。

所以我的建议很简单:同时启用 bf16 和 FA2。六个 Ettin reranker 会默认使用未填充输入,因为 Architecture Details 部分的模块化 Transformer 模块就是按这种方式配置的。完整代码片段与上文 Usage 部分中的相同:

"cross-encoder/ettin-reranker-150m-v1",

model_kwargs={

"dtype": "bfloat16",

"attn_implementation": "flash_attention_2", # 见下方提示

},

使用 pip install kernels 来安装 FA2。它为多种 GPU 架构、CUDA 版本和操作系统提供了预编译内核。

对于其他 CrossEncoder,有一个注意事项:完整加速只有在模型是用像 Ettin reranker 这样的模块化 Transformer 构建时才可用。把同样两个标志应用到通过 AutoModelForSequenceClassification 加载的 CrossEncoder 上,得到的就会是 Table 2 中更慢的带填充 bf16+FA2 一栏。

下面的训练脚本最初是 Sentence Transformers v5.5.0 中新推出的 train-sentence-transformers Agent Skill 的输出。如果你使用 AI 编码代理(Claude Code、Codex、Cursor、Gemini CLI,等等),你可以安装这个 skill,并让它微调一个 SentenceTransformer、CrossEncoder 或 SparseEncoder。

在你的数据上进行训练。这个技能包含了针对基础模型选择、损失函数和评估器选择、hard-negative 挖掘、蒸馏、LoRA、Matryoshka、多语言训练以及静态嵌入的版本感知指导,并为每种模型类型提供模板脚本。

hf skills add train-sentence-transformers --claude # symlinks into .claude/skills/

hf skills add train-sentence-transformers --global # under ~/.agents/skills/

像“Fine-tune a cross-encoder reranker on (query, document) pairs from my dataset, mine hard negatives, and push it to my Hub repo”这样的提示,会生成一个可运行的脚本,之后你可以在此基础上继续迭代。我就是这样开始着手下面这套方案的。

这六个 reranker 都采用了相同的单阶段训练方案。只有学习率和每个设备上的 batch size 会随模型大小而变化。完整训练脚本大约 150 行,并且只使用了一个公开数据集。

这套方案在一次覆盖不同模型大小的扫描后就收敛了。每个规模的学习率都通过在最终训练数据约 15% 的子集上进行小规模网格搜索来调优,而且得到的学习率可以直接迁移到全量数据训练中,无需重新调参。除学习率外,没有必要针对不同规模做额外调优。

大多数已发布的 reranker 方案,都是用人工标注的相关性三元组进行训练:一个查询、一个正样本文档,以及可选的 hard negatives;损失函数通常是对比式、点式、配对式或列表式,例如 MultipleNegativesRankingLoss、BinaryCrossEntropyLoss、RankNetLoss 或 LambdaLoss。比如可以参考我之前的博客《Training and Finetuning Reranker Models with Sentence Transformers》。

但这种方法在实践和理论上都有一些缺点。首先,正样本需要人工标注,这在很多领域里既昂贵又难以扩展。其次,模型只会看到那些有人处理过的那一小部分 (query, document) 对的标签。尤其是在进行 hard negative 挖掘之后,会产生很多假负样本,例如《Hard Negatives, Hard Lessons》中所展示的那样。第三,这种二元标注方式与现实并不匹配,因为在现实中,有些文档本来就比其他文档更相关。

在这里我选择了另一条路线:从一个已经很强的 teacher reranker 进行按点回归的 MSE 蒸馏。这个设置简单到可以用三行来描述:

- Teacher:mixedbread-ai/mxbai-rerank-large-v2(15.4 亿参数)。- Loss:对原始 teacher logits 使用 MSELoss(范围约为 [−12, 22]),也就是不进行重缩放。- 训练数据:约 1.43 亿个 (query, document, teacher_score) 三元组。

我已经把训练数据作为一个单独的 Hugging Face 数据集发布出来,cross-encoder/ettin-reranker-v1-data,由两个来源整理而成。为保证来源透明,每个来源都保留为各自独立的 split:

- LightOn 预训练数据(lightonai/embeddings-pre-training,未筛选):32 个 split,覆盖广域文本相似性信号(MTP、FW-EDU、Reddit、PAQ、S2ORC、Amazon、Wikipedia、MS MARCO 等)。我对其中一些 split 限制了样本数量,因此总计约有 1.1 亿个 (query, document, similarity) 三元组。- 来自 lightonai/embeddings-fine-tuning 的重打分检索数据:7 个 split(msmarco、hotpotqa、trivia、nq、squadv2、fiqa、fever

)。该源数据集每个查询最多包含 2048 篇候选文档(最初使用 Alibaba-NLP/gte-modernbert-base 进行打分),我用 mixedbread-ai/mxbai-rerank-large-v2 重新排序后,将结果上传为 cross-encoder/lightonai-embeddings-fine-tuning-reranked-v1。该数据集使用 Jang 等人的 quantile-anchor 配方,将每个查询的 2048 个候选缩减到 256 个(所有正样本 + 排名前 16 的 hard negative + 约 239 个按分位锚点分层采样的样本)。训练时,我从每个查询的这 256 个样本中选取 64 个:32 个来自按分数排序的前段(正样本加上最难的负样本),以及 32 个从教师排序更靠后的区间中采样的中等难度负样本。确切的 rank 位置请参见数据集卡片。

总计:约 1.43 亿条(query, document, score)三元组,外加一个保留的 5000 行评估切分(quora 的尾部),用于训练过程中的评估损失。

大多数超参数在不同模型规模之间保持不变:

CrossEncoderTrainingArguments(

num_train_epochs=1, # 我选择更多数据而不是更多轮次

per_device_train_batch_size=..., # global_batch_size // world_size(见下表)

gradient_accumulation_steps=1,

learning_rate=..., # 按模型规模不同,见表

warmup_ratio=0.03, # 约 3% 的线性 warmup,然后线性衰减(默认)

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,

只有学习率和全局批大小会随模型规模变化。

global_batch_size 是 per_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_end 的 metric_for_best_model。NanoBEIR 很快,因此每次训练运行我都负担得起评估 20 次。训练结束后,我在完整的 MTEB(eng, v2) Retrieval 基准上分别评估了按 NanoBEIR 选出的最佳 checkpoint 和最后一个 checkpoint。最终发布的 checkpoint 是在 MTEB 上表现最好的那个。除了 68m 之外,NanoBEIR 偏好的 checkpoint 在所有规模上都获胜;68m 的情况下,最后一个 checkpoint 略强一些。

完整脚本(所有已发布模型的训练方式)是一个单文件。每次运行只会变化 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.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)

# 按尺寸配置。我用这些全局(有效)batch size 进行了学习率搜索,

# 也通过纳入 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",

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 可能未激活(attn_impl={actual_attn!r});训练会更慢。")

# 2. 加载数据集。每个 config 都是一个源子集(32 lighton + 7 rerank retrieval

# domains)。留出的评估行位于 'quora' config 的 '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,

learning_rate=learning_rate,

warmup_ratio=0.03,

bf16=True,

eval_steps=0.05,

logging_steps=0.025,

logging_first_step=True,

dataloader_num_workers=dataloader_workers,

run_name=run_name,

# 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. 评估最终模型

# 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

# 4 节点多机配置,用于 150m,保持 global_batch_size=192:

torchrun --nproc_per_node=8 --nnodes=4 ... train.py

ettin-reranker-v1 系列使用单一、简单的训练配方,在截至目前发布的所有规模上都达到了最先进水平,最大到 1B 参数。基于一个强教师模型进行点式 MSE 蒸馏,并在广域数据与检索特定数据的混合上训练,从 17M 到 1B 参数都能平滑扩展,且不同规模之间只需要调整学习率和每设备批大小。

每一个 ettin-reranker-v1 模型在 MTEB 和 NanoBEIR 上都以相当明显的优势击败了 ms-marco-MiniLM-L*-v2 家族。cross-encoder/ettin-reranker-150m-v1

是我在 6 亿参数以下范围内测试过的最强中等规模 reranker,cross-encoder/ettin-reranker-400m-v1

的 MTEB 分数距离 1.54B 教师模型只有 0.0024,而 cross-encoder/ettin-reranker-1b-v1

则与该教师模型只差 0.0001。

所有内容汇总如下:

- 模型:

- 数据集:

cross-encoder/ettin-reranker-v1-data

约包含 ~143M(query, document, label)

triples,被保留为 39 个命名拆分,因此每一行的来源都清晰可见。- 训练脚本:上文“整体训练脚本”中的约 150 行代码,也就是这六个模型共用的同一脚本。

如果你基于这些内容做了什么,请告诉我!我真的很想看看大家会拿它们做出什么;如果你能利用发布的数据训练出更好的 reranker,那就更好了。这个方案是刻意设计得很简单,部分原因就是要给其他人留出足够的改进空间。训练一个更强的 teacher,同样的脚本就可以持续产出更好的 student。

我想感谢 Ettin 团队(Orion Weller、Kathryn Ricci、Marc Marone、Antoine Chaffin、Dawn Lawrie 和 Benjamin Van Durme)打造了这些 reranker 所依赖的基础编码器,感谢 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)在 teacher model 方面所做的工作。

如果你使用 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",

来源与参考

  1. 原始链接
  2. Introducing the Ettin Reranker Family

收录于 2026-05-20