paulomouraj commited on
Commit
9372da2
·
0 Parent(s):

Initial release

Browse files
.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,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ library_name: sentence-transformers
3
+ pipeline_tag: feature-extraction
4
+ base_model: lightonai/DenseOn
5
+ language:
6
+ - en
7
+ datasets:
8
+ - lightonai/embeddings-fine-tuning-filtered-en
9
+ tags:
10
+ - sentence-transformers
11
+ - custom-code
12
+ - sparse-autoencoder
13
+ - sparse-retrieval
14
+ ---
15
+ <div align="center">
16
+ <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;">
17
+
18
+ [![Website](https://img.shields.io/badge/LightOn-Website-blue?logo=google-chrome)](https://lighton.ai)
19
+ [![LinkedIn](https://img.shields.io/badge/LightOn-LinkedIn-0A66C2?logo=linkedin)](https://www.linkedin.com/company/lighton/)
20
+ [![X](https://img.shields.io/badge/@LightOnIO-X-black?logo=x)](https://x.com/LightOnIO)
21
+
22
+ [SlideOn](https://huggingface.co/lightonai/SlideOn) | [mSlideOn](https://huggingface.co/lightonai/mSlideOn) | 📚 [Collection](https://huggingface.co/collections/lightonai/slideon-and-mslideon)
23
+
24
+ 📝 [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-SAE</h1>
29
+ <h3 align="center">English Sparse Autoencoder by LightOn</h3>
30
+
31
+ > 🎯 **TL;DR**: The pretrained sparse autoencoder used to initialize **[SlideOn](https://huggingface.co/lightonai/SlideOn)**. It learns **32,768 latent features** from frozen DenseOn token representations and provides a starting point for supervised sparse retrieval fine-tuning. Use the final SlideOn checkpoint for the retrieval results reported in our blog.
32
+
33
+ ## Model Details
34
+
35
+ SlideOn-SAE maps L2-normalized DenseOn token representations to their top-32 latent activations, followed by ReLU. Its decoder reconstructs the original token representations during SAE pretraining. For retrieval, the included adapter max-pools token activations into one sparse vector per query or document.
36
+
37
+ | Property | Value |
38
+ | --- | --- |
39
+ | Backbone | [DenseOn](https://huggingface.co/lightonai/DenseOn) / ModernBERT-base |
40
+ | Training stage | SAE pretraining, before supervised retrieval fine-tuning |
41
+ | Token representation | 768 dimensions, L2-normalized before sparse encoding |
42
+ | Latent dimensionality | 32,768 learned features |
43
+ | Token sparsity | Top-32 latent selection followed by ReLU |
44
+ | Sequence pooling | Maximum over token activations |
45
+ | Similarity function | Dot product |
46
+ | Language | English |
47
+
48
+ The checkpoint includes the DenseOn backbone, SAE encoder, and original SAE decoder in a single `model.safetensors` file. The model and tokenizer load directly from this repository.
49
+
50
+ ## Training Data and Recipe
51
+
52
+ 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). Training code and the recorded training configuration are available in the [code repository](https://github.com/lightonai/slideon-mslideon/).
53
+
54
+ ## Usage
55
+
56
+ Install the supported library versions:
57
+
58
+ ```bash
59
+ pip install -U "sentence-transformers>=6.1,<6.2" "transformers>=5.3,<6"
60
+ ```
61
+
62
+ Load the SAE initialization and encode text into sparse vectors:
63
+
64
+ ```python
65
+ from sentence_transformers import SparseEncoder
66
+
67
+ model = SparseEncoder("lightonai/SlideOn-SAE", trust_remote_code=True)
68
+
69
+ query_embeddings = model.encode_query(["What is sparse retrieval?"])
70
+ document_embeddings = model.encode_document([
71
+ "Sparse retrieval uses inverted indexes to search sparse vectors."
72
+ ])
73
+
74
+ print(query_embeddings.shape, document_embeddings.shape)
75
+ # torch.Size([1, 32768]) torch.Size([1, 32768])
76
+ ```
77
+
78
+ The model loaded above can also be fine-tuned with Sentence Transformers' `SparseEncoderTrainer`. Our [code repository](https://github.com/lightonai/slideon-mslideon/) provides the custom retrieval loss and training scripts used for our fine-tuning recipe.
79
+
80
+ ## Related Checkpoints
81
+
82
+ | Model | Description |
83
+ | --- | --- |
84
+ | **[SlideOn-SAE](https://huggingface.co/lightonai/SlideOn-SAE)** *(this model)* | DenseOn-based SAE initialization |
85
+ | [SlideOn](https://huggingface.co/lightonai/SlideOn) | English sparse retriever after supervised fine-tuning |
86
+ | [mSlideOn-SAE](https://huggingface.co/lightonai/mSlideOn-SAE) | mLateOn-based multilingual SAE initialization |
87
+ | [mSlideOn](https://huggingface.co/lightonai/mSlideOn) | Multilingual sparse retriever after supervised fine-tuning |
88
+
89
+ ## Citation
90
+
91
+ ```bibtex
92
+ @misc{moura2026slideon,
93
+ author = {Moura Junior, Paulo Roberto and Sourty, Raphaël},
94
+ title = {{SlideOn} \& {mSlideOn}: Sparsified late-interaction and dense retrievers},
95
+ year = {2026},
96
+ howpublished = {Hugging Face Blog},
97
+ url = {https://huggingface.co/blog/lightonai/slideon-mslideon}
98
+ }
99
+ ```
config.json ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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": "sae",
89
+ "head_config": {
90
+ "d_lift": 0,
91
+ "d_model": 768,
92
+ "has_decoder": true,
93
+ "n_latents": 32768
94
+ },
95
+ "inference": {
96
+ "document_length": 8192,
97
+ "document_top_k": 0,
98
+ "head_chunk_size": 256,
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_revision": "669feba609eeb5d4ee59ef791679c7e393092677",
112
+ "sae_source": "lightonai/SlideOn-SAE",
113
+ "skip_token_ids": [],
114
+ "tokenizer_settings": {
115
+ "document_prefix": "",
116
+ "query_prefix": "",
117
+ "skiplist_words": []
118
+ },
119
+ "transformers_version": "5.3.0"
120
+ }
config_sentence_transformers.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "__version__": {
3
+ "pytorch": "2.14.1+cu130",
4
+ "sentence_transformers": "6.1.0",
5
+ "transformers": "5.19.0"
6
+ },
7
+ "default_prompt_name": null,
8
+ "model_type": "SparseEncoder",
9
+ "prompts": {
10
+ "document": "",
11
+ "query": ""
12
+ },
13
+ "similarity_fn_name": "dot"
14
+ }
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:81dde50d52aeadad52af09af3bb28905ff3c6c5c7b8a26421ca0e7b4a0ec5d6d
3
+ size 797925808
modeling_slideon.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # Loading tensors can reset the decoder's requires_grad flag.
124
+ if hasattr(model.head, "W_dec"):
125
+ model.head.W_dec.requires_grad_(False)
126
+ return result
127
+
128
+ def get_embedding_dimension(self):
129
+ return self.config.head_config["n_latents"]
130
+
131
+ @staticmethod
132
+ def _task(task):
133
+ task = task or "document"
134
+ if task not in ("query", "document"):
135
+ raise ValueError("task must be 'query' or 'document'")
136
+ return task
137
+
138
+
139
+ def forward(self, input_ids, attention_mask, token_type_ids=None, task=None,
140
+ slideon_task=None, top_k=None, token_k=None, **kwargs):
141
+ features = {"input_ids": input_ids, "attention_mask": attention_mask}
142
+ if slideon_task is not None or task is not None:
143
+ features["slideon_task"] = slideon_task or task
144
+ if token_type_ids is not None:
145
+ features["token_type_ids"] = token_type_ids
146
+ task = self._task(task or features.get("slideon_task"))
147
+ if features.get("slideon_task", task) != task:
148
+ raise ValueError("Preprocessing and forward tasks must match")
149
+ n_latents = self.get_embedding_dimension()
150
+ cap = self.settings[task + "_top_k"] if top_k is None else top_k
151
+ k = self.settings["token_k"] if token_k is None else token_k
152
+ if not isinstance(cap, int) or not 0 <= cap <= n_latents:
153
+ raise ValueError(f"top_k must be an integer between 0 and {n_latents}")
154
+ if not isinstance(k, int) or not 1 <= k <= n_latents:
155
+ raise ValueError(f"token_k must be an integer between 1 and {n_latents}")
156
+ model_inputs = {
157
+ key: features[key]
158
+ for key in ("input_ids", "attention_mask", "token_type_ids")
159
+ if key in features
160
+ }
161
+ tokens = self.backbone(**model_inputs).last_hidden_state
162
+ for projection in self.projections:
163
+ tokens = projection(tokens)
164
+ mask = features["attention_mask"].bool()
165
+ if task == "document" and self.config.skip_token_ids:
166
+ skip_ids = torch.tensor(self.config.skip_token_ids, device=input_ids.device)
167
+ mask = mask & ~torch.isin(input_ids, skip_ids)
168
+ owners = torch.arange(tokens.shape[0], device=tokens.device)[:, None].expand_as(
169
+ mask
170
+ )[mask]
171
+ tokens = torch.nn.functional.normalize(tokens[mask], p=2, dim=-1)
172
+ # Match the evaluation loader's FP16 token transfer before head pooling.
173
+ if not self.training:
174
+ tokens = tokens.half().float()
175
+ probability_sum = tokens.new_zeros(n_latents, dtype=torch.float32)
176
+ usage_sum = tokens.new_zeros(n_latents, dtype=torch.float32)
177
+ pooled = torch.zeros(
178
+ mask.shape[0] * n_latents, dtype=torch.float32, device=tokens.device
179
+ )
180
+ for start in range(0, tokens.shape[0], self.settings["head_chunk_size"]):
181
+ end = start + self.settings["head_chunk_size"]
182
+ # The reference evaluates the head under BF16 autocast on CUDA.
183
+ def head_chunk(chunk):
184
+ with torch.autocast(chunk.device.type, dtype=torch.bfloat16, enabled=chunk.is_cuda):
185
+ activations = self.head(chunk)
186
+ values, indices = activations.topk(k, dim=-1)
187
+ probabilities = activations.float().softmax(-1).sum(0) if self.training else chunk.new_zeros(n_latents)
188
+ return values, indices, probabilities
189
+ if self.training and torch.is_grad_enabled():
190
+ values, indices, probabilities = activation_checkpoint(head_chunk, tokens[start:end], use_reentrant=False)
191
+ else:
192
+ values, indices, probabilities = head_chunk(tokens[start:end])
193
+ if self.training:
194
+ probability_sum = probability_sum + probabilities
195
+ usage_sum = usage_sum + torch.bincount(indices.flatten(), minlength=n_latents)
196
+ values = values.float().relu()
197
+ indices = owners[start:end, None] * n_latents + indices
198
+ pooled = pooled.scatter_reduce(
199
+ 0, indices.flatten(), values.flatten(), reduce="amax", include_self=True
200
+ )
201
+ pooled = (
202
+ pooled.view(mask.shape[0], n_latents)
203
+ * self.head.gate_relu_phi.float().relu()
204
+ )
205
+ if cap:
206
+ values, indices = pooled.topk(cap, dim=-1)
207
+ pooled = torch.zeros_like(pooled).scatter(1, indices, values)
208
+ # The reference stores retrieval weights in FP16; retain that rounding.
209
+ features["sentence_embedding"] = pooled if self.training else pooled.half().float()
210
+ if self.training:
211
+ count = max(tokens.shape[0], 1)
212
+ features["load_balance"] = n_latents * (
213
+ probability_sum / count * usage_sum.detach() / (count * k)
214
+ ).sum()
215
+ return {key: features[key] for key in ("sentence_embedding", "load_balance") if key in features}
216
+
217
+
218
+ SlideOnConfig.register_for_auto_class()
219
+ 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,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "backend": "tokenizers",
3
+ "clean_up_tokenization_spaces": true,
4
+ "cls_token": "[CLS]",
5
+ "is_local": true,
6
+ "local_files_only": false,
7
+ "mask_token": "[MASK]",
8
+ "max_length": 512,
9
+ "model_input_names": [
10
+ "input_ids",
11
+ "attention_mask"
12
+ ],
13
+ "model_max_length": 512,
14
+ "pad_to_multiple_of": null,
15
+ "pad_token": "[MASK]",
16
+ "pad_token_type_id": 0,
17
+ "padding_side": "right",
18
+ "sep_token": "[SEP]",
19
+ "stride": 0,
20
+ "tokenizer_class": "TokenizersBackend",
21
+ "truncation_side": "right",
22
+ "truncation_strategy": "longest_first",
23
+ "unk_token": "[UNK]"
24
+ }