RuModernBERT-base Reranker (Russian)

Cross-encoder reranker for high-precision Russian text ranking in two-stage retrieval pipelines. Fine-tuned on RuModernBERT-base (150M parameters) as part of a bachelor thesis on contextual cross-encoders for Russian IR.

Use case: rerank BM25 (or other first-stage) top-k candidates by scoring query–passage pairs.

Model summary

Architecture Cross-encoder (ModernBertForSequenceClassification)
Base model deepvk/RuModernBERT-base
Parameters 150M
Max input length 512 tokens
Languages Russian (primary); some English training data
Checkpoint Step 4914 (best validation MAP)

Training

  • Corpus: multi-source Russian retrieval data (11 sources + selected English domain data), ~6.57M pairs after consistency filtering (k=30) with Qwen3-Embedding-0.6B
  • Hard negatives: 8 mined + 2 random in-batch per query (8HN+2RN)
  • Loss: Multiple Negatives Ranking Loss (scale 10.0), sentence-transformers 5.2.2
  • Optimizer: AdamW, lr 1e-5, weight decay 0.01, cosine schedule, 5% warmup
  • Batch size: 256 per device (128 with gradient caching), bf16 + TF32
  • Selection: early stopping on validation MAP (~1.3 epochs)

Evaluation (rusBEIR)

Macro-average over 9 out-of-distribution rusBEIR datasets (BM25 top-100 → cross-encoder rerank):

Metric Score
NDCG@10 0.560
MAP@10 0.432
MRR@10 0.597
P@3 0.311

Compared to off-the-shelf baselines on the same benchmark: beats Qwen3-Reranker-0.6B (596M) on all reported metrics; within ~3 pp NDCG@10 of bge-reranker-v2-m3 (568M) at ~4× fewer parameters.

Usage

pip install -U sentence-transformers
from sentence_transformers import CrossEncoder

model = CrossEncoder("n-bor1sov/RuModernBERT-base-reranker-ru")

query = "Куда подавать апелляцию, если дело рассматривал Головинский районный суд?"
candidates = [
    "Жалобу подают в Мосгорсуд через Головинский районный суд.",
    "Апелляционную жалобу на заочное решение подают в районный суд.",
]

scores = model.predict([[query, doc] for doc in candidates])
ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)

For a single query against many passages, use model.rank(query, candidates).

Inference with vLLM

vLLM serves the model as a scoring (cross-encoder) model with a Jina/Cohere-compatible rerank API. Scores already include the sigmoid and match sentence-transformers output.

pip install -U vllm
# vLLM < 0.10: replace `--runner pooling` with `--task score`
vllm serve n-bor1sov/RuModernBERT-base-reranker-ru \
  --runner pooling --dtype bfloat16 --max-model-len 512
curl http://localhost:8000/v1/rerank -H "Content-Type: application/json" -d '{
  "model": "n-bor1sov/RuModernBERT-base-reranker-ru",
  "query": "Куда подавать апелляцию, если дело рассматривал Головинский районный суд?",
  "documents": [
    "Жалобу подают в Мосгорсуд через Головинский районный суд.",
    "Апелляционную жалобу на заочное решение подают в районный суд."
  ]
}'
# → {"results": [{"index": 0, "relevance_score": 0.84, ...}, ...]}

For offline batch scoring in Python:

from vllm import LLM

llm = LLM(model="n-bor1sov/RuModernBERT-base-reranker-ru",
          runner="pooling", dtype="bfloat16", max_model_len=512)
outputs = llm.score(query, candidates)
scores = [o.outputs.score for o in outputs]

The model was trained with 512-token inputs. Requests longer than --max-model-len are rejected, so truncate long passages on the client side.

Inference with Triton

Export the model to ONNX and serve it with Triton's onnxruntime backend. Tokenization runs on the client.

1. Export (pip install torch transformers onnx, then mkdir -p model_repository/ru_reranker/1):

import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification

name = "n-bor1sov/RuModernBERT-base-reranker-ru"
tok = AutoTokenizer.from_pretrained(name)
model = AutoModelForSequenceClassification.from_pretrained(name, attn_implementation="eager").eval()

enc = tok(["запрос"], ["документ"], return_tensors="pt")
torch.onnx.export(
    model, (enc["input_ids"], enc["attention_mask"]),
    "model_repository/ru_reranker/1/model.onnx",
    input_names=["input_ids", "attention_mask"], output_names=["logits"],
    dynamic_axes={"input_ids": {0: "batch", 1: "seq"},
                  "attention_mask": {0: "batch", 1: "seq"},
                  "logits": {0: "batch"}},
    opset_version=17, dynamo=False,
)

2. Model repository:

model_repository/
└── ru_reranker/
    ├── config.pbtxt
    └── 1/model.onnx

config.pbtxt:

name: "ru_reranker"
backend: "onnxruntime"
max_batch_size: 64
input [
  { name: "input_ids",      data_type: TYPE_INT64, dims: [ -1 ] },
  { name: "attention_mask", data_type: TYPE_INT64, dims: [ -1 ] }
]
output [
  { name: "logits", data_type: TYPE_FP32, dims: [ 1 ] }
]
instance_group [ { kind: KIND_GPU, count: 1 } ]
dynamic_batching { max_queue_delay_microseconds: 1000 }

3. Run the server:

docker run --gpus all --rm -p 8000:8000 -p 8001:8001 \
  -v $PWD/model_repository:/models \
  nvcr.io/nvidia/tritonserver:24.12-py3 \
  tritonserver --model-repository=/models

4. Client (pip install tritonclient[http] transformers numpy):

import numpy as np
import tritonclient.http as httpclient
from transformers import AutoTokenizer

tok = AutoTokenizer.from_pretrained("n-bor1sov/RuModernBERT-base-reranker-ru")
client = httpclient.InferenceServerClient("localhost:8000")

def rerank(query, docs):
    enc = tok([query] * len(docs), docs, padding=True, truncation=True,
              max_length=512, return_tensors="np")
    inputs = []
    for name in ("input_ids", "attention_mask"):
        arr = enc[name].astype(np.int64)
        inp = httpclient.InferInput(name, arr.shape, "INT64")
        inp.set_data_from_numpy(arr)
        inputs.append(inp)
    logits = client.infer("ru_reranker", inputs).as_numpy("logits")[:, 0]
    return 1 / (1 + np.exp(-logits))  # same scores as CrossEncoder.predict

Send at most max_batch_size (64) pairs per request. Dynamic batching only merges requests whose padded lengths match. Padding to a fixed max_length=512 lets it merge every request, but makes short inputs slower.

Citation

If you use this model, please cite the thesis:

@thesis{dydalin2026russianreranker,
  author  = {Dydalin, Dmitrii Sergeevich},
  title   = {A Contextual Cross-Encoder for High-Precision Textual Reranking in Russian Language Information Retrieval},
  school  = {Innopolis University},
  year    = {2026},
  type    = {Bachelor's thesis},
}

Base model: RuModernBERT-base. Training library: Sentence Transformers.

Downloads last month
13
Safetensors
Model size
0.1B params
Tensor type
F32
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for n-bor1sov/RuModernBERT-base-reranker-ru

Finetuned
(19)
this model