mkrimmel-pplx commited on
Commit
ff0893c
·
1 Parent(s): 727dfda

fix: added new implementation

Browse files
Files changed (6) hide show
  1. config.json +6 -8
  2. configuration.py +128 -0
  3. configuration_qwen3.py +0 -206
  4. modeling.py +790 -0
  5. modules.json +2 -1
  6. st_quantize.py +30 -59
config.json CHANGED
@@ -1,13 +1,12 @@
1
  {
2
  "architectures": [
3
- "Qwen3Model"
4
  ],
5
  "attention_bias": false,
6
  "attention_dropout": 0.0,
7
  "auto_map": {
8
- "AutoConfig": "configuration_qwen3.Qwen3Config",
9
- "AutoModel": "perplexity-ai/bidirectional-qwen3-implementation--modeling_qwen3.Qwen3Model",
10
- "AutoModelForMaskedLM": "modeling_qwen3.Qwen3ForMaskedLM"
11
  },
12
  "bos_token_id": 151643,
13
  "dtype": "float32",
@@ -57,8 +56,7 @@
57
  ],
58
  "max_position_embeddings": 32768,
59
  "max_window_layers": 36,
60
- "mlm_loss_variant": "elbo_normalize",
61
- "model_type": "qwen3",
62
  "num_attention_heads": 32,
63
  "num_hidden_layers": 36,
64
  "num_key_value_heads": 8,
@@ -73,6 +71,6 @@
73
  "transformers_version": "5.0.0.dev0",
74
  "use_cache": false,
75
  "use_sliding_window": false,
76
- "variant": "bidirectional",
77
- "vocab_size": 151936
78
  }
 
1
  {
2
  "architectures": [
3
+ "PPLXQwen3Model"
4
  ],
5
  "attention_bias": false,
6
  "attention_dropout": 0.0,
7
  "auto_map": {
8
+ "AutoConfig": "configuration.PPLXQwen3Config",
9
+ "AutoModel": "modeling.PPLXQwen3Model"
 
10
  },
11
  "bos_token_id": 151643,
12
  "dtype": "float32",
 
56
  ],
57
  "max_position_embeddings": 32768,
58
  "max_window_layers": 36,
59
+ "model_type": "bidirectional_pplx_qwen3",
 
60
  "num_attention_heads": 32,
61
  "num_hidden_layers": 36,
62
  "num_key_value_heads": 8,
 
71
  "transformers_version": "5.0.0.dev0",
72
  "use_cache": false,
73
  "use_sliding_window": false,
74
+ "vocab_size": 151936,
75
+ "attn_implementation": "sdpa"
76
  }
