Layasaran commited on
Commit
8b90eb3
·
verified ·
1 Parent(s): 1cec9b6

Add new model.

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* 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
 
 
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
36
+ tokenizer.json filter=lfs diff=lfs merge=lfs -text
1_Pooling/config.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "embedding_dimension": 1024,
3
+ "pooling_mode": "mean",
4
+ "include_prompt": true
5
+ }
2_FlexibleQuantizer/st_quantize.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import numpy as np
3
+ from typing import Literal
4
+ from sentence_transformers.models import Module
5
+
6
+
7
+ class Quantizer(torch.nn.Module):
8
+ def __init__(self, hard: bool = True):
9
+ """
10
+ Args:
11
+ hard: Whether to use hard or soft quantization. Defaults to True.
12
+ """
13
+ super().__init__()
14
+ self._hard = hard
15
+
16
+ def _hard_quantize(self, x, *args, **kwargs) -> torch.Tensor:
17
+ raise NotImplementedError
18
+
19
+ def _soft_quantize(self, x, *args, **kwargs) -> torch.Tensor:
20
+ raise NotImplementedError
21
+
22
+ def forward(self, x, *args, **kwargs) -> torch.Tensor:
23
+ soft = self._soft_quantize(x, *args, **kwargs)
24
+
25
+ if not self._hard:
26
+ result = soft
27
+ else:
28
+ result = (
29
+ self._hard_quantize(x, *args, **kwargs).detach() + soft - soft.detach()
30
+ )
31
+
32
+ return result
33
+
34
+
35
+ class Int8TanhQuantizer(Quantizer):
36
+ def __init__(
37
+ self,
38
+ hard: bool = True,
39
+ ):
40
+ super().__init__(hard=hard)
41
+ self.qmin = -128
42
+ self.qmax = 127
43
+
44
+ def _soft_quantize(self, x, *args, **kwargs):
45
+ return torch.tanh(x)
46
+
47
+ def _hard_quantize(self, x, *args, **kwargs):
48
+ soft = self._soft_quantize(x)
49
+ int_x = torch.round(soft * self.qmax)
50
+ int_x = torch.clamp(int_x, self.qmin, self.qmax)
51
+ return int_x
52
+
53
+
54
+ class BinaryTanhQuantizer(Quantizer):
55
+ def __init__(
56
+ self,
57
+ hard: bool = True,
58
+ scale: float = 1.0,
59
+ ):
60
+ super().__init__(hard)
61
+ self._scale = scale
62
+
63
+ def _soft_quantize(self, x, *args, **kwargs):
64
+ return torch.tanh(self._scale * x)
65
+
66
+ def _hard_quantize(self, x, *args, **kwargs):
67
+ return torch.where(x >= 0, 1.0, -1.0)
68
+
69
+
70
+ class PackedBinaryQuantizer:
71
+ """
72
+ Packs binary embeddings into uint8 format for efficient storage.
73
+
74
+ This quantizer applies a binary threshold (x >= 0) and packs 8 consecutive
75
+ bits into a single uint8 byte using numpy.packbits. This reduces memory
76
+ usage by 8x compared to float32 and by 4x compared to int8.
77
+
78
+ IMPORTANT: This is an inference-only quantizer - it is not differentiable
79
+ and should only be used for encoding/inference, not during training.
80
+
81
+ Args:
82
+ x: Input tensor of any float dtype, shape (..., embedding_dim)
83
+
84
+ Returns:
85
+ Packed binary tensor of dtype uint8, shape (..., embedding_dim // 8)
86
+
87
+ Example:
88
+ >>> quantizer = PackedBinaryQuantizer()
89
+ >>> embeddings = torch.randn(2, 1024) # float32
90
+ >>> packed = quantizer(embeddings) # uint8, shape (2, 128)
91
+ """
92
+ def __call__(self, x: torch.Tensor) -> torch.Tensor:
93
+ bits = np.where(x.cpu().numpy() >= 0, True, False)
94
+ packed = np.packbits(bits, axis=-1)
95
+ return torch.from_numpy(packed).to(x.device)
96
+
97
+
98
+ class FlexibleQuantizer(Module):
99
+ def __init__(self):
100
+ super().__init__()
101
+ self._int8_quantizer = Int8TanhQuantizer()
102
+ self._binary_quantizer = BinaryTanhQuantizer()
103
+ self._packed_binary_quantizer = PackedBinaryQuantizer()
104
+
105
+ def forward(
106
+ self,
107
+ features: dict[str, torch.Tensor],
108
+ quantization: Literal["int8", "binary", "ubinary"] = "int8",
109
+ **kwargs,
110
+ ) -> dict[str, torch.Tensor]:
111
+ if quantization == "int8":
112
+ features["sentence_embedding"] = self._int8_quantizer(
113
+ features["sentence_embedding"]
114
+ )
115
+ elif quantization == "binary":
116
+ features["sentence_embedding"] = self._binary_quantizer(
117
+ features["sentence_embedding"]
118
+ )
119
+ elif quantization == "ubinary":
120
+ features["sentence_embedding"] = self._packed_binary_quantizer(
121
+ features["sentence_embedding"]
122
+ )
123
+ else:
124
+ raise ValueError(
125
+ f"Invalid quantization type: {quantization}. Must be 'binary', 'ubinary', or 'int8'."
126
+ )
127
+ return features
128
+
129
+ @classmethod
130
+ def load(
131
+ cls,
132
+ model_name_or_path: str,
133
+ subfolder: str = "",
134
+ token: bool | str | None = None,
135
+ cache_folder: str | None = None,
136
+ revision: str | None = None,
137
+ local_files_only: bool = False,
138
+ **kwargs,
139
+ ):
140
+ return cls()
141
+
142
+ def save(self, output_path: str, *args, **kwargs) -> None:
143
+ return
config.json ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "PPLXQwen3ContextualModel"
4
+ ],
5
+ "attention_bias": false,
6
+ "attention_dropout": 0.0,
7
+ "auto_map": {
8
+ "AutoConfig": "configuration.PPLXQwen3Config",
9
+ "AutoModel": "modeling.PPLXQwen3ContextualModel"
10
+ },
11
+ "bos_token_id": 151643,
12
+ "dtype": "float32",
13
+ "eos_token_id": 151643,
14
+ "head_dim": 128,
15
+ "hidden_act": "silu",
16
+ "hidden_size": 1024,
17
+ "initializer_range": 0.02,
18
+ "intermediate_size": 3072,
19
+ "layer_types": [
20
+ "full_attention",
21
+ "full_attention",
22
+ "full_attention",
23
+ "full_attention",
24
+ "full_attention",
25
+ "full_attention",
26
+ "full_attention",
27
+ "full_attention",
28
+ "full_attention",
29
+ "full_attention",
30
+ "full_attention",
31
+ "full_attention",
32
+ "full_attention",
33
+ "full_attention",
34
+ "full_attention",
35
+ "full_attention",
36
+ "full_attention",
37
+ "full_attention",
38
+ "full_attention",
39
+ "full_attention",
40
+ "full_attention",
41
+ "full_attention",
42
+ "full_attention",
43
+ "full_attention",
44
+ "full_attention",
45
+ "full_attention",
46
+ "full_attention",
47
+ "full_attention"
48
+ ],
49
+ "max_position_embeddings": 32768,
50
+ "max_window_layers": 28,
51
+ "model_type": "bidirectional_pplx_qwen3",
52
+ "num_attention_heads": 16,
53
+ "num_hidden_layers": 28,
54
+ "num_key_value_heads": 8,
55
+ "pad_token_id": null,
56
+ "rms_norm_eps": 1e-06,
57
+ "rope_parameters": {
58
+ "rope_theta": 1000000,
59
+ "rope_type": "default"
60
+ },
61
+ "sliding_window": null,
62
+ "tie_word_embeddings": true,
63
+ "transformers_version": "5.13.1",
64
+ "use_bidirectional_attention": true,
65
+ "use_cache": false,
66
+ "use_sliding_window": false,
67
+ "vocab_size": 151936
68
+ }
config_sentence_transformers.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "__version__": {
3
+ "pytorch": "2.11.0+cu128",
4
+ "sentence_transformers": "5.6.0",
5
+ "transformers": "5.13.1"
6
+ },
7
+ "default_prompt_name": null,
8
+ "model_type": "SentenceTransformer",
9
+ "prompts": {
10
+ "document": "",
11
+ "query": ""
12
+ },
13
+ "similarity_fn_name": "cosine"
14
+ }
configuration.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from transformers.models.qwen3.configuration_qwen3 import Qwen3Config
2
+
3
+
4
+ class PPLXQwen3Config(Qwen3Config):
5
+ model_type = "bidirectional_pplx_qwen3"
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dba2f2e1818c61012c053468480a54d52bfae47641e8dfcbcf02715e763a4486
3
+ size 2384233112
modeling.py ADDED
@@ -0,0 +1,353 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import inspect
2
+ from typing import Callable, Literal
3
+ import numpy as np
4
+ import torch
5
+ from transformers import Qwen3Model
6
+ from transformers.cache_utils import Cache
7
+ from transformers.masking_utils import create_causal_mask
8
+ from transformers.modeling_outputs import BaseModelOutputWithPooling
9
+ from transformers.processing_utils import Unpack
10
+ from transformers.utils import TransformersKwargs
11
+ from .configuration import PPLXQwen3Config
12
+ from transformers import AutoTokenizer
13
+ from .st_quantize import FlexibleQuantizer
14
+
15
+ # The transformers `create_causal_mask` signature has shifted over releases
16
+ # (the embeds kwarg was renamed `input_embeds` -> `inputs_embeds`, and
17
+ # `cache_position` was eventually dropped). Probe the actual signature at import
18
+ # time so this works on any installed release, including dev/main builds.
19
+ _CCM_PARAMS = inspect.signature(create_causal_mask).parameters
20
+ _CCM_EMBEDS_KEY = "inputs_embeds" if "inputs_embeds" in _CCM_PARAMS else "input_embeds"
21
+ _CCM_ACCEPTS_CACHE_POSITION = "cache_position" in _CCM_PARAMS
22
+
23
+
24
+ # From modeling_t5gemma.py
25
+ def bidirectional_mask_function(attention_mask: torch.Tensor | None) -> Callable:
26
+ """
27
+ This creates bidirectional attention mask.
28
+ """
29
+
30
+ def inner_mask(batch_idx: int, head_idx: int, q_idx: int, kv_idx: int) -> bool:
31
+ if attention_mask is None:
32
+ return torch.ones((), dtype=torch.bool)
33
+ return attention_mask[batch_idx, kv_idx].to(torch.bool)
34
+
35
+ return inner_mask
36
+
37
+
38
+ class PPLXQwen3Model(Qwen3Model):
39
+ _supports_flash_attn = True
40
+ _supports_sdpa = True
41
+
42
+ config_class = PPLXQwen3Config
43
+
44
+ def __init__(self, config):
45
+ super().__init__(config)
46
+ self.post_init()
47
+
48
+ def post_init(self):
49
+ super().post_init()
50
+ # Override to set all layers to non-causal attention. This'll work with attn_implementation="flash_attention_2" or "sdpa"
51
+ for layer in self.layers:
52
+ layer.self_attn.is_causal = False
53
+
54
+ def forward(
55
+ self,
56
+ input_ids: torch.LongTensor | None = None,
57
+ attention_mask: torch.Tensor | None = None,
58
+ position_ids: torch.LongTensor | None = None,
59
+ past_key_values: Cache | None = None,
60
+ inputs_embeds: torch.FloatTensor | None = None,
61
+ use_cache: bool | None = None,
62
+ cache_position: torch.LongTensor | None = None,
63
+ **kwargs: Unpack[TransformersKwargs],
64
+ ) -> BaseModelOutputWithPooling:
65
+ if inputs_embeds is None:
66
+ inputs_embeds = self.embed_tokens(input_ids)
67
+ input_ids = None
68
+
69
+ mask_kwargs = {
70
+ "config": self.config,
71
+ _CCM_EMBEDS_KEY: inputs_embeds,
72
+ "attention_mask": attention_mask,
73
+ "past_key_values": None,
74
+ "position_ids": position_ids,
75
+ "or_mask_function": bidirectional_mask_function(attention_mask),
76
+ }
77
+ if _CCM_ACCEPTS_CACHE_POSITION:
78
+ mask_kwargs["cache_position"] = torch.arange(
79
+ inputs_embeds.shape[1], device=inputs_embeds.device, dtype=torch.long
80
+ )
81
+ attention_mask = {"full_attention": create_causal_mask(**mask_kwargs)}
82
+
83
+ outputs = super().forward(
84
+ input_ids=input_ids,
85
+ attention_mask=attention_mask,
86
+ position_ids=position_ids,
87
+ past_key_values=past_key_values,
88
+ inputs_embeds=inputs_embeds,
89
+ use_cache=use_cache,
90
+ cache_position=cache_position,
91
+ **kwargs,
92
+ )
93
+ return outputs
94
+
95
+
96
+ class PPLXQwen3ContextualModel(PPLXQwen3Model):
97
+ """
98
+ Qwen3 model with contextual encoding support for late chunking.
99
+
100
+ This model extends PPLXQwen3Model with an encode() method that supports both
101
+ standard encoding (list[str]) and contextual encoding (list[list[str]]) with late chunking.
102
+
103
+ IMPORTANT: This model MUST be loaded with trust_remote_code=True:
104
+
105
+ from transformers import AutoModel
106
+
107
+ model = AutoModel.from_pretrained(
108
+ "path/to/model",
109
+ trust_remote_code=True # REQUIRED!
110
+ )
111
+
112
+ embeddings = model.encode([["chunk1", "chunk2"]])
113
+
114
+ Loading without trust_remote_code=True will fail to load this custom model class.
115
+ """
116
+
117
+ config_class = PPLXQwen3Config
118
+
119
+ def __init__(self, config):
120
+ super().__init__(config)
121
+
122
+ if not isinstance(config, PPLXQwen3Config):
123
+ raise TypeError(
124
+ f"PPLXQwen3ContextualModel requires PPLXQwen3Config, got {type(config).__name__}. "
125
+ f"Did you forget to load with trust_remote_code=True?"
126
+ )
127
+
128
+ self.tokenizer = AutoTokenizer.from_pretrained(config._name_or_path)
129
+ self._flexible_quantizer = FlexibleQuantizer()
130
+
131
+ @staticmethod
132
+ def mean_pooling(
133
+ token_embeddings: torch.Tensor, attention_mask: torch.Tensor
134
+ ) -> torch.Tensor:
135
+ """Apply mean pooling to token embeddings."""
136
+ input_mask_expanded = (
137
+ attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()
138
+ )
139
+ return torch.sum(token_embeddings * input_mask_expanded, 1) / torch.clamp(
140
+ input_mask_expanded.sum(1), min=1e-9
141
+ )
142
+
143
+ @torch.inference_mode()
144
+ def encode(
145
+ self,
146
+ documents: list[list[str]],
147
+ batch_size: int = 32,
148
+ show_progress_bar: bool = False,
149
+ device: str | torch.device | None = None,
150
+ normalize_embeddings: bool = False,
151
+ convert_to_numpy: bool = True,
152
+ quantization: Literal["int8", "binary", "ubinary"] = "int8",
153
+ ) -> list[np.ndarray] | list[torch.Tensor]:
154
+ """
155
+ Encode documents with late chunking (contextual embeddings).
156
+
157
+ This model is designed specifically for contextual encoding and always expects
158
+ documents as nested lists where each document is a list of text chunks.
159
+
160
+ The encoding process:
161
+ 1. Concatenate chunks with separator tokens
162
+ 2. Run forward pass to get token embeddings
163
+ 3. Extract and pool individual chunk embeddings (late chunking)
164
+ 4. Apply quantization (Int8 or binary, always enabled)
165
+ 5. Normalize embeddings if requested (applied after quantization)
166
+ 6. Convert to numpy or return as tensors
167
+
168
+ Args:
169
+ documents: List of documents, where each document is a list of text chunks.
170
+ Example: [["chunk1", "chunk2"], ["chunk1", "chunk2", "chunk3"]]
171
+ batch_size: Batch size for encoding
172
+ show_progress_bar: Show progress bar during encoding
173
+ device: Device to use for computation (defaults to model's device)
174
+ normalize_embeddings: Normalize embeddings to unit length (applied after quantization)
175
+ convert_to_numpy: If True, returns list[np.ndarray], otherwise list[torch.Tensor]
176
+ quantization: Quantization type to apply. Options:
177
+ - "int8": Int8 tanh quantization (default)
178
+ - "binary": Binary tanh quantization (-1.0 or 1.0)
179
+ - "ubinary": Unsigned packed binary (uint8, 8x compression)
180
+
181
+ Returns:
182
+ List of numpy arrays or tensors (preserves document structure).
183
+ Each element has shape (n_chunks, hidden_dim) or (n_chunks, hidden_dim // 8) for ubinary.
184
+ Example: embeddings[0].shape = (2, 1024), embeddings[1].shape = (3, 1024)
185
+ Output type depends on quantization method:
186
+ - "int8": int8 dtype, values in range [-128, 127], shape (..., hidden_dim)
187
+ - "binary": float32 dtype, values -1.0 or 1.0, shape (..., hidden_dim)
188
+ - "ubinary": uint8 dtype, packed bits (8x smaller), shape (..., hidden_dim // 8)
189
+ """
190
+
191
+ if not isinstance(documents, list) or not all(
192
+ isinstance(doc, list) for doc in documents
193
+ ):
194
+ raise TypeError(
195
+ "Input 'documents' must be a list of lists of strings for contextual encoding."
196
+ )
197
+
198
+ if quantization not in ["int8", "binary", "ubinary"]:
199
+ raise ValueError(
200
+ f"Unsupported quantization type: '{quantization}'. "
201
+ f"Supported types are: 'int8', 'binary', 'ubinary'. "
202
+ f"Got: {type(quantization).__name__} = '{quantization}'"
203
+ )
204
+
205
+ if normalize_embeddings and quantization == "ubinary":
206
+ raise ValueError(
207
+ "normalize_embeddings=True is incompatible with quantization='ubinary'. "
208
+ "Packed binary embeddings (uint8) cannot be normalized because each byte "
209
+ "represents 8 packed bits, not a single dimension. "
210
+ "Either set normalize_embeddings=False or use 'binary' quantization instead."
211
+ )
212
+
213
+ self.eval()
214
+
215
+ if device is None:
216
+ device = next(self.parameters()).device
217
+
218
+ all_embeddings = []
219
+
220
+ range_iter = range(0, len(documents), batch_size)
221
+ if show_progress_bar:
222
+ try:
223
+ from tqdm import tqdm
224
+
225
+ range_iter = tqdm(range_iter, desc="Encoding documents")
226
+ except ImportError:
227
+ pass
228
+
229
+ for i in range_iter:
230
+ batch_docs = documents[i : i + batch_size]
231
+
232
+ doc_strings = [
233
+ self.tokenizer.sep_token.join(chunks) for chunks in batch_docs
234
+ ]
235
+
236
+ inputs = self.tokenizer(
237
+ doc_strings,
238
+ padding=True,
239
+ truncation=True,
240
+ return_tensors="pt",
241
+ )
242
+ inputs = {k: v.to(device) for k, v in inputs.items()}
243
+
244
+ outputs = self.forward(**inputs)
245
+ token_embeddings = outputs.last_hidden_state
246
+
247
+ batch_chunk_embeddings = self._extract_chunks_from_concatenated(
248
+ input_ids=inputs["input_ids"],
249
+ token_embeddings=token_embeddings,
250
+ attention_mask=inputs["attention_mask"],
251
+ )
252
+
253
+ batch_chunk_embeddings = [
254
+ torch.stack([chunk for chunk in doc_chunks], dim=0)
255
+ for doc_chunks in batch_chunk_embeddings
256
+ ]
257
+
258
+ batch_chunk_embeddings = [
259
+ self._flexible_quantizer(
260
+ {"sentence_embedding": emb}, quantization=quantization
261
+ )["sentence_embedding"]
262
+ for emb in batch_chunk_embeddings
263
+ ]
264
+
265
+ if normalize_embeddings:
266
+ batch_chunk_embeddings = [
267
+ torch.nn.functional.normalize(emb, p=2, dim=-1)
268
+ for emb in batch_chunk_embeddings
269
+ ]
270
+
271
+ batch_chunk_embeddings = [emb.cpu() for emb in batch_chunk_embeddings]
272
+
273
+ all_embeddings.extend(batch_chunk_embeddings)
274
+
275
+ if convert_to_numpy:
276
+ all_embeddings = [emb.numpy() for emb in all_embeddings]
277
+
278
+ return all_embeddings
279
+
280
+ def _extract_chunks_from_concatenated(
281
+ self,
282
+ input_ids: torch.Tensor,
283
+ token_embeddings: torch.Tensor,
284
+ attention_mask: torch.Tensor,
285
+ ) -> list[list[torch.Tensor]]:
286
+ """
287
+ Extract individual chunk embeddings from concatenated sequence using late chunking.
288
+
289
+ This method splits concatenated sequences like "[chunk1][SEP][chunk2][SEP]..."
290
+ back into individual chunk embeddings by finding SEP token positions.
291
+
292
+ Args:
293
+ input_ids: Token IDs (batch_size, seq_len)
294
+ token_embeddings: Token embeddings (batch_size, seq_len, hidden_dim)
295
+ attention_mask: Attention mask (batch_size, seq_len)
296
+
297
+ Returns:
298
+ list[list[torch.Tensor]]: List of documents, each containing list of chunk embeddings
299
+
300
+ Note:
301
+ The sep_token_id is retrieved from self.tokenizer.sep_token_id.
302
+ Common values: Qwen2=151643, BERT=102, varies by tokenizer.
303
+ """
304
+ sep_token_id = self.tokenizer.sep_token_id
305
+ batch_size = input_ids.shape[0]
306
+
307
+ all_doc_chunks = []
308
+
309
+ for batch_idx in range(batch_size):
310
+ # non-pad sep tokens
311
+ valid_positions = attention_mask[batch_idx].bool()
312
+ sep_positions = (
313
+ (input_ids[batch_idx] == sep_token_id) & valid_positions
314
+ ).nonzero(as_tuple=True)[0]
315
+
316
+ chunk_embeddings = []
317
+ start_pos = 0
318
+
319
+ for sep_pos in sep_positions:
320
+ chunk_tokens = token_embeddings[batch_idx, start_pos:sep_pos]
321
+ chunk_mask = attention_mask[batch_idx, start_pos:sep_pos]
322
+
323
+ chunk_emb = self.mean_pooling(
324
+ chunk_tokens.unsqueeze(0), chunk_mask.unsqueeze(0)
325
+ ).squeeze(0)
326
+
327
+ chunk_embeddings.append(chunk_emb)
328
+
329
+ start_pos = sep_pos + 1
330
+
331
+ # Handle the last chunk (after the last SEP token)
332
+ last_valid_pos = attention_mask[batch_idx].sum().item()
333
+
334
+ chunk_tokens = token_embeddings[batch_idx, start_pos:last_valid_pos]
335
+ chunk_mask = attention_mask[batch_idx, start_pos:last_valid_pos]
336
+
337
+ if chunk_mask.sum() > 0:
338
+ chunk_emb = self.mean_pooling(
339
+ chunk_tokens.unsqueeze(0), chunk_mask.unsqueeze(0)
340
+ ).squeeze(0)
341
+ else:
342
+ # Empty chunk - create zero embedding
343
+ chunk_emb = torch.zeros(
344
+ token_embeddings.shape[-1],
345
+ device=token_embeddings.device,
346
+ dtype=token_embeddings.dtype,
347
+ )
348
+
349
+ chunk_embeddings.append(chunk_emb)
350
+
351
+ all_doc_chunks.append(chunk_embeddings)
352
+
353
+ return all_doc_chunks
modules.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "idx": 0,
4
+ "name": "0",
5
+ "path": "",
6
+ "type": "sentence_transformers.base.modules.transformer.Transformer"
7
+ },
8
+ {
9
+ "idx": 1,
10
+ "name": "1",
11
+ "path": "1_Pooling",
12
+ "type": "sentence_transformers.sentence_transformer.modules.pooling.Pooling"
13
+ },
14
+ {
15
+ "idx": 2,
16
+ "name": "2",
17
+ "path": "2_FlexibleQuantizer",
18
+ "type": "st_quantize.FlexibleQuantizer",
19
+ "kwargs": [
20
+ "quantization"
21
+ ]
22
+ }
23
+ ]
sentence_bert_config.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "transformer_task": "feature-extraction",
3
+ "modality_config": {
4
+ "text": {
5
+ "method": "forward",
6
+ "method_output_name": "last_hidden_state"
7
+ }
8
+ },
9
+ "module_output_name": "token_embeddings"
10
+ }
st_quantize.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import numpy as np
3
+ from typing import Literal
4
+ from sentence_transformers.models import Module
5
+
6
+
7
+ class Quantizer(torch.nn.Module):
8
+ def __init__(self, hard: bool = True):
9
+ """
10
+ Args:
11
+ hard: Whether to use hard or soft quantization. Defaults to True.
12
+ """
13
+ super().__init__()
14
+ self._hard = hard
15
+
16
+ def _hard_quantize(self, x, *args, **kwargs) -> torch.Tensor:
17
+ raise NotImplementedError
18
+
19
+ def _soft_quantize(self, x, *args, **kwargs) -> torch.Tensor:
20
+ raise NotImplementedError
21
+
22
+ def forward(self, x, *args, **kwargs) -> torch.Tensor:
23
+ soft = self._soft_quantize(x, *args, **kwargs)
24
+
25
+ if not self._hard:
26
+ result = soft
27
+ else:
28
+ result = (
29
+ self._hard_quantize(x, *args, **kwargs).detach() + soft - soft.detach()
30
+ )
31
+
32
+ return result
33
+
34
+
35
+ class Int8TanhQuantizer(Quantizer):
36
+ def __init__(
37
+ self,
38
+ hard: bool = True,
39
+ ):
40
+ super().__init__(hard=hard)
41
+ self.qmin = -128
42
+ self.qmax = 127
43
+
44
+ def _soft_quantize(self, x, *args, **kwargs):
45
+ return torch.tanh(x)
46
+
47
+ def _hard_quantize(self, x, *args, **kwargs):
48
+ soft = self._soft_quantize(x)
49
+ int_x = torch.round(soft * self.qmax)
50
+ int_x = torch.clamp(int_x, self.qmin, self.qmax)
51
+ return int_x
52
+
53
+
54
+ class BinaryTanhQuantizer(Quantizer):
55
+ def __init__(
56
+ self,
57
+ hard: bool = True,
58
+ scale: float = 1.0,
59
+ ):
60
+ super().__init__(hard)
61
+ self._scale = scale
62
+
63
+ def _soft_quantize(self, x, *args, **kwargs):
64
+ return torch.tanh(self._scale * x)
65
+
66
+ def _hard_quantize(self, x, *args, **kwargs):
67
+ return torch.where(x >= 0, 1.0, -1.0)
68
+
69
+
70
+ class PackedBinaryQuantizer:
71
+ """
72
+ Packs binary embeddings into uint8 format for efficient storage.
73
+
74
+ This quantizer applies a binary threshold (x >= 0) and packs 8 consecutive
75
+ bits into a single uint8 byte using numpy.packbits. This reduces memory
76
+ usage by 8x compared to float32 and by 4x compared to int8.
77
+
78
+ IMPORTANT: This is an inference-only quantizer - it is not differentiable
79
+ and should only be used for encoding/inference, not during training.
80
+
81
+ Args:
82
+ x: Input tensor of any float dtype, shape (..., embedding_dim)
83
+
84
+ Returns:
85
+ Packed binary tensor of dtype uint8, shape (..., embedding_dim // 8)
86
+
87
+ Example:
88
+ >>> quantizer = PackedBinaryQuantizer()
89
+ >>> embeddings = torch.randn(2, 1024) # float32
90
+ >>> packed = quantizer(embeddings) # uint8, shape (2, 128)
91
+ """
92
+ def __call__(self, x: torch.Tensor) -> torch.Tensor:
93
+ bits = np.where(x.cpu().numpy() >= 0, True, False)
94
+ packed = np.packbits(bits, axis=-1)
95
+ return torch.from_numpy(packed).to(x.device)
96
+
97
+
98
+ class FlexibleQuantizer(Module):
99
+ def __init__(self):
100
+ super().__init__()
101
+ self._int8_quantizer = Int8TanhQuantizer()
102
+ self._binary_quantizer = BinaryTanhQuantizer()
103
+ self._packed_binary_quantizer = PackedBinaryQuantizer()
104
+
105
+ def forward(
106
+ self,
107
+ features: dict[str, torch.Tensor],
108
+ quantization: Literal["int8", "binary", "ubinary"] = "int8",
109
+ **kwargs,
110
+ ) -> dict[str, torch.Tensor]:
111
+ if quantization == "int8":
112
+ features["sentence_embedding"] = self._int8_quantizer(
113
+ features["sentence_embedding"]
114
+ )
115
+ elif quantization == "binary":
116
+ features["sentence_embedding"] = self._binary_quantizer(
117
+ features["sentence_embedding"]
118
+ )
119
+ elif quantization == "ubinary":
120
+ features["sentence_embedding"] = self._packed_binary_quantizer(
121
+ features["sentence_embedding"]
122
+ )
123
+ else:
124
+ raise ValueError(
125
+ f"Invalid quantization type: {quantization}. Must be 'binary', 'ubinary', or 'int8'."
126
+ )
127
+ return features
128
+
129
+ @classmethod
130
+ def load(
131
+ cls,
132
+ model_name_or_path: str,
133
+ subfolder: str = "",
134
+ token: bool | str | None = None,
135
+ cache_folder: str | None = None,
136
+ revision: str | None = None,
137
+ local_files_only: bool = False,
138
+ **kwargs,
139
+ ):
140
+ return cls()
141
+
142
+ def save(self, output_path: str, *args, **kwargs) -> None:
143
+ return
tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:32687b48a8d7da95d23b32a8f24677795496605001bddee04016bb78ebcc2e67
3
+ size 11422833
tokenizer_config.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_prefix_space": false,
3
+ "backend": "tokenizers",
4
+ "bos_token": null,
5
+ "clean_up_tokenization_spaces": false,
6
+ "eos_token": "<|endoftext|>",
7
+ "errors": "replace",
8
+ "is_local": false,
9
+ "local_files_only": false,
10
+ "mask_token": "â½Ĺ",
11
+ "model_max_length": 32768,
12
+ "pad_token": "<|endoftext|>",
13
+ "sep_token": "<|endoftext|>",
14
+ "split_special_tokens": false,
15
+ "tokenizer_class": "Qwen2Tokenizer",
16
+ "unk_token": null
17
+ }