Feature Extraction
sentence-transformers
Safetensors
English
slideon
sparse
sentence-similarity
custom-code
sparse-retrieval
matryoshka
custom_code
🇪🇺 Region: EU
Instructions to use lightonai/SlideOn with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- sentence-transformers
How to use lightonai/SlideOn with sentence-transformers:
from sentence_transformers import SentenceTransformer model = SentenceTransformer("lightonai/SlideOn", trust_remote_code=True) sentences = [ "The weather is lovely today.", "It's so sunny outside!", "He drove to the stadium." ] embeddings = model.encode(sentences) similarities = model.similarity(embeddings, embeddings) print(similarities.shape) # [3, 3] - Notebooks
- Google Colab
- Kaggle
Commit ·
0688794
0
Parent(s):
Initial release
Browse files- .gitattributes +35 -0
- README.md +248 -0
- config.json +185 -0
- config_sentence_transformers.json +12 -0
- model.safetensors +3 -0
- modeling_slideon.py +216 -0
- modules.json +8 -0
- slideon.py +96 -0
- tokenizer.json +0 -0
- tokenizer_config.json +23 -0
.gitattributes
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
README.md
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
library_name: sentence-transformers
|
| 3 |
+
pipeline_tag: feature-extraction
|
| 4 |
+
base_model: lightonai/DenseOn
|
| 5 |
+
base_model_relation: finetune
|
| 6 |
+
language:
|
| 7 |
+
- en
|
| 8 |
+
datasets:
|
| 9 |
+
- lightonai/embeddings-fine-tuning-filtered-en
|
| 10 |
+
tags:
|
| 11 |
+
- sentence-transformers
|
| 12 |
+
- sentence-similarity
|
| 13 |
+
- custom-code
|
| 14 |
+
- sparse-retrieval
|
| 15 |
+
- matryoshka
|
| 16 |
+
---
|
| 17 |
+
<div align="center">
|
| 18 |
+
<img src="https://cdn-uploads.huggingface.co/production/uploads/67e184d667350657a7d68aac/5PySy2cTawrkFWwaqNtDA.png" alt="SlideOn: a puppy sliding through a library with a bag of glowing stars" width="512" style="max-width: 100%; height: auto;">
|
| 19 |
+
|
| 20 |
+
[](https://lighton.ai)
|
| 21 |
+
[](https://www.linkedin.com/company/lighton/)
|
| 22 |
+
[](https://x.com/LightOnIO)
|
| 23 |
+
|
| 24 |
+
📚 [Collection](https://huggingface.co/collections/lightonai/slideon-and-mslideon) | 📝 [Blogpost](https://huggingface.co/blog/lightonai/slideon-mslideon) | 🛠️ [Training and Evaluation](https://github.com/lightonai/slideon-mslideon/)
|
| 25 |
+
|
| 26 |
+
</div>
|
| 27 |
+
|
| 28 |
+
<h1 align="center">SlideOn</h1>
|
| 29 |
+
<h3 align="center">English Sparse Retrieval Model by LightOn</h3>
|
| 30 |
+
|
| 31 |
+
<p align="center">
|
| 32 |
+
<a href="https://huggingface.co/lightonai/SlideOn">SlideOn</a> |
|
| 33 |
+
<a href="https://huggingface.co/lightonai/mSlideOn">mSlideOn</a> |
|
| 34 |
+
<a href="https://huggingface.co/lightonai/SlideOn-SAE">SlideOn-SAE</a> |
|
| 35 |
+
<a href="https://huggingface.co/lightonai/mSlideOn-SAE">mSlideOn-SAE</a>
|
| 36 |
+
</p>
|
| 37 |
+
|
| 38 |
+
> 🎯 **TL;DR**: A **174M-parameter English sparse retrieval model** built from DenseOn through sparse autoencoder (SAE) pretraining and supervised retrieval fine-tuning. SlideOn reaches **56.68 nDCG@10 on BEIR-15**, surpassing DenseOn's 56.20, while producing sparse representations compatible with inverted-index search. Joint query/document **Matryoshka training** preserves retrieval quality under pruning, allowing search and storage costs to be adjusted at inference without retraining.
|
| 39 |
+
|
| 40 |
+
## About the SlideOn / mSlideOn Family
|
| 41 |
+
|
| 42 |
+
With [DenseOn and LateOn](https://huggingface.co/blog/lightonai/denseon-lateon), then [mDenseOn and mLateOn](https://huggingface.co/blog/lightonai/mdenseon-mlateon), we explored dense and late-interaction retrieval using our open data recipe. SlideOn and mSlideOn reuse these models' retrieval knowledge to learn sparse representations through SAE pretraining followed by supervised fine-tuning.
|
| 43 |
+
|
| 44 |
+
**SlideOn** starts from [DenseOn](https://huggingface.co/lightonai/DenseOn) for English retrieval. **[mSlideOn](https://huggingface.co/lightonai/mSlideOn)** starts from [mLateOn](https://huggingface.co/lightonai/mLateOn) for multilingual, long-context, and code retrieval. We release both the pretrained SAE initializations and the final retrieval models.
|
| 45 |
+
|
| 46 |
+
See our [blog post](https://huggingface.co/blog/lightonai/slideon-mslideon) for the full method, results, and ablations.
|
| 47 |
+
|
| 48 |
+
## SlideOn
|
| 49 |
+
|
| 50 |
+
SlideOn encodes queries and documents into **32,768-dimensional nonnegative sparse vectors**. A learned linear encoder maps each normalized DenseOn token representation to its top-32 latent activations, followed by ReLU. Max pooling across tokens produces one vector per query or document, and retrieval uses the **dot product** between these vectors.
|
| 51 |
+
|
| 52 |
+
The dimensions are learned features, independent of the tokenizer vocabulary. Active latents can be stored in an inverted index, where query latents select posting lists and their weights contribute to document scores.
|
| 53 |
+
|
| 54 |
+
SlideOn notably:
|
| 55 |
+
|
| 56 |
+
- Reaches **56.68 on BEIR-15** with unpruned vectors, improving on DenseOn by **0.48 points**.
|
| 57 |
+
- Reaches **56.98** with unpruned queries and documents capped at **512 nonzero latents**.
|
| 58 |
+
- Retains **56.66** with queries capped at **64** and documents at **512**, reducing the measured retrieval FLOPS proxy to **8.72**.
|
| 59 |
+
- Supports inputs of up to **8,192 tokens** and independently adjustable query and document sparsity.
|
| 60 |
+
|
| 61 |
+
## Results
|
| 62 |
+
|
| 63 |
+
### Headline Results (BEIR-15, nDCG@10)
|
| 64 |
+
|
| 65 |
+
BEIR-15 scores are average nDCG@10 across 15 datasets, reported on a 0–100 scale. Sparse models use dot-product scoring and exact search over each full evaluation corpus. All rows use **unpruned query and document vectors**. Dense and late-interaction results are taken from our [DenseOn and LateOn blog post](https://huggingface.co/blog/lightonai/denseon-lateon).
|
| 66 |
+
|
| 67 |
+
<div style="width: fit-content; max-width: 100%; margin: 0 auto;">
|
| 68 |
+
<div style="overflow-x: auto;">
|
| 69 |
+
<table style="margin: 0 auto; border-collapse: collapse; color: inherit; text-align: right; font-size: 0.85em; line-height: 1.6;">
|
| 70 |
+
<thead>
|
| 71 |
+
<tr style="border-bottom: 1px solid rgba(128, 128, 128, 0.35);">
|
| 72 |
+
<th scope="col" style="padding: 8px 20px 8px 12px; text-align: left;">Model</th>
|
| 73 |
+
<th scope="col" style="padding: 8px 16px;">Parameters</th>
|
| 74 |
+
<th scope="col" style="padding: 8px 16px; white-space: nowrap;">BEIR-15 ↑</th>
|
| 75 |
+
<th scope="col" style="padding: 8px 12px 8px 16px; white-space: nowrap;">Retrieval FLOPS ↓</th>
|
| 76 |
+
</tr>
|
| 77 |
+
</thead>
|
| 78 |
+
<tbody>
|
| 79 |
+
<tr>
|
| 80 |
+
<th colspan="4" scope="rowgroup" style="padding: 16px 12px 6px; text-align: left; font-size: 0.8em; letter-spacing: 0.08em; text-transform: uppercase;">Sparse</th>
|
| 81 |
+
</tr>
|
| 82 |
+
<tr><td style="padding: 3px 20px 3px 12px; text-align: left; color: #3b82f6;"><strong style="color: #3b82f6;">SlideOn</strong></td><td style="padding: 3px 16px; color: #3b82f6;">174M</td><td style="padding: 3px 16px; color: #3b82f6;">56.68</td><td style="padding: 3px 12px 3px 16px; color: #3b82f6;">30.87</td></tr>
|
| 83 |
+
<tr><td style="padding: 3px 20px 3px 12px; text-align: left;">LACONIC-8B</td><td style="padding: 3px 16px;">8.03B</td><td style="padding: 3px 16px;"><strong>60.06</strong></td><td style="padding: 3px 12px 3px 16px;">20.07</td></tr>
|
| 84 |
+
<tr><td style="padding: 3px 20px 3px 12px; text-align: left;">LACONIC-3B</td><td style="padding: 3px 16px;">3.21B</td><td style="padding: 3px 16px;">58.30</td><td style="padding: 3px 12px 3px 16px;">39.53</td></tr>
|
| 85 |
+
<tr><td style="padding: 3px 20px 3px 12px; text-align: left;">LACONIC-1B</td><td style="padding: 3px 16px;">1.24B</td><td style="padding: 3px 16px;">56.13</td><td style="padding: 3px 12px 3px 16px;">47.95</td></tr>
|
| 86 |
+
<tr><td style="padding: 3px 20px 3px 12px; text-align: left;">SPARSEUP</td><td style="padding: 3px 16px;">188M</td><td style="padding: 3px 16px;">54.46</td><td style="padding: 3px 12px 3px 16px;">6.20</td></tr>
|
| 87 |
+
<tr><td style="padding: 3px 20px 3px 12px; text-align: left;">Lion-SP-8B</td><td style="padding: 3px 16px;">8.0B</td><td style="padding: 3px 16px;">53.62</td><td style="padding: 3px 12px 3px 16px;">57.03</td></tr>
|
| 88 |
+
<tr><td style="padding: 3px 20px 3px 12px; text-align: left;">OpenSearch doc-v3-GTE</td><td style="padding: 3px 16px;">137M</td><td style="padding: 3px 16px;">52.68</td><td style="padding: 3px 12px 3px 16px;"><strong>0.72</strong></td></tr>
|
| 89 |
+
<tr><td style="padding: 3px 20px 3px 12px; text-align: left;">OpenSearch v2-distill</td><td style="padding: 3px 16px;">67M</td><td style="padding: 3px 16px;">51.20</td><td style="padding: 3px 12px 3px 16px;">5.14</td></tr>
|
| 90 |
+
<tr><td style="padding: 3px 20px 3px 12px; text-align: left;">SPLADE-v3</td><td style="padding: 3px 16px;">133M</td><td style="padding: 3px 16px;">50.46</td><td style="padding: 3px 12px 3px 16px;">5.55</td></tr>
|
| 91 |
+
<tr><td style="padding: 3px 20px 3px 12px; text-align: left;">SPLADE-v3-DistilBERT</td><td style="padding: 3px 16px;">90M</td><td style="padding: 3px 16px;">49.04</td><td style="padding: 3px 12px 3px 16px;">5.79</td></tr>
|
| 92 |
+
<tr><td style="padding: 3px 20px 3px 12px; text-align: left;">Granite-30M</td><td style="padding: 3px 16px;">30M</td><td style="padding: 3px 16px;">48.86</td><td style="padding: 3px 12px 3px 16px;">6.42</td></tr>
|
| 93 |
+
<tr><td style="padding: 3px 20px 12px 12px; text-align: left;">SAE-SPLADE</td><td style="padding: 3px 16px 12px;">117M</td><td style="padding: 3px 16px 12px;">48.21</td><td style="padding: 3px 12px 12px 16px;">2.67</td></tr>
|
| 94 |
+
</tbody>
|
| 95 |
+
<tbody style="border-top: 1px solid rgba(128, 128, 128, 0.25);">
|
| 96 |
+
<tr>
|
| 97 |
+
<th colspan="4" scope="rowgroup" style="padding: 16px 12px 6px; text-align: left; font-size: 0.8em; letter-spacing: 0.08em; text-transform: uppercase;">Dense</th>
|
| 98 |
+
</tr>
|
| 99 |
+
<tr><td style="padding: 3px 20px 3px 12px; text-align: left; color: #3b82f6;"><strong style="color: #3b82f6;">DenseOn</strong></td><td style="padding: 3px 16px; color: #3b82f6;">149M</td><td style="padding: 3px 16px; color: #3b82f6;"><strong style="color: #3b82f6;">56.20</strong></td><td style="padding: 3px 12px 3px 16px; color: #3b82f6;">—</td></tr>
|
| 100 |
+
<tr><td style="padding: 3px 20px 3px 12px; text-align: left;">gte-modernbert-base</td><td style="padding: 3px 16px;">149M</td><td style="padding: 3px 16px;">55.19</td><td style="padding: 3px 12px 3px 16px;">—</td></tr>
|
| 101 |
+
<tr><td style="padding: 3px 20px 3px 12px; text-align: left;">bge-large-en-v1.5</td><td style="padding: 3px 16px;">335M</td><td style="padding: 3px 16px;">54.34</td><td style="padding: 3px 12px 3px 16px;">—</td></tr>
|
| 102 |
+
<tr><td style="padding: 3px 20px 12px 12px; text-align: left;">modernbert-embed-base</td><td style="padding: 3px 16px 12px;">149M</td><td style="padding: 3px 16px 12px;">52.89</td><td style="padding: 3px 12px 12px 16px;">—</td></tr>
|
| 103 |
+
</tbody>
|
| 104 |
+
<tbody style="border-top: 1px solid rgba(128, 128, 128, 0.25); border-bottom: 1px solid rgba(128, 128, 128, 0.35);">
|
| 105 |
+
<tr>
|
| 106 |
+
<th colspan="4" scope="rowgroup" style="padding: 16px 12px 6px; text-align: left; font-size: 0.8em; letter-spacing: 0.08em; text-transform: uppercase;">Late interaction</th>
|
| 107 |
+
</tr>
|
| 108 |
+
<tr><td style="padding: 3px 20px 3px 12px; text-align: left; color: #3b82f6;"><strong style="color: #3b82f6;">LateOn</strong></td><td style="padding: 3px 16px; color: #3b82f6;">149M</td><td style="padding: 3px 16px; color: #3b82f6;"><strong style="color: #3b82f6;">57.22</strong></td><td style="padding: 3px 12px 3px 16px; color: #3b82f6;">—</td></tr>
|
| 109 |
+
<tr><td style="padding: 3px 20px 3px 12px; text-align: left;">ColBERT-Zero</td><td style="padding: 3px 16px;">149M</td><td style="padding: 3px 16px;">55.39</td><td style="padding: 3px 12px 3px 16px;">—</td></tr>
|
| 110 |
+
<tr><td style="padding: 3px 20px 3px 12px; text-align: left;">GTE-ModernColBERT-v1</td><td style="padding: 3px 16px;">149M</td><td style="padding: 3px 16px;">54.75</td><td style="padding: 3px 12px 3px 16px;">—</td></tr>
|
| 111 |
+
<tr><td style="padding: 3px 20px 3px 12px; text-align: left;">ColBERT-small</td><td style="padding: 3px 16px;">33M</td><td style="padding: 3px 16px;">53.79</td><td style="padding: 3px 12px 3px 16px;">—</td></tr>
|
| 112 |
+
<tr><td style="padding: 3px 20px 12px 12px; text-align: left;">ColBERTv2</td><td style="padding: 3px 16px 12px;">110M</td><td style="padding: 3px 16px 12px;">48.63</td><td style="padding: 3px 12px 12px 16px;">—</td></tr>
|
| 113 |
+
</tbody>
|
| 114 |
+
</table>
|
| 115 |
+
</div>
|
| 116 |
+
|
| 117 |
+
<p style="max-width: 650px; margin: 14px auto 0; font-size: 0.85em; line-height: 1.5; text-align: center;">
|
| 118 |
+
<em>Average nDCG@10 on BEIR-15 (↑ higher is better) and retrieval FLOPS (↓ lower is better). <span style="color: #3b82f6;">Blue</span> marks our models. <strong>Bold</strong> marks best per column within each family.</em>
|
| 119 |
+
</p>
|
| 120 |
+
</div>
|
| 121 |
+
|
| 122 |
+
SlideOn achieves the highest BEIR-15 score among the sparse models below 3B parameters in this comparison. It improves on SPARSEUP by **2.22 points** and on DenseOn by **0.48 points**, while remaining **0.54 points** behind LateOn. The figures below show how pruning changes the comparison when search and storage costs are taken into account.
|
| 123 |
+
|
| 124 |
+
### Retrieval Quality versus Search and Storage Cost
|
| 125 |
+
|
| 126 |
+
Query pruning reduces the number of posting lists consulted during search. Document pruning shortens those lists and reduces the number of stored document weights. Matryoshka training concentrates retrieval information in the strongest latents so that both budgets can be adjusted after training.
|
| 127 |
+
|
| 128 |
+
<div style="margin: 0 auto; text-align: center;">
|
| 129 |
+
<img
|
| 130 |
+
src="https://cdn-uploads.huggingface.co/production/uploads/67e184d667350657a7d68aac/biV_cKSk6mQfoo0x_GSSu.png"
|
| 131 |
+
alt="BEIR-15 retrieval quality versus retrieval FLOPS for sparse models below 2B parameters, including SlideOn with different query pruning budgets."
|
| 132 |
+
style="display: block; max-width: 100%; height: auto; margin: 0 auto;"
|
| 133 |
+
/>
|
| 134 |
+
<p style="max-width: 750px; margin: 12px auto 0; font-size: 0.85em; line-height: 1.5;">
|
| 135 |
+
<em>Retrieval quality versus search cost for SlideOn and sub-2B baselines. <span style="color: #3b82f6;">Blue</span> points show SlideOn with documents pruned to the top-512 latents and varying query budgets, where q-full denotes unpruned queries. Higher BEIR-15 ↑ scores and lower FLOPS ↓ are better. Retrieval FLOPS are measured on a subset of SciFact, NFCorpus, and FiQA.</em>
|
| 136 |
+
</p>
|
| 137 |
+
</div>
|
| 138 |
+
|
| 139 |
+
<div style="margin: 0 auto; text-align: center;">
|
| 140 |
+
<img
|
| 141 |
+
src="https://cdn-uploads.huggingface.co/production/uploads/67e184d667350657a7d68aac/kzPT8yhDHf1lIUh2Z2_7V.png"
|
| 142 |
+
alt="BEIR-15 retrieval quality versus average document NNZ for sparse models below 2B parameters, including SlideOn with different document pruning budgets."
|
| 143 |
+
style="display: block; max-width: 100%; height: auto; margin: 0 auto;"
|
| 144 |
+
/>
|
| 145 |
+
<p style="max-width: 750px; margin: 12px auto 0; font-size: 0.85em; line-height: 1.5;">
|
| 146 |
+
<em>Retrieval quality versus index storage cost for SlideOn and sub-2B baselines. <span style="color: #3b82f6;">Blue</span> points show SlideOn with unpruned queries and varying document budgets, where d-full denotes unpruned documents. Higher BEIR-15 ↑ scores and lower NNZ ↓ are better. Document NNZ is measured on SciFact, NFCorpus, and FiQA.</em>
|
| 147 |
+
</p>
|
| 148 |
+
</div>
|
| 149 |
+
|
| 150 |
+
**Retrieval FLOPS** estimates the expected number of shared active features per query–document pair. It is a search-cost proxy, not measured latency or encoder FLOPs. **Document NNZ** is the average number of nonzero document features and serves as a storage-cost proxy, rather than a measurement of index size in bytes. Efficiency measurements use the fixed sample of **6,000 documents and 1,271 queries** from SciFact, NFCorpus, and FiQA.
|
| 151 |
+
|
| 152 |
+
## Related Checkpoints
|
| 153 |
+
|
| 154 |
+
| Model | Description |
|
| 155 |
+
| --- | --- |
|
| 156 |
+
| **[SlideOn](https://huggingface.co/lightonai/SlideOn)** *(this model)* | English sparse retrieval model, after SAE pretraining and retrieval fine-tuning |
|
| 157 |
+
| [SlideOn-SAE](https://huggingface.co/lightonai/SlideOn-SAE) | DenseOn-based SAE initialization before retrieval fine-tuning |
|
| 158 |
+
| [mSlideOn](https://huggingface.co/lightonai/mSlideOn) | Multilingual sparse retrieval model for natural language, long documents, and code |
|
| 159 |
+
| [mSlideOn-SAE](https://huggingface.co/lightonai/mSlideOn-SAE) | mLateOn-based SAE initialization before retrieval fine-tuning |
|
| 160 |
+
| [DenseOn](https://huggingface.co/lightonai/DenseOn) | English dense retriever used to initialize SlideOn's backbone |
|
| 161 |
+
|
| 162 |
+
The SAE checkpoints are starting points for training. Use SlideOn or mSlideOn for the retrieval results reported in the blog.
|
| 163 |
+
|
| 164 |
+
## Training Data and Recipe
|
| 165 |
+
|
| 166 |
+
See our blog post for [training data](https://huggingface.co/blog/lightonai/slideon-mslideon#training-data), [SAE pretraining](https://huggingface.co/blog/lightonai/slideon-mslideon#sparse-autoencoder-pretraining), and [supervised retrieval fine-tuning](https://huggingface.co/blog/lightonai/slideon-mslideon#supervised-fine-tuning-for-sparse-retrieval).
|
| 167 |
+
|
| 168 |
+
## Model Details
|
| 169 |
+
|
| 170 |
+
### Model Description
|
| 171 |
+
|
| 172 |
+
| Property | Value |
|
| 173 |
+
| --- | --- |
|
| 174 |
+
| Model type | Learned sparse retriever; Sentence Transformers `SparseEncoder` |
|
| 175 |
+
| Backbone | DenseOn / ModernBERT-base |
|
| 176 |
+
| Total parameters | 174M: 149M backbone + 25M sparse head |
|
| 177 |
+
| Maximum sequence length | 8,192 tokens, including prompts and special tokens |
|
| 178 |
+
| Output dimensionality | 32,768 learned latent features |
|
| 179 |
+
| Token representation | 768 dimensions, L2-normalized before sparse encoding |
|
| 180 |
+
| Token sparsity | Top-32 latent selection followed by ReLU |
|
| 181 |
+
| Sequence pooling | Maximum over token activations |
|
| 182 |
+
| Similarity function | Dot product |
|
| 183 |
+
| Prompts | `query: ` for queries; `document: ` for documents |
|
| 184 |
+
| Language | English |
|
| 185 |
+
|
| 186 |
+
## Usage
|
| 187 |
+
|
| 188 |
+
### Direct Usage (Sentence Transformers)
|
| 189 |
+
|
| 190 |
+
Install the supported library versions:
|
| 191 |
+
|
| 192 |
+
```bash
|
| 193 |
+
pip install -U "sentence-transformers>=6.1,<6.2" "transformers>=5.3,<6"
|
| 194 |
+
```
|
| 195 |
+
|
| 196 |
+
Load the model and encode queries and documents. This example uses the **q-64 / d-512** configuration:
|
| 197 |
+
|
| 198 |
+
```python
|
| 199 |
+
from sentence_transformers import SparseEncoder
|
| 200 |
+
|
| 201 |
+
model = SparseEncoder("lightonai/SlideOn", trust_remote_code=True)
|
| 202 |
+
|
| 203 |
+
queries = ["What causes a solar eclipse?"]
|
| 204 |
+
documents = [
|
| 205 |
+
"A solar eclipse occurs when the Moon passes between the Sun and Earth.",
|
| 206 |
+
"Wind turbines convert the kinetic energy of wind into electricity.",
|
| 207 |
+
]
|
| 208 |
+
|
| 209 |
+
query_embeddings = model.encode_query(queries, top_k=64)
|
| 210 |
+
document_embeddings = model.encode_document(documents, top_k=512)
|
| 211 |
+
|
| 212 |
+
print(query_embeddings.shape, document_embeddings.shape)
|
| 213 |
+
# torch.Size([1, 32768]) torch.Size([2, 32768])
|
| 214 |
+
|
| 215 |
+
scores = model.similarity(query_embeddings, document_embeddings)
|
| 216 |
+
print(scores)
|
| 217 |
+
```
|
| 218 |
+
|
| 219 |
+
`encode_query()` and `encode_document()` apply the appropriate prompts and preprocessing automatically. Pass the raw text without manually adding prefixes. The returned embeddings are PyTorch sparse COO tensors. The model and custom inference code are included in this repository; loading does not require the training package or PyLate.
|
| 220 |
+
|
| 221 |
+
### Choosing Query and Document Budgets
|
| 222 |
+
|
| 223 |
+
Use `top_k` independently on each encoding call to cap the number of nonzero features **after pooling**. This is separate from the per-token top-32 operation and does not change the vector dimensionality.
|
| 224 |
+
|
| 225 |
+
To reproduce the unpruned setting:
|
| 226 |
+
|
| 227 |
+
```python
|
| 228 |
+
query_embeddings = model.encode_query(queries, top_k=0)
|
| 229 |
+
document_embeddings = model.encode_document(documents, top_k=0)
|
| 230 |
+
```
|
| 231 |
+
|
| 232 |
+
Omitting `top_k` also uses the saved unpruned defaults. To use the **q-full / d-512** setting, keep the query call at `top_k=0` and set the document call to `top_k=512`.
|
| 233 |
+
|
| 234 |
+
Dimensions represent **learned latents**, so they do not map directly to tokenizer vocabulary entries. Inspect the sparse indices and weights; `SparseEncoder.decode()` cannot label them as vocabulary tokens. Retrieval uses raw dot products, so leave final vector normalization disabled.
|
| 235 |
+
|
| 236 |
+
## Citation
|
| 237 |
+
|
| 238 |
+
If you find this work useful for your research, please consider citing us:
|
| 239 |
+
|
| 240 |
+
```bibtex
|
| 241 |
+
@misc{moura2026slideon,
|
| 242 |
+
author = {Moura Junior, Paulo Roberto and Sourty, Raphaël},
|
| 243 |
+
title = {{SlideOn} \& {mSlideOn}: Sparsified late-interaction and dense retrievers},
|
| 244 |
+
year = {2026},
|
| 245 |
+
howpublished = {Hugging Face Blog},
|
| 246 |
+
url = {https://huggingface.co/blog/lightonai/slideon-mslideon}
|
| 247 |
+
}
|
| 248 |
+
```
|
config.json
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"SlideOnModel"
|
| 4 |
+
],
|
| 5 |
+
"auto_map": {
|
| 6 |
+
"AutoConfig": "modeling_slideon.SlideOnConfig",
|
| 7 |
+
"AutoModel": "modeling_slideon.SlideOnModel"
|
| 8 |
+
},
|
| 9 |
+
"backbone_config": {
|
| 10 |
+
"architectures": [
|
| 11 |
+
"ModernBertModel"
|
| 12 |
+
],
|
| 13 |
+
"attention_bias": false,
|
| 14 |
+
"attention_dropout": 0.0,
|
| 15 |
+
"bos_token_id": 50281,
|
| 16 |
+
"classifier_activation": "gelu",
|
| 17 |
+
"classifier_bias": false,
|
| 18 |
+
"classifier_dropout": 0.0,
|
| 19 |
+
"classifier_pooling": "mean",
|
| 20 |
+
"cls_token_id": 50281,
|
| 21 |
+
"decoder_bias": true,
|
| 22 |
+
"deterministic_flash_attn": false,
|
| 23 |
+
"dtype": "float32",
|
| 24 |
+
"embedding_dropout": 0.0,
|
| 25 |
+
"eos_token_id": 50282,
|
| 26 |
+
"global_attn_every_n_layers": 3,
|
| 27 |
+
"gradient_checkpointing": false,
|
| 28 |
+
"hidden_activation": "gelu",
|
| 29 |
+
"hidden_size": 768,
|
| 30 |
+
"initializer_cutoff_factor": 2.0,
|
| 31 |
+
"initializer_range": 0.02,
|
| 32 |
+
"intermediate_size": 1152,
|
| 33 |
+
"layer_norm_eps": 1e-05,
|
| 34 |
+
"layer_types": [
|
| 35 |
+
"full_attention",
|
| 36 |
+
"sliding_attention",
|
| 37 |
+
"sliding_attention",
|
| 38 |
+
"full_attention",
|
| 39 |
+
"sliding_attention",
|
| 40 |
+
"sliding_attention",
|
| 41 |
+
"full_attention",
|
| 42 |
+
"sliding_attention",
|
| 43 |
+
"sliding_attention",
|
| 44 |
+
"full_attention",
|
| 45 |
+
"sliding_attention",
|
| 46 |
+
"sliding_attention",
|
| 47 |
+
"full_attention",
|
| 48 |
+
"sliding_attention",
|
| 49 |
+
"sliding_attention",
|
| 50 |
+
"full_attention",
|
| 51 |
+
"sliding_attention",
|
| 52 |
+
"sliding_attention",
|
| 53 |
+
"full_attention",
|
| 54 |
+
"sliding_attention",
|
| 55 |
+
"sliding_attention",
|
| 56 |
+
"full_attention"
|
| 57 |
+
],
|
| 58 |
+
"local_attention": 128,
|
| 59 |
+
"max_position_embeddings": 8192,
|
| 60 |
+
"mlp_bias": false,
|
| 61 |
+
"mlp_dropout": 0.0,
|
| 62 |
+
"model_type": "modernbert",
|
| 63 |
+
"norm_bias": false,
|
| 64 |
+
"norm_eps": 1e-05,
|
| 65 |
+
"num_attention_heads": 12,
|
| 66 |
+
"num_hidden_layers": 22,
|
| 67 |
+
"pad_token_id": 50283,
|
| 68 |
+
"position_embedding_type": "absolute",
|
| 69 |
+
"repad_logits_with_grad": false,
|
| 70 |
+
"rope_parameters": {
|
| 71 |
+
"full_attention": {
|
| 72 |
+
"rope_theta": 160000.0,
|
| 73 |
+
"rope_type": "default"
|
| 74 |
+
},
|
| 75 |
+
"sliding_attention": {
|
| 76 |
+
"rope_theta": 10000.0,
|
| 77 |
+
"rope_type": "default"
|
| 78 |
+
}
|
| 79 |
+
},
|
| 80 |
+
"sep_token_id": 50282,
|
| 81 |
+
"sparse_pred_ignore_index": -100,
|
| 82 |
+
"sparse_prediction": false,
|
| 83 |
+
"tie_word_embeddings": true,
|
| 84 |
+
"vocab_size": 50368
|
| 85 |
+
},
|
| 86 |
+
"base_model": "lightonai/DenseOn",
|
| 87 |
+
"base_model_revision": "cb9947ebccb33862d24e3c7ca2edb25e51acd887",
|
| 88 |
+
"checkpoint_type": "retrieval",
|
| 89 |
+
"head_config": {
|
| 90 |
+
"d_lift": 0,
|
| 91 |
+
"d_model": 768,
|
| 92 |
+
"has_decoder": false,
|
| 93 |
+
"n_latents": 32768
|
| 94 |
+
},
|
| 95 |
+
"inference": {
|
| 96 |
+
"document_length": 8192,
|
| 97 |
+
"document_top_k": 0,
|
| 98 |
+
"head_chunk_size": 2048,
|
| 99 |
+
"pooling": "max_relu",
|
| 100 |
+
"prompts": {
|
| 101 |
+
"document": "document: ",
|
| 102 |
+
"query": "query: "
|
| 103 |
+
},
|
| 104 |
+
"query_length": 8192,
|
| 105 |
+
"query_top_k": 0,
|
| 106 |
+
"score": "dot",
|
| 107 |
+
"token_k": 32
|
| 108 |
+
},
|
| 109 |
+
"model_type": "slideon",
|
| 110 |
+
"projection_configs": [],
|
| 111 |
+
"sae_path": "lightonai/SlideOn-SAE",
|
| 112 |
+
"skip_token_ids": [
|
| 113 |
+
2,
|
| 114 |
+
3,
|
| 115 |
+
4,
|
| 116 |
+
5,
|
| 117 |
+
6,
|
| 118 |
+
7,
|
| 119 |
+
8,
|
| 120 |
+
9,
|
| 121 |
+
10,
|
| 122 |
+
11,
|
| 123 |
+
12,
|
| 124 |
+
13,
|
| 125 |
+
14,
|
| 126 |
+
15,
|
| 127 |
+
16,
|
| 128 |
+
27,
|
| 129 |
+
28,
|
| 130 |
+
29,
|
| 131 |
+
30,
|
| 132 |
+
31,
|
| 133 |
+
32,
|
| 134 |
+
33,
|
| 135 |
+
60,
|
| 136 |
+
61,
|
| 137 |
+
62,
|
| 138 |
+
63,
|
| 139 |
+
64,
|
| 140 |
+
65,
|
| 141 |
+
92,
|
| 142 |
+
93,
|
| 143 |
+
94,
|
| 144 |
+
95
|
| 145 |
+
],
|
| 146 |
+
"tokenizer_settings": {
|
| 147 |
+
"document_prefix": "",
|
| 148 |
+
"query_prefix": "",
|
| 149 |
+
"skiplist_words": [
|
| 150 |
+
"!",
|
| 151 |
+
"\"",
|
| 152 |
+
"#",
|
| 153 |
+
"$",
|
| 154 |
+
"%",
|
| 155 |
+
"&",
|
| 156 |
+
"'",
|
| 157 |
+
"(",
|
| 158 |
+
")",
|
| 159 |
+
"*",
|
| 160 |
+
"+",
|
| 161 |
+
",",
|
| 162 |
+
"-",
|
| 163 |
+
".",
|
| 164 |
+
"/",
|
| 165 |
+
":",
|
| 166 |
+
";",
|
| 167 |
+
"<",
|
| 168 |
+
"=",
|
| 169 |
+
">",
|
| 170 |
+
"?",
|
| 171 |
+
"@",
|
| 172 |
+
"[",
|
| 173 |
+
"\\",
|
| 174 |
+
"]",
|
| 175 |
+
"^",
|
| 176 |
+
"_",
|
| 177 |
+
"`",
|
| 178 |
+
"{",
|
| 179 |
+
"|",
|
| 180 |
+
"}",
|
| 181 |
+
"~"
|
| 182 |
+
]
|
| 183 |
+
},
|
| 184 |
+
"transformers_version": "5.3.0"
|
| 185 |
+
}
|
config_sentence_transformers.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_type": "SparseEncoder",
|
| 3 |
+
"__version__": {
|
| 4 |
+
"sentence_transformers": "6.1.0"
|
| 5 |
+
},
|
| 6 |
+
"prompts": {
|
| 7 |
+
"query": "query: ",
|
| 8 |
+
"document": "document: "
|
| 9 |
+
},
|
| 10 |
+
"default_prompt_name": "document",
|
| 11 |
+
"similarity_fn_name": "dot"
|
| 12 |
+
}
|
model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a06ae32e0a4736077f1997f02eb80b4460626e4ea665d60f1e5335dbb1165f6e
|
| 3 |
+
size 697000192
|
modeling_slideon.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Complete SlideOn model: Transformer, token projections, and sparse latent head."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
from torch.utils.checkpoint import checkpoint as activation_checkpoint
|
| 6 |
+
from transformers import AutoConfig, AutoModel, PretrainedConfig, PreTrainedModel
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class SlideOnConfig(PretrainedConfig):
|
| 10 |
+
model_type = "slideon"
|
| 11 |
+
sub_configs = {"backbone_config": AutoConfig}
|
| 12 |
+
|
| 13 |
+
def __init__(self, backbone_config=None, projection_configs=None, head_config=None,
|
| 14 |
+
inference=None, tokenizer_settings=None, skip_token_ids=None,
|
| 15 |
+
checkpoint_type="retrieval", **kwargs):
|
| 16 |
+
super().__init__(**kwargs)
|
| 17 |
+
if isinstance(backbone_config, dict):
|
| 18 |
+
backbone_config = dict(backbone_config)
|
| 19 |
+
model_type = backbone_config.pop("model_type", "modernbert")
|
| 20 |
+
backbone_config = AutoConfig.for_model(model_type, **backbone_config)
|
| 21 |
+
self.backbone_config = backbone_config or AutoConfig.for_model("modernbert")
|
| 22 |
+
self.projection_configs = projection_configs or []
|
| 23 |
+
self.head_config = head_config or dict(d_model=768, n_latents=32768, d_lift=0)
|
| 24 |
+
self.inference = inference or dict(
|
| 25 |
+
token_k=32, query_top_k=0, document_top_k=0,
|
| 26 |
+
query_length=8192, document_length=8192, head_chunk_size=2048,
|
| 27 |
+
prompts={"query": "", "document": ""}, pooling="max_relu", score="dot")
|
| 28 |
+
self.tokenizer_settings = tokenizer_settings or {}
|
| 29 |
+
self.skip_token_ids = skip_token_ids or []
|
| 30 |
+
self.checkpoint_type = checkpoint_type
|
| 31 |
+
if self.inference.get("pooling") != "max_relu" or self.inference.get("score") != "dot":
|
| 32 |
+
raise ValueError("SlideOn supports max_relu pooling and dot scoring")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class TokenProjection(torch.nn.Module):
|
| 36 |
+
"""The token-wise residual Dense layers used by mSlideOn."""
|
| 37 |
+
|
| 38 |
+
def __init__(self, config):
|
| 39 |
+
super().__init__()
|
| 40 |
+
self.config = config
|
| 41 |
+
self.linear = torch.nn.Linear(
|
| 42 |
+
config["in_features"], config["out_features"], bias=config["bias"]
|
| 43 |
+
)
|
| 44 |
+
self.use_residual = config.get("use_residual", False)
|
| 45 |
+
self.residual = None
|
| 46 |
+
if self.use_residual and config["in_features"] != config["out_features"]:
|
| 47 |
+
self.residual = torch.nn.Linear(
|
| 48 |
+
config["in_features"], config["out_features"], bias=False
|
| 49 |
+
)
|
| 50 |
+
if config["activation_function"] != "torch.nn.modules.linear.Identity":
|
| 51 |
+
raise ValueError("Unsupported projection activation")
|
| 52 |
+
|
| 53 |
+
def forward(self, tokens):
|
| 54 |
+
projected = self.linear(tokens)
|
| 55 |
+
if self.use_residual:
|
| 56 |
+
projected = projected + (
|
| 57 |
+
self.residual(tokens) if self.residual is not None else tokens
|
| 58 |
+
)
|
| 59 |
+
return projected
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class LatentHead(torch.nn.Module):
|
| 63 |
+
def __init__(self, config):
|
| 64 |
+
super().__init__()
|
| 65 |
+
d_model, n_latents = config["d_model"], config["n_latents"]
|
| 66 |
+
d_lift = config.get("d_lift") or 0
|
| 67 |
+
self.W_enc = torch.nn.Parameter(torch.empty(d_lift or d_model, n_latents))
|
| 68 |
+
self.b_enc = torch.nn.Parameter(torch.zeros(n_latents))
|
| 69 |
+
self.b_dec = torch.nn.Parameter(torch.zeros(d_model))
|
| 70 |
+
self.gate_relu_phi = torch.nn.Parameter(torch.ones(n_latents))
|
| 71 |
+
self.lift = None
|
| 72 |
+
if d_lift:
|
| 73 |
+
hidden = config["hidden_dim"]
|
| 74 |
+
self.lift = torch.nn.Linear(d_model, d_lift, bias=False)
|
| 75 |
+
self.mlp = torch.nn.Sequential(
|
| 76 |
+
torch.nn.Linear(d_lift, hidden), torch.nn.ReLU(),
|
| 77 |
+
torch.nn.Linear(hidden, d_lift))
|
| 78 |
+
if config.get("has_decoder", False):
|
| 79 |
+
# Preserve the pretrained SAE decoder without optimizing it in retrieval SFT.
|
| 80 |
+
self.W_dec = torch.nn.Parameter(torch.empty(n_latents, d_model), requires_grad=False)
|
| 81 |
+
self.register_buffer("steps_since_fired", torch.zeros(n_latents, dtype=torch.long))
|
| 82 |
+
|
| 83 |
+
def forward(self, tokens):
|
| 84 |
+
centered = tokens - self.b_dec
|
| 85 |
+
if self.lift is not None:
|
| 86 |
+
lifted = self.lift(centered)
|
| 87 |
+
centered = lifted + self.mlp(lifted)
|
| 88 |
+
return centered @ self.W_enc + self.b_enc
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
class SlideOnModel(PreTrainedModel):
|
| 92 |
+
config_class = SlideOnConfig
|
| 93 |
+
base_model_prefix = "backbone"
|
| 94 |
+
_supports_sdpa = True
|
| 95 |
+
_supports_flash_attn = True
|
| 96 |
+
_no_split_modules = ["ModernBertEncoderLayer", "LatentHead", "TokenProjection"]
|
| 97 |
+
_keep_in_fp32_modules_strict = ["head"]
|
| 98 |
+
|
| 99 |
+
def __init__(self, config):
|
| 100 |
+
super().__init__(config)
|
| 101 |
+
config.backbone_config._attn_implementation = config._attn_implementation
|
| 102 |
+
self.backbone = AutoModel.from_config(config.backbone_config)
|
| 103 |
+
self.projections = torch.nn.ModuleList(
|
| 104 |
+
TokenProjection(item) for item in config.projection_configs)
|
| 105 |
+
self.head = LatentHead(config.head_config)
|
| 106 |
+
self.settings = config.inference
|
| 107 |
+
self.post_init()
|
| 108 |
+
|
| 109 |
+
def _init_weights(self, module):
|
| 110 |
+
if isinstance(module, LatentHead):
|
| 111 |
+
torch.nn.init.normal_(module.W_enc, std=0.02)
|
| 112 |
+
if hasattr(module, "W_dec"):
|
| 113 |
+
torch.nn.init.normal_(module.W_dec, std=0.02)
|
| 114 |
+
else:
|
| 115 |
+
super()._init_weights(module)
|
| 116 |
+
|
| 117 |
+
@classmethod
|
| 118 |
+
def from_pretrained(cls, *args, **kwargs):
|
| 119 |
+
result = super().from_pretrained(*args, **kwargs)
|
| 120 |
+
model = result[0] if isinstance(result, tuple) else result
|
| 121 |
+
# Match the original PyLate encoder's RoPE buffer precision.
|
| 122 |
+
model.backbone.to(dtype=model.backbone.dtype)
|
| 123 |
+
return result
|
| 124 |
+
|
| 125 |
+
def get_embedding_dimension(self):
|
| 126 |
+
return self.config.head_config["n_latents"]
|
| 127 |
+
|
| 128 |
+
@staticmethod
|
| 129 |
+
def _task(task):
|
| 130 |
+
task = task or "document"
|
| 131 |
+
if task not in ("query", "document"):
|
| 132 |
+
raise ValueError("task must be 'query' or 'document'")
|
| 133 |
+
return task
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def forward(self, input_ids, attention_mask, token_type_ids=None, task=None,
|
| 137 |
+
slideon_task=None, top_k=None, token_k=None, **kwargs):
|
| 138 |
+
features = {"input_ids": input_ids, "attention_mask": attention_mask}
|
| 139 |
+
if slideon_task is not None or task is not None:
|
| 140 |
+
features["slideon_task"] = slideon_task or task
|
| 141 |
+
if token_type_ids is not None:
|
| 142 |
+
features["token_type_ids"] = token_type_ids
|
| 143 |
+
task = self._task(task or features.get("slideon_task"))
|
| 144 |
+
if features.get("slideon_task", task) != task:
|
| 145 |
+
raise ValueError("Preprocessing and forward tasks must match")
|
| 146 |
+
n_latents = self.get_embedding_dimension()
|
| 147 |
+
cap = self.settings[task + "_top_k"] if top_k is None else top_k
|
| 148 |
+
k = self.settings["token_k"] if token_k is None else token_k
|
| 149 |
+
if not isinstance(cap, int) or not 0 <= cap <= n_latents:
|
| 150 |
+
raise ValueError(f"top_k must be an integer between 0 and {n_latents}")
|
| 151 |
+
if not isinstance(k, int) or not 1 <= k <= n_latents:
|
| 152 |
+
raise ValueError(f"token_k must be an integer between 1 and {n_latents}")
|
| 153 |
+
model_inputs = {
|
| 154 |
+
key: features[key]
|
| 155 |
+
for key in ("input_ids", "attention_mask", "token_type_ids")
|
| 156 |
+
if key in features
|
| 157 |
+
}
|
| 158 |
+
tokens = self.backbone(**model_inputs).last_hidden_state
|
| 159 |
+
for projection in self.projections:
|
| 160 |
+
tokens = projection(tokens)
|
| 161 |
+
mask = features["attention_mask"].bool()
|
| 162 |
+
if task == "document" and self.config.skip_token_ids:
|
| 163 |
+
skip_ids = torch.tensor(self.config.skip_token_ids, device=input_ids.device)
|
| 164 |
+
mask = mask & ~torch.isin(input_ids, skip_ids)
|
| 165 |
+
owners = torch.arange(tokens.shape[0], device=tokens.device)[:, None].expand_as(
|
| 166 |
+
mask
|
| 167 |
+
)[mask]
|
| 168 |
+
tokens = torch.nn.functional.normalize(tokens[mask], p=2, dim=-1)
|
| 169 |
+
# Match the evaluation loader's FP16 token transfer before head pooling.
|
| 170 |
+
if not self.training:
|
| 171 |
+
tokens = tokens.half().float()
|
| 172 |
+
probability_sum = tokens.new_zeros(n_latents, dtype=torch.float32)
|
| 173 |
+
usage_sum = tokens.new_zeros(n_latents, dtype=torch.float32)
|
| 174 |
+
pooled = torch.zeros(
|
| 175 |
+
mask.shape[0] * n_latents, dtype=torch.float32, device=tokens.device
|
| 176 |
+
)
|
| 177 |
+
for start in range(0, tokens.shape[0], self.settings["head_chunk_size"]):
|
| 178 |
+
end = start + self.settings["head_chunk_size"]
|
| 179 |
+
# The reference evaluates the head under BF16 autocast on CUDA.
|
| 180 |
+
def head_chunk(chunk):
|
| 181 |
+
with torch.autocast(chunk.device.type, dtype=torch.bfloat16, enabled=chunk.is_cuda):
|
| 182 |
+
activations = self.head(chunk)
|
| 183 |
+
values, indices = activations.topk(k, dim=-1)
|
| 184 |
+
probabilities = activations.float().softmax(-1).sum(0) if self.training else chunk.new_zeros(n_latents)
|
| 185 |
+
return values, indices, probabilities
|
| 186 |
+
if self.training and torch.is_grad_enabled():
|
| 187 |
+
values, indices, probabilities = activation_checkpoint(head_chunk, tokens[start:end], use_reentrant=False)
|
| 188 |
+
else:
|
| 189 |
+
values, indices, probabilities = head_chunk(tokens[start:end])
|
| 190 |
+
if self.training:
|
| 191 |
+
probability_sum = probability_sum + probabilities
|
| 192 |
+
usage_sum = usage_sum + torch.bincount(indices.flatten(), minlength=n_latents)
|
| 193 |
+
values = values.float().relu()
|
| 194 |
+
indices = owners[start:end, None] * n_latents + indices
|
| 195 |
+
pooled = pooled.scatter_reduce(
|
| 196 |
+
0, indices.flatten(), values.flatten(), reduce="amax", include_self=True
|
| 197 |
+
)
|
| 198 |
+
pooled = (
|
| 199 |
+
pooled.view(mask.shape[0], n_latents)
|
| 200 |
+
* self.head.gate_relu_phi.float().relu()
|
| 201 |
+
)
|
| 202 |
+
if cap:
|
| 203 |
+
values, indices = pooled.topk(cap, dim=-1)
|
| 204 |
+
pooled = torch.zeros_like(pooled).scatter(1, indices, values)
|
| 205 |
+
# The reference stores retrieval weights in FP16; retain that rounding.
|
| 206 |
+
features["sentence_embedding"] = pooled if self.training else pooled.half().float()
|
| 207 |
+
if self.training:
|
| 208 |
+
count = max(tokens.shape[0], 1)
|
| 209 |
+
features["load_balance"] = n_latents * (
|
| 210 |
+
probability_sum / count * usage_sum.detach() / (count * k)
|
| 211 |
+
).sum()
|
| 212 |
+
return {key: features[key] for key in ("sentence_embedding", "load_balance") if key in features}
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
SlideOnConfig.register_for_auto_class()
|
| 216 |
+
SlideOnModel.register_for_auto_class("AutoModel")
|
modules.json
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"idx": 0,
|
| 4 |
+
"name": "0",
|
| 5 |
+
"path": "",
|
| 6 |
+
"type": "slideon.SlideOn"
|
| 7 |
+
}
|
| 8 |
+
]
|
slideon.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sentence Transformers adapter for the complete, flat SlideOn checkpoint."""
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
from sentence_transformers.base.modules import InputModule
|
| 6 |
+
from transformers import AutoConfig, AutoModel, AutoTokenizer
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class SlideOn(InputModule):
|
| 10 |
+
config_file_name = "config.json"
|
| 11 |
+
forward_kwargs = {"task", "top_k", "token_k"}
|
| 12 |
+
save_in_root = True
|
| 13 |
+
|
| 14 |
+
def __init__(self, checkpoint, model_kwargs=None, processor_kwargs=None,
|
| 15 |
+
config_kwargs=None, **hub_kwargs):
|
| 16 |
+
super().__init__()
|
| 17 |
+
config = AutoConfig.from_pretrained(
|
| 18 |
+
checkpoint, trust_remote_code=True, **hub_kwargs, **(config_kwargs or {}))
|
| 19 |
+
if config.model_type != "slideon":
|
| 20 |
+
raise ValueError("Expected a flat SlideOn checkpoint; use slideon.export for legacy checkpoints")
|
| 21 |
+
self.model = AutoModel.from_pretrained(
|
| 22 |
+
checkpoint, config=config, trust_remote_code=True,
|
| 23 |
+
**hub_kwargs, **(model_kwargs or {}))
|
| 24 |
+
self.tokenizer = AutoTokenizer.from_pretrained(
|
| 25 |
+
checkpoint, trust_remote_code=True, **hub_kwargs, **(processor_kwargs or {}))
|
| 26 |
+
if self.tokenizer.mask_token_id is not None:
|
| 27 |
+
self.tokenizer.pad_token_id = self.tokenizer.mask_token_id
|
| 28 |
+
elif self.tokenizer.eos_token_id is not None:
|
| 29 |
+
self.tokenizer.pad_token_id = self.tokenizer.eos_token_id
|
| 30 |
+
self.settings = self.model.config.inference
|
| 31 |
+
self.encoder_config = self.model.config.tokenizer_settings
|
| 32 |
+
self.max_seq_length = max(self.settings["query_length"], self.settings["document_length"])
|
| 33 |
+
|
| 34 |
+
def get_embedding_dimension(self):
|
| 35 |
+
return self.model.get_embedding_dimension()
|
| 36 |
+
|
| 37 |
+
@staticmethod
|
| 38 |
+
def _task(task):
|
| 39 |
+
task = task or "document"
|
| 40 |
+
if task not in ("query", "document"):
|
| 41 |
+
raise ValueError("task must be 'query' or 'document'")
|
| 42 |
+
return task
|
| 43 |
+
|
| 44 |
+
def preprocess(
|
| 45 |
+
self, inputs, prompt=None, task=None, processing_kwargs=None, **kwargs
|
| 46 |
+
):
|
| 47 |
+
task = self._task(task)
|
| 48 |
+
if not all(isinstance(text, str) for text in inputs):
|
| 49 |
+
raise TypeError("SlideOn accepts text strings only")
|
| 50 |
+
if prompt is None:
|
| 51 |
+
prompt = self.settings["prompts"][task]
|
| 52 |
+
texts = [(prompt + text).strip() for text in inputs]
|
| 53 |
+
prefix = self.encoder_config.get(task + "_prefix", "")
|
| 54 |
+
length = min(self.max_seq_length, self.settings[task + "_length"])
|
| 55 |
+
options = dict(
|
| 56 |
+
padding=True,
|
| 57 |
+
truncation=True,
|
| 58 |
+
max_length=length - bool(prefix),
|
| 59 |
+
return_tensors="pt",
|
| 60 |
+
)
|
| 61 |
+
options.update(processing_kwargs or {})
|
| 62 |
+
features = dict(self.tokenizer(texts, **options))
|
| 63 |
+
if prefix:
|
| 64 |
+
prefix_id = self.tokenizer.convert_tokens_to_ids(prefix)
|
| 65 |
+
for name, value in list(features.items()):
|
| 66 |
+
fill = (
|
| 67 |
+
prefix_id
|
| 68 |
+
if name == "input_ids"
|
| 69 |
+
else (1 if name == "attention_mask" else 0)
|
| 70 |
+
)
|
| 71 |
+
marker = torch.full(
|
| 72 |
+
(value.shape[0], 1), fill, dtype=value.dtype, device=value.device
|
| 73 |
+
)
|
| 74 |
+
features[name] = torch.cat((value[:, :1], marker, value[:, 1:]), dim=1)
|
| 75 |
+
features["slideon_task"] = task
|
| 76 |
+
return features
|
| 77 |
+
|
| 78 |
+
def forward(self, features, task=None, top_k=None, token_k=None, **kwargs):
|
| 79 |
+
inputs = {key: features[key] for key in
|
| 80 |
+
("input_ids", "attention_mask", "token_type_ids", "slideon_task") if key in features}
|
| 81 |
+
features.update(self.model(**inputs, task=task, top_k=top_k, token_k=token_k))
|
| 82 |
+
return features
|
| 83 |
+
|
| 84 |
+
def save(self, output_path, safe_serialization=True, **kwargs):
|
| 85 |
+
self.model.save_pretrained(output_path, safe_serialization=safe_serialization)
|
| 86 |
+
self.tokenizer.save_pretrained(output_path)
|
| 87 |
+
|
| 88 |
+
@classmethod
|
| 89 |
+
def load(cls, model_name_or_path, subfolder="", token=None, cache_folder=None,
|
| 90 |
+
revision=None, local_files_only=False, model_kwargs=None,
|
| 91 |
+
processor_kwargs=None, config_kwargs=None, backend="torch", **kwargs):
|
| 92 |
+
if backend != "torch":
|
| 93 |
+
raise ValueError("SlideOn currently supports the torch backend only")
|
| 94 |
+
return cls(model_name_or_path, model_kwargs, processor_kwargs, config_kwargs,
|
| 95 |
+
subfolder=subfolder, token=token, cache_dir=cache_folder,
|
| 96 |
+
revision=revision, local_files_only=local_files_only)
|
tokenizer.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
tokenizer_config.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"backend": "tokenizers",
|
| 3 |
+
"clean_up_tokenization_spaces": true,
|
| 4 |
+
"cls_token": "[CLS]",
|
| 5 |
+
"is_local": true,
|
| 6 |
+
"mask_token": "[MASK]",
|
| 7 |
+
"max_length": 512,
|
| 8 |
+
"model_input_names": [
|
| 9 |
+
"input_ids",
|
| 10 |
+
"attention_mask"
|
| 11 |
+
],
|
| 12 |
+
"model_max_length": 512,
|
| 13 |
+
"pad_to_multiple_of": null,
|
| 14 |
+
"pad_token": "[MASK]",
|
| 15 |
+
"pad_token_type_id": 0,
|
| 16 |
+
"padding_side": "right",
|
| 17 |
+
"sep_token": "[SEP]",
|
| 18 |
+
"stride": 0,
|
| 19 |
+
"tokenizer_class": "TokenizersBackend",
|
| 20 |
+
"truncation_side": "right",
|
| 21 |
+
"truncation_strategy": "longest_first",
|
| 22 |
+
"unk_token": "[UNK]"
|
| 23 |
+
}
|