configuration.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+ # Copyright 2024 The Qwen team, Alibaba Group and the HuggingFace Inc. team. All rights reserved.
3
+ #
4
+ # This file has been modified from the original Qwen3 implementation.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ from typing import Optional
19
+ from transformers import PretrainedConfig
20
+
21
+
22
+ class PPLXQwen3Config(PretrainedConfig):
23
+ """
24
+ PPLX configuration class for Qwen3Model compatible with transformers < 5.X.
25
+ This implementation only supports bidirectional attention (no causal or dropout variants).
26
+
27
+ Args:
28
+ vocab_size (int, optional, defaults to 151936):
29
+ Vocabulary size of the Qwen3 model.
30
+ hidden_size (int, optional, defaults to 4096):
31
+ Dimension of the hidden representations.
32
+ intermediate_size (int, optional, defaults to 22016):
33
+ Dimension of the MLP representations.
34
+ num_hidden_layers (int, optional, defaults to 32):
35
+ Number of hidden layers in the Transformer encoder.
36
+ num_attention_heads (int, optional, defaults to 32):
37
+ Number of attention heads for each attention layer.
38
+ num_key_value_heads (int, optional, defaults to 32):
39
+ Number of key_value heads for Grouped Query Attention.
40
+ head_dim (int, optional, defaults to 128):
41
+ The attention head dimension.
42
+ hidden_act (str, optional, defaults to "silu"):
43
+ The non-linear activation function.
44
+ max_position_embeddings (int, optional, defaults to 32768):
45
+ The maximum sequence length.
46
+ initializer_range (float, optional, defaults to 0.02):
47
+ The standard deviation for weight initialization.
48
+ rms_norm_eps (float, optional, defaults to 1e-06):
49
+ The epsilon for rms normalization layers.
50
+ attention_bias (bool, optional, defaults to False):
51
+ Whether to use bias in attention projection layers.
52
+ attention_dropout (float, optional, defaults to 0.0):
53
+ The dropout ratio for attention probabilities.
54
+ rope_theta (float, optional, defaults to 10000.0):
55
+ The base period of the RoPE embeddings.
56
+ pad_token_id (int, optional):
57
+ The id of the padding token.
58
+ bos_token_id (int, optional):
59
+ The id of the beginning-of-sequence token.
60
+ eos_token_id (int, optional):
61
+ The id of the end-of-sequence token.
62
+ attn_implementation (str, optional):
63
+ The attention implementation to use. Options: "eager", "sdpa".
64
+ If None, will auto-select based on availability.
65
+ """
66
+
67
+ model_type = "bidirectional_pplx_qwen3"
68
+
69
+ def __init__(
70
+ self,
71
+ vocab_size: Optional[int] = 151936,
72
+ hidden_size: Optional[int] = 4096,
73
+ intermediate_size: Optional[int] = 22016,
74
+ num_hidden_layers: Optional[int] = 32,
75
+ num_attention_heads: Optional[int] = 32,
76
+ num_key_value_heads: Optional[int] = 32,
77
+ head_dim: Optional[int] = 128,
78
+ hidden_act: Optional[str] = "silu",
79
+ max_position_embeddings: Optional[int] = 32768,
80
+ initializer_range: Optional[float] = 0.02,
81
+ rms_norm_eps: Optional[float] = 1e-6,
82
+ attention_bias: Optional[bool] = False,
83
+ attention_dropout: Optional[float] = 0.0,
84
+ rope_theta: Optional[float] = 10000.0,
85
+ pad_token_id: Optional[int] = None,
86
+ bos_token_id: Optional[int] = None,
87
+ eos_token_id: Optional[int] = None,
88
+ attn_implementation: Optional[str] = None,
89
+ **kwargs,
90
+ ):
91
+ # Extract attn_implementation from kwargs if not explicitly provided
92
+ if attn_implementation is None and 'attn_implementation' in kwargs:
93
+ attn_implementation = kwargs.pop('attn_implementation')
94
+
95
+ self.vocab_size = vocab_size
96
+ self.max_position_embeddings = max_position_embeddings
97
+ self.hidden_size = hidden_size
98
+ self.intermediate_size = intermediate_size
99
+ self.num_hidden_layers = num_hidden_layers
100
+ self.num_attention_heads = num_attention_heads
101
+ self.num_key_value_heads = num_key_value_heads if num_key_value_heads is not None else num_attention_heads
102
+ self.head_dim = head_dim
103
+ self.hidden_act = hidden_act
104
+ self.initializer_range = initializer_range
105
+ self.rms_norm_eps = rms_norm_eps
106
+ self.attention_bias = attention_bias
107
+ self.attention_dropout = attention_dropout
108
+ self.rope_theta = rope_theta
109
+
110
+ # Legacy: only bidirectional attention supported
111
+ self.is_causal = False
112
+
113
+ # Initialize parent class with token IDs
114
+ super().__init__(
115
+ pad_token_id=pad_token_id,
116
+ bos_token_id=bos_token_id,
117
+ eos_token_id=eos_token_id,
118
+ **kwargs,
119
+ )
120
+
121
+ # Store attn_implementation as a regular attribute AFTER super().__init__() (will be serialized)
122
+ self.attn_implementation = attn_implementation
123
+
124
+
125
+ # Register for AutoConfig
126
+ PPLXQwen3Config.register_for_auto_class()
127
+
128
+ __all__ = ["PPLXQwen3Config"]
configuration_qwen3.py DELETED
@@ -1,206 +0,0 @@
1
- # coding=utf-8
2
- # Copyright 2024 The Qwen team, Alibaba Group and the HuggingFace Inc. team. All rights reserved.
3
- #
4
- # Licensed under the Apache License, Version 2.0 (the "License");
5
- # you may not use this file except in compliance with the License.
6
- # You may obtain a copy of the License at
7
- #
8
- # http://www.apache.org/licenses/LICENSE-2.0
9
- #
10
- # Unless required by applicable law or agreed to in writing, software
11
- # distributed under the License is distributed on an "AS IS" BASIS,
12
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- # See the License for the specific language governing permissions and
14
- # limitations under the License.
15
- """Qwen3 model configuration"""
16
-
17
- from typing import Optional, Literal
18
-
19
- import warnings
20
-
21
- from transformers.configuration_utils import PreTrainedConfig, layer_type_validation
22
- from transformers.modeling_rope_utils import RopeParameters, rope_config_validation, standardize_rope_params
23
- from transformers.utils import logging
24
-
25
-
26
- logger = logging.get_logger(__name__)
27
-
28
-
29
- class Qwen3Config(PreTrainedConfig):
30
- r"""
31
- This is the configuration class to store the configuration of a [`Qwen3Model`]. It is used to instantiate a
32
- Qwen3 model according to the specified arguments, defining the model architecture. Instantiating a configuration
33
- with the defaults will yield a similar configuration to that of
34
- Qwen3-8B [Qwen/Qwen3-8B](https://huggingface.co/Qwen/Qwen3-8B).
35
-
36
- Configuration objects inherit from [`PreTrainedConfig`] and can be used to control the model outputs. Read the
37
- documentation from [`PreTrainedConfig`] for more information.
38
-
39
-
40
- Args:
41
- vocab_size (`int`, *optional*, defaults to 151936):
42
- Vocabulary size of the Qwen3 model. Defines the number of different tokens that can be represented by the
43
- `inputs_ids` passed when calling [`Qwen3Model`]
44
- hidden_size (`int`, *optional*, defaults to 4096):
45
- Dimension of the hidden representations.
46
- intermediate_size (`int`, *optional*, defaults to 22016):
47
- Dimension of the MLP representations.
48
- num_hidden_layers (`int`, *optional*, defaults to 32):
49
- Number of hidden layers in the Transformer encoder.
50
- num_attention_heads (`int`, *optional*, defaults to 32):
51
- Number of attention heads for each attention layer in the Transformer encoder.
52
- num_key_value_heads (`int`, *optional*, defaults to 32):
53
- This is the number of key_value heads that should be used to implement Grouped Query Attention. If
54
- `num_key_value_heads=num_attention_heads`, the model will use Multi Head Attention (MHA), if
55
- `num_key_value_heads=1` the model will use Multi Query Attention (MQA) otherwise GQA is used. When
56
- converting a multi-head checkpoint to a GQA checkpoint, each group key and value head should be constructed
57
- by meanpooling all the original heads within that group. For more details, check out [this
58
- paper](https://huggingface.co/papers/2305.13245). If it is not specified, will default to `32`.
59
- head_dim (`int`, *optional*, defaults to 128):
60
- The attention head dimension.
61
- hidden_act (`str` or `function`, *optional*, defaults to `"silu"`):
62
- The non-linear activation function (function or string) in the decoder.
63
- max_position_embeddings (`int`, *optional*, defaults to 32768):
64
- The maximum sequence length that this model might ever be used with.
65
- initializer_range (`float`, *optional*, defaults to 0.02):
66
- The standard deviation of the truncated_normal_initializer for initializing all weight matrices.
67
- rms_norm_eps (`float`, *optional*, defaults to 1e-06):
68
- The epsilon used by the rms normalization layers.
69
- use_cache (`bool`, *optional*, defaults to `True`):
70
- Whether or not the model should return the last key/values attentions (not used by all models). Only
71
- relevant if `config.is_decoder=True`.
72
- tie_word_embeddings (`bool`, *optional*, defaults to `False`):
73
- Whether the model's input and output word embeddings should be tied.
74
- rope_parameters (`RopeParameters`, *optional*):
75
- Dictionary containing the configuration parameters for the RoPE embeddings. The dictionaty should contain
76
- a value for `rope_theta` and optionally parameters used for scaling in case you want to use RoPE
77
- with longer `max_position_embeddings`.
78
- attention_bias (`bool`, defaults to `False`, *optional*, defaults to `False`):
79
- Whether to use a bias in the query, key, value and output projection layers during self-attention.
80
- use_sliding_window (`bool`, *optional*, defaults to `False`):
81
- Whether to use sliding window attention.
82
- sliding_window (`int`, *optional*, defaults to 4096):
83
- Sliding window attention (SWA) window size. If not specified, will default to `4096`.
84
- max_window_layers (`int`, *optional*, defaults to 28):
85
- The number of layers using full attention. The first `max_window_layers` layers will use full attention, while any
86
- additional layer afterwards will use SWA (Sliding Window Attention).
87
- layer_types (`list`, *optional*):
88
- Attention pattern for each layer.
89
- attention_dropout (`float`, *optional*, defaults to 0.0):
90
- The dropout ratio for the attention probabilities.
91
-
92
- ```python
93
- >>> from transformers import Qwen3Model, Qwen3Config
94
-
95
- >>> # Initializing a Qwen3 style configuration
96
- >>> configuration = Qwen3Config()
97
-
98
- >>> # Initializing a model from the Qwen3-8B style configuration
99
- >>> model = Qwen3Model(configuration)
100
-
101
- >>> # Accessing the model configuration
102
- >>> configuration = model.config
103
- ```"""
104
-
105
- model_type = "qwen3"
106
- keys_to_ignore_at_inference = ["past_key_values"]
107
-
108
- # Default tensor parallel plan for base model `Qwen3`
109
- base_model_tp_plan = {
110
- "layers.*.self_attn.q_proj": "colwise",
111
- "layers.*.self_attn.k_proj": "colwise",
112
- "layers.*.self_attn.v_proj": "colwise",
113
- "layers.*.self_attn.o_proj": "rowwise",
114
- "layers.*.mlp.gate_proj": "colwise",
115
- "layers.*.mlp.up_proj": "colwise",
116
- "layers.*.mlp.down_proj": "rowwise",
117
- }
118
- base_model_pp_plan = {
119
- "embed_tokens": (["input_ids"], ["inputs_embeds"]),
120
- "layers": (["hidden_states", "attention_mask"], ["hidden_states"]),
121
- "norm": (["hidden_states"], ["hidden_states"]),
122
- }
123
-
124
- def __init__(
125
- self,
126
- vocab_size: Optional[int] = 151936,
127
- hidden_size: Optional[int] = 4096,
128
- intermediate_size: Optional[int] = 22016,
129
- num_hidden_layers: Optional[int] = 32,
130
- num_attention_heads: Optional[int] = 32,
131
- num_key_value_heads: Optional[int] = 32,
132
- head_dim: Optional[int] = 128,
133
- hidden_act: Optional[str] = "silu",
134
- max_position_embeddings: Optional[int] = 32768,
135
- initializer_range: Optional[float] = 0.02,
136
- rms_norm_eps: Optional[int] = 1e-6,
137
- use_cache: Optional[bool] = True,
138
- tie_word_embeddings: Optional[bool] = False,
139
- rope_parameters: Optional[RopeParameters | dict[RopeParameters]] = None,
140
- attention_bias: Optional[bool] = False,
141
- use_sliding_window: Optional[bool] = False,
142
- sliding_window: Optional[int] = 4096,
143
- max_window_layers: Optional[int] = 28,
144
- layer_types: Optional[list[str]] = None,
145
- attention_dropout: Optional[float] = 0.0,
146
- variant: Literal["causal", "bidirectional", "causal_dropout"] = "causal",
147
- mlm_loss_variant: Literal["simple", "masked_normalize", "elbo_normalize", "flat_cart"] = "simple",
148
- **kwargs,
149
- ):
150
- self.vocab_size = vocab_size
151
- self.max_position_embeddings = max_position_embeddings
152
- self.hidden_size = hidden_size
153
- self.intermediate_size = intermediate_size
154
- self.num_hidden_layers = num_hidden_layers
155
- self.num_attention_heads = num_attention_heads
156
- self.use_sliding_window = use_sliding_window
157
- self.sliding_window = sliding_window if self.use_sliding_window else None
158
- self.max_window_layers = max_window_layers
159
-
160
- # for backward compatibility
161
- if num_key_value_heads is None:
162
- num_key_value_heads = num_attention_heads
163
-
164
- self.num_key_value_heads = num_key_value_heads
165
- self.head_dim = head_dim
166
- self.hidden_act = hidden_act
167
- self.initializer_range = initializer_range
168
- self.rms_norm_eps = rms_norm_eps
169
- self.use_cache = use_cache
170
- self.attention_bias = attention_bias
171
- self.attention_dropout = attention_dropout
172
- # Try to set `rope_scaling` if available, otherwise use `rope_parameters`
173
- rope_scaling = kwargs.pop("rope_scaling", None)
174
- self.rope_parameters = rope_scaling or rope_parameters
175
-
176
- self.layer_types = layer_types
177
- if self.layer_types is None:
178
- self.layer_types = [
179
- "sliding_attention"
180
- if self.sliding_window is not None and i >= self.max_window_layers
181
- else "full_attention"
182
- for i in range(self.num_hidden_layers)
183
- ]
184
- layer_type_validation(self.layer_types, self.num_hidden_layers)
185
-
186
- # Validate the correctness of rotary position embeddings parameters
187
- rope_theta = kwargs.get("rope_theta", 10000.0)
188
- standardize_rope_params(self, rope_theta=rope_theta)
189
- rope_config_validation(self)
190
-
191
- self.variant = variant
192
- self.mlm_loss_variant = mlm_loss_variant
193
-
194
- if mlm_loss_variant not in ["simple", "masked_normalize", "elbo_normalize", "flat_cart"]:
195
- raise NotImplementedError(f"Loss variant {mlm_loss_variant} unknown")
196
-
197
- if variant != "causal" and use_cache:
198
- warnings.warn("Cannot use cache (use_cache) and bidirectional attention (is_causal=False)")
199
-
200
- super().__init__(
201
- tie_word_embeddings=tie_word_embeddings,
202
- **kwargs,
203
- )
204
-
205
-
206
- __all__ = ["Qwen3Config"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modeling.py ADDED
@@ -0,0 +1,790 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+ # Copyright 2025 The Qwen team, Alibaba Group and the HuggingFace Inc. team. All rights reserved.
3
+ #
4
+ # This file has been modified from the original Qwen3 implementation.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ from typing import Optional, Tuple
19
+
20
+ import numpy as np
21
+ import torch
22
+ from torch import nn
23
+ import torch.nn.functional as F
24
+ from transformers import AutoTokenizer
25
+ from transformers.modeling_utils import PreTrainedModel
26
+ from transformers.modeling_outputs import BaseModelOutputWithPast
27
+
28
+ from .configuration import PPLXQwen3Config
29
+ from .st_quantize import Int8TanhQuantizer
30
+
31
+
32
+ # Activation functions mapping
33
+ ACT2FN = {
34
+ "silu": nn.functional.silu,
35
+ "gelu": nn.functional.gelu,
36
+ "relu": nn.functional.relu,
37
+ }
38
+
39
+
40
+ class PPLXQwen3RMSNorm(nn.Module):
41
+ """RMSNorm implementation compatible with transformers < 5.X"""
42
+
43
+ def __init__(self, hidden_size, eps: float = 1e-6) -> None:
44
+ super().__init__()
45
+ self.weight = nn.Parameter(torch.ones(hidden_size))
46
+ self.variance_epsilon = eps
47
+
48
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
49
+ input_dtype = hidden_states.dtype
50
+ hidden_states = hidden_states.to(torch.float32)
51
+ variance = hidden_states.pow(2).mean(-1, keepdim=True)
52
+ hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
53
+ return self.weight * hidden_states.to(input_dtype)
54
+
55
+
56
+ class PPLXQwen3MLP(nn.Module):
57
+ """MLP implementation compatible with transformers < 5.X"""
58
+
59
+ def __init__(self, config):
60
+ super().__init__()
61
+ self.config = config
62
+ self.hidden_size = config.hidden_size
63
+ self.intermediate_size = config.intermediate_size
64
+ self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False)
65
+ self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False)
66
+ self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False)
67
+ self.act_fn = ACT2FN[config.hidden_act]
68
+
69
+ def forward(self, x):
70
+ down_proj = self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x))
71
+ return down_proj
72
+
73
+
74
+ class PPLXQwen3RotaryEmbedding(nn.Module):
75
+ """Rotary Position Embedding implementation compatible with transformers < 5.X"""
76
+
77
+ def __init__(self, config, device=None):
78
+ super().__init__()
79
+ self.max_seq_len_cached = config.max_position_embeddings
80
+ self.original_max_seq_len = config.max_position_embeddings
81
+ self.config = config
82
+
83
+ # Check rope type and raise if not default
84
+ self.rope_type = self.config.rope_parameters["rope_type"]
85
+ if self.rope_type != "default":
86
+ raise NotImplementedError("Only default RoPE implemented")
87
+
88
+ # Compute inverse frequencies using the static method
89
+ inv_freq, self.attention_scaling = self.compute_default_rope_parameters(
90
+ config, device
91
+ )
92
+ self.register_buffer("inv_freq", inv_freq, persistent=False)
93
+ self.original_inv_freq = inv_freq
94
+
95
+ @staticmethod
96
+ def compute_default_rope_parameters(
97
+ config: Optional["PPLXQwen3Config"] = None,
98
+ device: Optional[torch.device] = None,
99
+ ) -> Tuple[torch.Tensor, float]:
100
+ """
101
+ Computes the inverse frequencies according to the original RoPE implementation
102
+
103
+ Args:
104
+ config: The model configuration.
105
+ device: The device to use for initialization of the inverse frequencies.
106
+
107
+ Returns:
108
+ Tuple of (inv_freq, attention_scaling), containing the inverse frequencies
109
+ for the RoPE embeddings and the post-processing scaling factor applied to
110
+ the computed cos/sin.
111
+ """
112
+ base = config.rope_parameters["rope_theta"]
113
+ dim = config.head_dim
114
+
115
+ attention_factor = 1.0 # Unused in default RoPE
116
+
117
+ # Compute the inverse frequencies
118
+ inv_freq = 1.0 / (
119
+ base ** (torch.arange(0, dim, 2, dtype=torch.int64).to(device=device, dtype=torch.float) / dim)
120
+ )
121
+ return inv_freq, attention_factor
122
+
123
+ def forward(self, x, position_ids):
124
+ # Expand inv_freq to match batch size
125
+ inv_freq_expanded = (
126
+ self.inv_freq[None, :, None]
127
+ .float()
128
+ .expand(position_ids.shape[0], -1, 1)
129
+ .to(x.device)
130
+ )
131
+ position_ids_expanded = position_ids[:, None, :].float()
132
+
133
+ # Compute frequencies
134
+ device_type = (
135
+ x.device.type
136
+ if isinstance(x.device.type, str) and x.device.type != "mps"
137
+ else "cpu"
138
+ )
139
+ with torch.autocast(device_type=device_type, enabled=False):
140
+ freqs = (
141
+ inv_freq_expanded.float() @ position_ids_expanded.float()
142
+ ).transpose(1, 2)
143
+ emb = torch.cat((freqs, freqs), dim=-1)
144
+ cos = emb.cos() * self.attention_scaling
145
+ sin = emb.sin() * self.attention_scaling
146
+
147
+ return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype)
148
+
149
+
150
+ def rotate_half(x):
151
+ """Rotates half the hidden dims of the input."""
152
+ x1 = x[..., : x.shape[-1] // 2]
153
+ x2 = x[..., x.shape[-1] // 2 :]
154
+ return torch.cat((-x2, x1), dim=-1)
155
+
156
+
157
+ def apply_rotary_pos_emb(q, k, cos, sin, position_ids=None, unsqueeze_dim=1):
158
+ """Applies Rotary Position Embedding to the query and key tensors."""
159
+ cos = cos.unsqueeze(unsqueeze_dim)
160
+ sin = sin.unsqueeze(unsqueeze_dim)
161
+ q_embed = (q * cos) + (rotate_half(q) * sin)
162
+ k_embed = (k * cos) + (rotate_half(k) * sin)
163
+ return q_embed, k_embed
164
+
165
+
166
+ def repeat_kv(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor:
167
+ """
168
+ Equivalent of torch.repeat_interleave(x, dim=1, repeats=n_rep).
169
+ Hidden states go from (batch, num_key_value_heads, seqlen, head_dim)
170
+ to (batch, num_attention_heads, seqlen, head_dim)
171
+ """
172
+ batch, num_key_value_heads, slen, head_dim = hidden_states.shape
173
+ if n_rep == 1:
174
+ return hidden_states
175
+ hidden_states = hidden_states[:, :, None, :, :].expand(
176
+ batch, num_key_value_heads, n_rep, slen, head_dim
177
+ )
178
+ return hidden_states.reshape(batch, num_key_value_heads * n_rep, slen, head_dim)
179
+
180
+
181
+ def eager_attention_forward(
182
+ query: torch.Tensor,
183
+ key: torch.Tensor,
184
+ value: torch.Tensor,
185
+ attention_mask: Optional[torch.Tensor],
186
+ scaling: float,
187
+ dropout: float = 0.0,
188
+ training: bool = False,
189
+ num_key_value_groups: int = 1,
190
+ **kwargs,
191
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
192
+ """
193
+ Eager (vanilla) attention implementation.
194
+
195
+ Args:
196
+ query: (batch, num_heads, seq_len, head_dim)
197
+ key: (batch, num_kv_heads, seq_len, head_dim)
198
+ value: (batch, num_kv_heads, seq_len, head_dim)
199
+ attention_mask: (batch, 1, seq_len, seq_len)
200
+ scaling: attention scaling factor
201
+ dropout: dropout probability
202
+ training: whether in training mode
203
+ num_key_value_groups: number of query heads per key/value head (for GQA)
204
+ """
205
+ # Repeat k/v heads if using GQA
206
+ key_states = repeat_kv(key, num_key_value_groups)
207
+ value_states = repeat_kv(value, num_key_value_groups)
208
+
209
+ # Compute attention scores
210
+ attn_weights = torch.matmul(query, key_states.transpose(2, 3)) * scaling
211
+
212
+ # Apply attention mask
213
+ if attention_mask is not None:
214
+ attn_weights = attn_weights + attention_mask
215
+
216
+ # Softmax and dropout
217
+ attn_weights = F.softmax(attn_weights, dim=-1, dtype=torch.float32).to(query.dtype)
218
+ attn_weights = F.dropout(attn_weights, p=dropout, training=training)
219
+
220
+ # Compute output
221
+ attn_output = torch.matmul(attn_weights, value_states)
222
+
223
+ return attn_output, attn_weights
224
+
225
+
226
+ def sdpa_attention_forward(
227
+ query: torch.Tensor,
228
+ key: torch.Tensor,
229
+ value: torch.Tensor,
230
+ attention_mask: Optional[torch.Tensor],
231
+ scaling: float,
232
+ dropout: float = 0.0,
233
+ training: bool = False,
234
+ num_key_value_groups: int = 1,
235
+ **kwargs,
236
+ ) -> Tuple[torch.Tensor, None]:
237
+ """
238
+ Scaled Dot Product Attention using PyTorch's native implementation.
239
+
240
+ Args:
241
+ query: (batch, num_heads, seq_len, head_dim)
242
+ key: (batch, num_kv_heads, seq_len, head_dim)
243
+ value: (batch, num_kv_heads, seq_len, head_dim)
244
+ attention_mask: (batch, 1, seq_len, seq_len) or None
245
+ scaling: attention scaling factor (handled internally by SDPA)
246
+ dropout: dropout probability
247
+ training: whether in training mode
248
+ num_key_value_groups: number of query heads per key/value head (for GQA)
249
+ """
250
+ # Repeat k/v heads if using GQA
251
+ key = repeat_kv(key, num_key_value_groups)
252
+ value = repeat_kv(value, num_key_value_groups)
253
+
254
+ # Convert attention mask for SDPA
255
+ # SDPA expects additive mask in shape (batch, num_heads, seq_len, seq_len) or broadcastable
256
+ attn_mask = None
257
+ if attention_mask is not None:
258
+ # attention_mask is (batch, 1, seq_len, seq_len)
259
+ # Broadcast to (batch, num_heads, seq_len, seq_len) by repeating
260
+ batch_size, _, seq_len, _ = attention_mask.shape
261
+ num_heads = query.shape[1]
262
+ # Expand to match num_heads
263
+ attn_mask = attention_mask.expand(batch_size, num_heads, seq_len, seq_len)
264
+
265
+ # PyTorch SDPA
266
+ attn_output = F.scaled_dot_product_attention(
267
+ query,
268
+ key,
269
+ value,
270
+ attn_mask=attn_mask,
271
+ dropout_p=dropout if training else 0.0,
272
+ is_causal=False, # We handle masking explicitly for bidirectional
273
+ scale=scaling,
274
+ )
275
+
276
+ return attn_output, None
277
+
278
+
279
+ # Mapping of attention implementation names to functions
280
+ ATTENTION_IMPLEMENTATIONS = {
281
+ "eager": eager_attention_forward,
282
+ "sdpa": sdpa_attention_forward,
283
+ }
284
+
285
+
286
+ class PPLXQwen3Attention(nn.Module):
287
+ """
288
+ Multi-headed attention implementation compatible with transformers < 5.X
289
+ Supports multiple attention backends: eager, sdpa
290
+ """
291
+
292
+ def __init__(self, config, layer_idx: int):
293
+ super().__init__()
294
+ self.config = config
295
+ self.layer_idx = layer_idx
296
+ self.head_dim = config.head_dim
297
+ self.num_attention_heads = config.num_attention_heads
298
+ self.num_key_value_heads = config.num_key_value_heads
299
+ self.num_key_value_groups = (
300
+ config.num_attention_heads // config.num_key_value_heads
301
+ )
302
+ self.scaling = self.head_dim**-0.5
303
+ self.attention_dropout = config.attention_dropout
304
+
305
+ self.q_proj = nn.Linear(
306
+ config.hidden_size,
307
+ config.num_attention_heads * self.head_dim,
308
+ bias=config.attention_bias,
309
+ )
310
+ self.k_proj = nn.Linear(
311
+ config.hidden_size,
312
+ config.num_key_value_heads * self.head_dim,
313
+ bias=config.attention_bias,
314
+ )
315
+ self.v_proj = nn.Linear(
316
+ config.hidden_size,
317
+ config.num_key_value_heads * self.head_dim,
318
+ bias=config.attention_bias,
319
+ )
320
+ self.o_proj = nn.Linear(
321
+ config.num_attention_heads * self.head_dim,
322
+ config.hidden_size,
323
+ bias=config.attention_bias,
324
+ )
325
+ self.q_norm = PPLXQwen3RMSNorm(self.head_dim, eps=config.rms_norm_eps)
326
+ self.k_norm = PPLXQwen3RMSNorm(self.head_dim, eps=config.rms_norm_eps)
327
+
328
+ # Select attention implementation
329
+ self._select_attention_implementation(config)
330
+
331
+ def _select_attention_implementation(self, config):
332
+ """Select the attention implementation based on config or availability."""
333
+ attn_impl = getattr(config, "attn_implementation", None)
334
+
335
+ if attn_impl is None:
336
+ # Auto-select: prefer faster implementations
337
+ if hasattr(F, "scaled_dot_product_attention"):
338
+ attn_impl = "sdpa"
339
+ else:
340
+ attn_impl = "eager"
341
+
342
+ if attn_impl not in ATTENTION_IMPLEMENTATIONS:
343
+ raise ValueError(
344
+ f"Unknown attention implementation: {attn_impl}. "
345
+ f"Available: {list(ATTENTION_IMPLEMENTATIONS.keys())}"
346
+ )
347
+
348
+ # Check availability
349
+ if attn_impl == "sdpa" and not hasattr(F, "scaled_dot_product_attention"):
350
+ raise ImportError(
351
+ "sdpa requested but not available. Please use PyTorch >= 2.0"
352
+ )
353
+
354
+ self.attn_implementation = attn_impl
355
+ self.attn_function = ATTENTION_IMPLEMENTATIONS[attn_impl]
356
+
357
+ def forward(
358
+ self,
359
+ hidden_states: torch.Tensor,
360
+ position_embeddings: Tuple[torch.Tensor, torch.Tensor],
361
+ attention_mask: Optional[torch.Tensor] = None,
362
+ **kwargs,
363
+ ) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
364
+ input_shape = hidden_states.shape[:-1]
365
+ hidden_shape = (*input_shape, -1, self.head_dim)
366
+
367
+ # Project and reshape
368
+ query_states = self.q_norm(
369
+ self.q_proj(hidden_states).view(hidden_shape)
370
+ ).transpose(1, 2)
371
+ key_states = self.k_norm(
372
+ self.k_proj(hidden_states).view(hidden_shape)
373
+ ).transpose(1, 2)
374
+ value_states = self.v_proj(hidden_states).view(hidden_shape).transpose(1, 2)
375
+
376
+ # Apply rotary embeddings
377
+ cos, sin = position_embeddings
378
+ query_states, key_states = apply_rotary_pos_emb(
379
+ query_states, key_states, cos, sin
380
+ )
381
+
382
+ # Call the selected attention implementation
383
+ attn_output, attn_weights = self.attn_function(
384
+ query=query_states,
385
+ key=key_states,
386
+ value=value_states,
387
+ attention_mask=attention_mask,
388
+ scaling=self.scaling,
389
+ dropout=self.attention_dropout,
390
+ training=self.training,
391
+ num_key_value_groups=self.num_key_value_groups,
392
+ )
393
+
394
+ # Reshape and project output
395
+ attn_output = attn_output.transpose(1, 2).contiguous()
396
+ attn_output = attn_output.reshape(*input_shape, -1).contiguous()
397
+ attn_output = self.o_proj(attn_output)
398
+
399
+ return attn_output, attn_weights
400
+
401
+
402
+ class PPLXQwen3DecoderLayer(nn.Module):
403
+ """Decoder layer implementation compatible with transformers < 5.X"""
404
+
405
+ def __init__(self, config, layer_idx: int):
406
+ super().__init__()
407
+ self.hidden_size = config.hidden_size
408
+ self.self_attn = PPLXQwen3Attention(config=config, layer_idx=layer_idx)
409
+ self.mlp = PPLXQwen3MLP(config)
410
+ self.input_layernorm = PPLXQwen3RMSNorm(
411
+ config.hidden_size, eps=config.rms_norm_eps
412
+ )
413
+ self.post_attention_layernorm = PPLXQwen3RMSNorm(
414
+ config.hidden_size, eps=config.rms_norm_eps
415
+ )
416
+
417
+ def forward(
418
+ self,
419
+ hidden_states: torch.Tensor,
420
+ attention_mask: Optional[torch.Tensor] = None,
421
+ position_embeddings: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
422
+ **kwargs,
423
+ ) -> torch.Tensor:
424
+ # Self Attention
425
+ residual = hidden_states
426
+ hidden_states = self.input_layernorm(hidden_states)
427
+ hidden_states, _ = self.self_attn(
428
+ hidden_states=hidden_states,
429
+ attention_mask=attention_mask,
430
+ position_embeddings=position_embeddings,
431
+ )
432
+ hidden_states = residual + hidden_states
433
+
434
+ # MLP
435
+ residual = hidden_states
436
+ hidden_states = self.post_attention_layernorm(hidden_states)
437
+ hidden_states = self.mlp(hidden_states)
438
+ hidden_states = residual + hidden_states
439
+
440
+ return hidden_states
441
+
442
+
443
+ class PPLXQwen3PreTrainedModel(PreTrainedModel):
444
+ """
445
+ An abstract class to handle weights initialization and a simple interface for downloading and loading pretrained
446
+ models.
447
+ """
448
+
449
+ config_class = PPLXQwen3Config
450
+ base_model_prefix = "model"
451
+ supports_gradient_checkpointing = False
452
+ _no_split_modules = ["PPLXQwen3DecoderLayer"]
453
+ _skip_keys_device_placement = ["past_key_values"]
454
+
455
+
456
+ class PPLXQwen3Model(PPLXQwen3PreTrainedModel):
457
+ """
458
+ Qwen3 Model implementation compatible with transformers < 5.X.
459
+ Only supports bidirectional attention (no causal masking or caching).
460
+ """
461
+
462
+ def __init__(self, config):
463
+ super().__init__(config)
464
+ self.padding_idx = config.pad_token_id
465
+ self.vocab_size = config.vocab_size
466
+
467
+ self.embed_tokens = nn.Embedding(
468
+ config.vocab_size, config.hidden_size, self.padding_idx
469
+ )
470
+ self.layers = nn.ModuleList(
471
+ [
472
+ PPLXQwen3DecoderLayer(config, layer_idx)
473
+ for layer_idx in range(config.num_hidden_layers)
474
+ ]
475
+ )
476
+ self.norm = PPLXQwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
477
+ self.rotary_emb = PPLXQwen3RotaryEmbedding(config=config)
478
+
479
+ # Initialize weights and apply final processing
480
+ self.post_init()
481
+
482
+ def forward(
483
+ self,
484
+ input_ids: Optional[torch.LongTensor] = None,
485
+ attention_mask: Optional[torch.Tensor] = None,
486
+ position_ids: Optional[torch.LongTensor] = None,
487
+ inputs_embeds: Optional[torch.FloatTensor] = None,
488
+ **kwargs,
489
+ ) -> BaseModelOutputWithPast:
490
+ # Get embeddings
491
+ if inputs_embeds is None:
492
+ inputs_embeds = self.embed_tokens(input_ids)
493
+
494
+ batch_size, seq_length = inputs_embeds.shape[:2]
495
+
496
+ # Create position IDs if not provided
497
+ if position_ids is None:
498
+ position_ids = (
499
+ torch.arange(seq_length, device=inputs_embeds.device)
500
+ .unsqueeze(0)
501
+ .expand(batch_size, -1)
502
+ )
503
+
504
+ # Create bidirectional attention mask
505
+ # Transform from (batch_size, seq_length) to (batch_size, 1, seq_length, seq_length)
506
+ if attention_mask is not None:
507
+ # Expand attention mask to 4D
508
+ attention_mask = attention_mask[:, None, None, :].to(
509
+ dtype=inputs_embeds.dtype
510
+ )
511
+ attention_mask = (1.0 - attention_mask) * torch.finfo(
512
+ inputs_embeds.dtype
513
+ ).min
514
+ # Broadcast to full attention shape
515
+ attention_mask = attention_mask.expand(
516
+ batch_size, 1, seq_length, seq_length
517
+ )
518
+ else:
519
+ # No masking needed for bidirectional attention with no padding
520
+ attention_mask = torch.zeros(
521
+ (batch_size, 1, seq_length, seq_length),
522
+ dtype=inputs_embeds.dtype,
523
+ device=inputs_embeds.device,
524
+ )
525
+
526
+ # Get rotary embeddings
527
+ position_embeddings = self.rotary_emb(inputs_embeds, position_ids)
528
+
529
+ # Pass through decoder layers
530
+ hidden_states = inputs_embeds
531
+ for decoder_layer in self.layers:
532
+ hidden_states = decoder_layer(
533
+ hidden_states,
534
+ attention_mask=attention_mask,
535
+ position_embeddings=position_embeddings,
536
+ )
537
+
538
+ # Final norm
539
+ hidden_states = self.norm(hidden_states)
540
+
541
+ return BaseModelOutputWithPast(last_hidden_state=hidden_states)
542
+
543
+
544
+ class PPLXQwen3ContextualModel(PPLXQwen3PreTrainedModel):
545
+ """
546
+ Qwen3 model with contextual encoding support for late chunking.
547
+
548
+ This model extends PPLXQwen3Model with an encode() method that supports both
549
+ standard encoding (list[str]) and contextual encoding (list[list[str]]) with late chunking.
550
+ """
551
+
552
+ def __init__(self, config):
553
+ super().__init__(config)
554
+ self.model = PPLXQwen3Model(config)
555
+ self.tokenizer = AutoTokenizer.from_pretrained(config._name_or_path)
556
+ self.quantizer = Int8TanhQuantizer(hard=True)
557
+ self.post_init()
558
+
559
+ def forward(
560
+ self,
561
+ input_ids: Optional[torch.LongTensor] = None,
562
+ attention_mask: Optional[torch.Tensor] = None,
563
+ position_ids: Optional[torch.LongTensor] = None,
564
+ inputs_embeds: Optional[torch.FloatTensor] = None,
565
+ **kwargs,
566
+ ) -> BaseModelOutputWithPast:
567
+ """Forward pass through the model."""
568
+ return self.model(
569
+ input_ids=input_ids,
570
+ attention_mask=attention_mask,
571
+ position_ids=position_ids,
572
+ inputs_embeds=inputs_embeds,
573
+ **kwargs,
574
+ )
575
+
576
+ @staticmethod
577
+ def mean_pooling(
578
+ token_embeddings: torch.Tensor, attention_mask: torch.Tensor
579
+ ) -> torch.Tensor:
580
+ """Apply mean pooling to token embeddings."""
581
+ input_mask_expanded = (
582
+ attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()
583
+ )
584
+ return torch.sum(token_embeddings * input_mask_expanded, 1) / torch.clamp(
585
+ input_mask_expanded.sum(1), min=1e-9
586
+ )
587
+
588
+ @torch.inference_mode()
589
+ def encode(
590
+ self,
591
+ documents: list[list[str]],
592
+ batch_size: int = 32,
593
+ show_progress_bar: bool = False,
594
+ device: str | torch.device | None = None,
595
+ normalize_embeddings: bool = False,
596
+ convert_to_numpy: bool = True,
597
+ ) -> list[np.ndarray] | list[torch.Tensor]:
598
+ """
599
+ Encode documents with late chunking (contextual embeddings).
600
+
601
+ This model is designed specifically for contextual encoding and always expects
602
+ documents as nested lists where each document is a list of text chunks.
603
+
604
+ The encoding process:
605
+ 1. Concatenate chunks with separator tokens
606
+ 2. Run forward pass to get token embeddings
607
+ 3. Extract and pool individual chunk embeddings (late chunking)
608
+ 4. Apply quantization (Int8 tanh quantization)
609
+ 5. Convert to numpy or return as tensors
610
+
611
+ Args:
612
+ documents: List of documents, where each document is a list of text chunks.
613
+ Example: [["chunk1", "chunk2"], ["chunk1", "chunk2", "chunk3"]]
614
+ batch_size: Batch size for encoding
615
+ show_progress_bar: Show progress bar during encoding
616
+ device: Device to use for computation (defaults to model's device)
617
+ normalize_embeddings: Normalize embeddings to unit length (applied before quantization)
618
+ convert_to_numpy: If True, returns list[np.ndarray], otherwise list[torch.Tensor]
619
+
620
+ Returns:
621
+ List of numpy arrays or tensors (preserves document structure).
622
+ Each element has shape (n_chunks, hidden_dim).
623
+ embeddings[0].shape = (2, 1024), embeddings[1].shape = (3, 1024)
624
+ With quantization, embeddings are int8 values in range [-128, 127].
625
+ """
626
+
627
+ if not isinstance(documents, list) or not all(
628
+ isinstance(doc, list) for doc in documents
629
+ ):
630
+ raise TypeError(
631
+ "Input 'documents' must be a list of lists of strings for contextual encoding."
632
+ )
633
+
634
+ self.eval()
635
+
636
+ if device is None:
637
+ device = next(self.parameters()).device
638
+
639
+ all_embeddings = []
640
+
641
+ range_iter = range(0, len(documents), batch_size)
642
+ if show_progress_bar:
643
+ try:
644
+ from tqdm import tqdm
645
+
646
+ range_iter = tqdm(range_iter, desc="Encoding documents")
647
+ except ImportError:
648
+ pass
649
+
650
+ for i in range_iter:
651
+ batch_docs = documents[i : i + batch_size]
652
+
653
+ doc_strings = [
654
+ self.tokenizer.sep_token.join(chunks) for chunks in batch_docs
655
+ ]
656
+
657
+ inputs = self.tokenizer(
658
+ doc_strings,
659
+ padding=True,
660
+ truncation=True,
661
+ return_tensors="pt",
662
+ )
663
+ inputs = {k: v.to(device) for k, v in inputs.items()}
664
+
665
+ outputs = self.forward(**inputs)
666
+ token_embeddings = outputs.last_hidden_state
667
+
668
+ batch_chunk_embeddings = self._extract_chunks_from_concatenated(
669
+ input_ids=inputs["input_ids"],
670
+ token_embeddings=token_embeddings,
671
+ attention_mask=inputs["attention_mask"],
672
+ )
673
+
674
+ batch_chunk_embeddings = [
675
+ torch.stack([chunk for chunk in doc_chunks], dim=0)
676
+ for doc_chunks in batch_chunk_embeddings
677
+ ]
678
+
679
+ if self.quantizer is not None:
680
+ batch_chunk_embeddings = [
681
+ self.quantizer(emb) for emb in batch_chunk_embeddings
682
+ ]
683
+
684
+ if normalize_embeddings:
685
+ batch_chunk_embeddings = [
686
+ torch.nn.functional.normalize(emb, p=2, dim=-1)
687
+ for emb in batch_chunk_embeddings
688
+ ]
689
+
690
+ batch_chunk_embeddings = [emb.cpu() for emb in batch_chunk_embeddings]
691
+
692
+ all_embeddings.extend(batch_chunk_embeddings)
693
+
694
+ # Convert to numpy if requested
695
+ if convert_to_numpy:
696
+ all_embeddings = [emb.numpy() for emb in all_embeddings]
697
+
698
+ return all_embeddings
699
+
700
+ def _extract_chunks_from_concatenated(
701
+ self,
702
+ input_ids: torch.Tensor,
703
+ token_embeddings: torch.Tensor,
704
+ attention_mask: torch.Tensor,
705
+ ) -> list[list[torch.Tensor]]:
706
+ """
707
+ Extract individual chunk embeddings from concatenated sequence using late chunking.
708
+
709
+ This method splits concatenated sequences like "[chunk1][SEP][chunk2][SEP]..."
710
+ back into individual chunk embeddings by finding SEP token positions.
711
+
712
+ Args:
713
+ input_ids: Token IDs (batch_size, seq_len)
714
+ token_embeddings: Token embeddings (batch_size, seq_len, hidden_dim)
715
+ attention_mask: Attention mask (batch_size, seq_len)
716
+
717
+ Returns:
718
+ list[list[torch.Tensor]]: List of documents, each containing list of chunk embeddings
719
+
720
+ Note:
721
+ The sep_token_id is retrieved from self.tokenizer.sep_token_id.
722
+ Common values: Qwen2=151643, BERT=102, varies by tokenizer.
723
+ """
724
+ sep_token_id = self.tokenizer.sep_token_id
725
+ batch_size = input_ids.shape[0]
726
+
727
+ all_doc_chunks = []
728
+
729
+ for batch_idx in range(batch_size):
730
+ # non-pad sep tokens
731
+ valid_positions = attention_mask[batch_idx].bool()
732
+ sep_positions = (
733
+ (input_ids[batch_idx] == sep_token_id) & valid_positions
734
+ ).nonzero(as_tuple=True)[0]
735
+
736
+ chunk_embeddings = []
737
+ start_pos = 0
738
+
739
+ for sep_pos in sep_positions:
740
+ chunk_tokens = token_embeddings[batch_idx, start_pos:sep_pos]
741
+ chunk_mask = attention_mask[batch_idx, start_pos:sep_pos]
742
+
743
+ chunk_emb = self.mean_pooling(
744
+ chunk_tokens.unsqueeze(0), chunk_mask.unsqueeze(0)
745
+ ).squeeze(0)
746
+
747
+ chunk_embeddings.append(chunk_emb)
748
+
749
+ start_pos = sep_pos + 1
750
+
751
+ # Handle the last chunk (after the last SEP token)
752
+ last_valid_pos = attention_mask[batch_idx].sum().item()
753
+
754
+ chunk_tokens = token_embeddings[batch_idx, start_pos:last_valid_pos]
755
+ chunk_mask = attention_mask[batch_idx, start_pos:last_valid_pos]
756
+
757
+ if chunk_mask.sum() > 0:
758
+ chunk_emb = self.mean_pooling(
759
+ chunk_tokens.unsqueeze(0), chunk_mask.unsqueeze(0)
760
+ ).squeeze(0)
761
+ else:
762
+ # Empty chunk - create zero embedding
763
+ chunk_emb = torch.zeros(
764
+ token_embeddings.shape[-1],
765
+ device=token_embeddings.device,
766
+ dtype=token_embeddings.dtype,
767
+ )
768
+
769
+ chunk_embeddings.append(chunk_emb)
770
+
771
+ all_doc_chunks.append(chunk_embeddings)
772
+
773
+ return all_doc_chunks
774
+
775
+
776
+ # Register for AutoModel
777
+ PPLXQwen3Model.register_for_auto_class("AutoModel")
778
+ PPLXQwen3ContextualModel.register_for_auto_class("AutoModel")
779
+
780
+ __all__ = [
781
+ "PPLXQwen3Config",
782
+ "PPLXQwen3Model",
783
+ "PPLXQwen3PreTrainedModel",
784
+ "PPLXQwen3ContextualModel",
785
+ "PPLXQwen3RMSNorm",
786
+ "PPLXQwen3MLP",
787
+ "PPLXQwen3RotaryEmbedding",
788
+ "PPLXQwen3Attention",
789
+ "PPLXQwen3DecoderLayer",
790
+ ]
modules.json CHANGED
@@ -15,6 +15,7 @@
15
  "idx": 2,
16
  "name": "2",
17
  "path": "",
18
- "type": "st_quantize.UnnormalizedInt8TanhQuantizer"
 
19
  }
20
  ]
 
15
  "idx": 2,
16
  "name": "2",
17
  "path": "",
18
+ "type": "st_quantize.FlexibleQuantizer",
19
+ "kwargs": ["quantization"]
20
  }
21
  ]
st_quantize.py CHANGED
@@ -1,6 +1,4 @@
1
  import torch
2
- from torch import nn
3
- from typing import Optional
4
  from typing import Literal
5
 
6
 
@@ -37,85 +35,58 @@ class Quantizer(torch.nn.Module):
37
  class Int8TanhQuantizer(Quantizer):
38
  def __init__(
39
  self,
40
- normalize: bool = False,
41
  hard: bool = True,
42
  ):
43
  super().__init__(hard=hard)
44
  self.qmin = -128
45
  self.qmax = 127
46
- self._normalize = normalize
47
 
48
  def _soft_quantize(self, x, *args, **kwargs):
49
- if self._normalize:
50
- x = (x - x.mean(dim=-1, keepdim=True)) / (
51
- x.std(dim=-1, keepdim=True) + 1e-8
52
- )
53
-
54
  return torch.tanh(x)
55
 
56
  def _hard_quantize(self, x, *args, **kwargs):
57
  soft = self._soft_quantize(x)
58
  int_x = torch.round(soft * self.qmax)
59
  int_x = torch.clamp(int_x, self.qmin, self.qmax)
60
- return int_x / self.qmax
61
-
62
 
63
- class UnnormalizedInt8TanhQuantizer(Int8TanhQuantizer):
64
- def __init__(self):
65
- super().__init__()
66
- self.quantizer = Int8TanhQuantizer(normalize=False)
67
 
68
- def forward(self, features: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
69
- features["sentence_embedding"] = self.quantizer(
70
- features["sentence_embedding"]
71
- )
72
- return features
73
-
74
- @classmethod
75
- def load(cls, input_path: str) -> "PoolAndQuantize":
76
- return cls()
77
-
78
-
79
- class NormalizedInt8TanhQuantizer(Int8TanhQuantizer):
80
- def __init__(self):
81
- super().__init__()
82
- self.quantizer = Int8TanhQuantizer(normalize=True)
83
 
84
- def forward(self, features: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
85
- features["sentence_embedding"] = self.quantizer(
86
- features["sentence_embedding"]
87
- )
88
- return features
89
-
90
- @classmethod
91
- def load(cls, input_path: str) -> "PoolAndQuantize":
92
- return cls()
93
-
94
-
95
- class Binarizer(Quantizer):
96
- def __init__(self, tanh_scale: float = 1.0, **kwargs):
97
- super().__init__(**kwargs)
98
- self._tanh_scale = tanh_scale
99
-
100
- def _hard_quantize(self, x, *args, **kwargs) -> torch.Tensor:
101
- return torch.where(x > 0, 1.0, -1.0)
102
 
103
- def _soft_quantize(self, x, *args, **kwargs) -> torch.Tensor:
104
- return torch.tanh(x * self._tanh_scale)
105
 
106
 
107
- class UnnormalizedBinarizer(nn.Module):
108
- def __init__(self, tanh_scale: float = 1.0, hard: bool = True):
109
  super().__init__()
110
- self.quantizer = Binarizer(tanh_scale=tanh_scale, hard=hard)
 
111
 
112
- def forward(self, features: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
113
- features["sentence_embedding"] = self.quantizer(
114
- features["sentence_embedding"]
115
- )
 
 
 
 
 
 
 
116
  return features
117
-
118
  @classmethod
119
- def load(cls, input_path: str) -> "UnnormalizedBinarizer":
120
  return cls()
121
 
 
1
  import torch
 
 
2
  from typing import Literal
3
 
4
 
 
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 FlexibleQuantizer(torch.nn.Module):
71
+ def __init__(self):
72
  super().__init__()
73
+ self._int8_quantizer = Int8TanhQuantizer()
74
+ self._binary_quantizer = BinaryTanhQuantizer()
75
 
76
+ def forward(self, features: dict[str, torch.Tensor], quantization: Literal["binary", "int8"] = "int8") -> dict[str, torch.Tensor]:
77
+ if quantization == "int8":
78
+ features["sentence_embedding"] = self._int8_quantizer(
79
+ features["sentence_embedding"]
80
+ )
81
+ elif quantization == "binary":
82
+ features["sentence_embedding"] = self._binary_quantizer(
83
+ features["sentence_embedding"]
84
+ )
85
+ else:
86
+ raise ValueError(f"Invalid quantization type: {quantization}. Must be 'binary' or 'int8'.")
87
  return features
88
+
89
  @classmethod
90
+ def load(cls, input_path: str):
91
  return cls()
92