GrEarl commited on
Commit
6d82c0b
·
verified ·
1 Parent(s): ed17348

finalize pinned NVFP4A16 checkpoint metadata

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
+ model.safetensors.index.json filter=lfs diff=lfs merge=lfs -text
LICENSE ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Kimi K3 License
2
+
3
+ Copyright (c) 2026 Moonshot AI
4
+
5
+ Permission is hereby granted, free of charge, to any person (the "Licensee")
6
+ obtaining a copy of this software — including the model weights, parameters,
7
+ configuration files, inference and training code, and associated documentation
8
+ (collectively, the "Software") — to deal in the Software without restriction.
9
+ This includes, without limitation, the rights to use, copy, modify, merge,
10
+ publish, distribute, sublicense, and/or sell copies of the Software; to run,
11
+ deploy, fine-tune, or otherwise modify the Software and create derivative works
12
+ from it; and to permit persons to whom the Software is furnished to do so, in
13
+ each case subject to the following conditions:
14
+
15
+ 1. The above copyright notice and this permission notice shall be included in
16
+ all copies or substantial portions of the Software. Licensee's use of the
17
+ Software must comply with applicable laws and regulations.
18
+
19
+ 2. "Model as a Service" means giving a third party access to language model
20
+ inference or fine-tuning (e.g., via API) in a manner that allows such third
21
+ party to exercise meaningful control over the inputs, parameters, or training
22
+ data. This does not include (a) end-user products with model capabilities solely
23
+ embedded within specific features or harnesses, or (b) mere relaying of requests
24
+ to models hosted by others.
25
+
26
+ If the Licensee or any of its affiliates operates a Model as a Service business,
27
+ and the aggregate revenue of the Licensee and its affiliates exceeds 20 million
28
+ US dollars (or the equivalent in other currencies) in total over any consecutive
29
+ 12 months, the Licensee must enter into a separate agreement with Moonshot AI
30
+ before using the Software or its derivative works for any commercial purpose.
31
+
32
+ 3. If the Software (or any derivative works thereof) is used for any of the
33
+ Licensee's commercial products or services that have more than 100 million
34
+ monthly active users, or more than 20 million US dollars (or equivalent in other
35
+ currencies) in monthly revenue, "Kimi K3" must be prominently displayed on the
36
+ user interface of such product or service.
37
+
38
+ 4. The requirements set forth in Sections 2 and 3 do not apply to: (a) internal
39
+ use of the Software, defined as any use that does not make the Software, its
40
+ outputs, or its underlying capabilities available to third parties; or (b) any
41
+ use of the Software accessed through Moonshot AI's official products or
42
+ certified inference partners.
43
+
44
+ 5. THE SOFTWARE AND ANY OUTPUT AND RESULTS THEREFROM ARE PROVIDED ON AN “AS IS”
45
+ BASIS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
46
+ LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
47
+ AND NONINFRINGEMENT. IN NO EVENT SHALL MOONSHOT AI OR ITS AFFILIATES OR
48
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
49
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
50
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
51
+
52
+ For any questions regarding this license, please contact <license@moonshot.ai>.
README.md CHANGED
@@ -1,15 +1,51 @@
1
  ---
 
 
 
2
  base_model: moonshotai/Kimi-K3
3
- tags: [compressed-tensors, nvfp4, nvfp4a16, upload-in-progress]
 
 
 
 
 
 
4
  ---
5
 
6
- # Kimi K3 NVFP4A16 — UPLOAD IN PROGRESS
7
 
8
- **Do not download or serve this repository yet.** The 96 weight shards are
9
- being uploaded. `model.safetensors.index.json` and the final conversion manifest
10
- will be published only after every expected shard is present exactly once.
11
 
12
- Mode: `requantized`
13
- Pinned source revision: [`9f62e4e9fffbd0a83ddd60e1c209d828994b3569`](https://huggingface.co/moonshotai/Kimi-K3/tree/9f62e4e9fffbd0a83ddd60e1c209d828994b3569)
14
- Lossless over-span policy: `fail; never silently requantize`
15
- Runtime load verification: **not yet performed**
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ license: other
3
+ license_name: kimi-k3
4
+ license_link: https://huggingface.co/moonshotai/Kimi-K3/blob/9f62e4e9fffbd0a83ddd60e1c209d828994b3569/LICENSE
5
  base_model: moonshotai/Kimi-K3
6
+ base_model_relation: quantized
7
+ tags:
8
+ - compressed-tensors
9
+ - nvfp4
10
+ - nvfp4a16
11
+ - kimi-k3
12
+ - moe
13
  ---
14
 
15
+ # Kimi-K3 NVFP4A16 — Standard Requantization
16
 
17
+ Weight-only NVFP4A16 derivative of source revision
18
+ [`moonshotai/Kimi-K3@9f62e4e9fffbd0a83ddd60e1c209d828994b3569`](https://huggingface.co/moonshotai/Kimi-K3/tree/9f62e4e9fffbd0a83ddd60e1c209d828994b3569).
 
19
 
20
+ ## Method
21
+
22
+ MXFP4 values are dequantized per tensor and requantized with the compressed-tensors memoryless_minmax NVFP4 algorithm. Gate/up experts share one global scale as required by fused MoE loaders; w2 uses its own tensor global scale.
23
+
24
+ Pinned shard 49, expert 0 real-weight sample versus MXFP4: w1 relative RMSE 0.064053 / cosine 0.998657480; w2 0.063678 / 0.998681610; w3 0.063978 / 0.998660965. All pass relative RMSE < 0.10 and cosine similarity > 0.99.
25
+
26
+ - Format: `compressed-tensors`, `nvfp4-pack-quantized`
27
+ - Weights: FP4 E2M1, tensor-group strategy, group size 16
28
+ - Local scales: FP8 E4M3FN
29
+ - Activations: unquantized (`input_activations: null`)
30
+ - Quantized modules: routed MoE experts only; all ignored K3 modules remain bit-identical
31
+ - Source revision: `9f62e4e9fffbd0a83ddd60e1c209d828994b3569`
32
+ - Source provenance: `pinned-staging-config-and-commit-window`
33
+ - Lossless over-span policy: `fail; never silently requantize`
34
+ - Tensor data size: 1.497 TiB
35
+ - Size change versus source tensor data: +5.45%
36
+ - Companion artifact: [`GrEarl/Kimi-K3-NVFP4A16-Transcoded`](https://huggingface.co/GrEarl/Kimi-K3-NVFP4A16-Transcoded)
37
+
38
+ This is **NVFP4A16**, not calibrated W4A4 NVFP4. Kimi K3 was trained
39
+ with MXFP8 activations; FP4 activation quantization would require a separate
40
+ calibration and quality-validation run.
41
+
42
+ ## Runtime status
43
+
44
+ **Runtime load has not been verified.** Static checkpoint structure and numeric
45
+ conversion have been validated, but no end-to-end load or generation was run in
46
+ the intended dedicated K3 vLLM/SGLang image. Do not treat structural validation
47
+ as runtime compatibility or performance evidence.
48
+
49
+ ## License
50
+
51
+ Inherits the [Kimi K3 License](https://huggingface.co/moonshotai/Kimi-K3/blob/9f62e4e9fffbd0a83ddd60e1c209d828994b3569/LICENSE).
configuration_kimi_k3.py ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+
3
+ from transformers.configuration_utils import PretrainedConfig
4
+
5
+
6
+ class KimiLinearConfig(PretrainedConfig):
7
+ model_type = "kimi_linear"
8
+ keys_to_ignore_at_inference = ["past_key_values"]
9
+
10
+ def __init__(
11
+ self,
12
+ model_type="kimi_linear",
13
+ vocab_size=163840,
14
+ hidden_size=4096,
15
+ head_dim=None,
16
+ intermediate_size=11008,
17
+ num_hidden_layers=32,
18
+ num_attention_heads=32,
19
+ num_key_value_heads=None,
20
+ hidden_act="silu",
21
+ initializer_range=0.02,
22
+ rms_norm_eps=1e-6,
23
+ use_cache=True,
24
+ pad_token_id=0,
25
+ bos_token_id=1,
26
+ eos_token_id=2,
27
+ rope_theta=10000.0,
28
+ rope_scaling=None,
29
+ tie_word_embeddings=False,
30
+ moe_intermediate_size: Optional[int] = None,
31
+ moe_renormalize: bool = True,
32
+ moe_router_activation_func: str = "sigmoid",
33
+ num_experts: Optional[int] = None,
34
+ num_experts_per_token: Optional[int] = None,
35
+ num_shared_experts: int = 0,
36
+ routed_scaling_factor: float = 1.0,
37
+ first_k_dense_replace: int = 0,
38
+ moe_layer_freq: int = 1,
39
+ use_grouped_topk: bool = True,
40
+ num_expert_group: int = 1,
41
+ topk_group: int = 1,
42
+ q_lora_rank: Optional[int] = None,
43
+ kv_lora_rank: Optional[int] = None,
44
+ qk_nope_head_dim: Optional[int] = None,
45
+ qk_rope_head_dim: Optional[int] = None,
46
+ v_head_dim: Optional[int] = None,
47
+ mla_use_nope: Optional[bool] = False,
48
+ mla_use_output_gate: Optional[bool] = False,
49
+ num_nextn_predict_layers: int = 0,
50
+ linear_attn_config: Optional[dict] = None,
51
+ attn_res_block_size: Optional[int] = None,
52
+ latent_moe_use_norm: bool = False,
53
+ activation_situ_beta: Optional[float] = None,
54
+ activation_situ_linear_beta: Optional[float] = None,
55
+ max_position_embeddings: int = 4096,
56
+ routed_expert_hidden_size: Optional[int] = None,
57
+ topk_method: str = "noaux_tc",
58
+ **kwargs,
59
+ ):
60
+ self.model_type = model_type
61
+ self.vocab_size = vocab_size
62
+ self.hidden_size = hidden_size
63
+ self.head_dim = (
64
+ head_dim if head_dim is not None else hidden_size // num_attention_heads
65
+ )
66
+ self.intermediate_size = intermediate_size
67
+ self.num_hidden_layers = num_hidden_layers
68
+ self.num_attention_heads = num_attention_heads
69
+
70
+ # for backward compatibility
71
+ if num_key_value_heads is None:
72
+ num_key_value_heads = num_attention_heads
73
+
74
+ self.num_key_value_heads = num_key_value_heads
75
+ self.hidden_act = hidden_act
76
+ self.initializer_range = initializer_range
77
+ self.rms_norm_eps = rms_norm_eps
78
+ self.use_cache = use_cache
79
+ self.rope_theta = rope_theta
80
+ self.rope_scaling = rope_scaling
81
+
82
+ self.q_lora_rank = q_lora_rank
83
+ self.kv_lora_rank = kv_lora_rank
84
+ self.qk_nope_head_dim = qk_nope_head_dim
85
+ self.qk_rope_head_dim = qk_rope_head_dim
86
+ self.v_head_dim = v_head_dim
87
+ self.mla_use_nope = mla_use_nope
88
+ self.mla_use_output_gate = mla_use_output_gate
89
+ # moe config
90
+ self.num_experts = num_experts
91
+ self.num_experts_per_token = num_experts_per_token
92
+ self.moe_renormalize = moe_renormalize
93
+ self.num_shared_experts = num_shared_experts
94
+ self.routed_scaling_factor = routed_scaling_factor
95
+ self.moe_router_activation_func = moe_router_activation_func
96
+ assert self.moe_router_activation_func in ("softmax", "sigmoid")
97
+ self.moe_intermediate_size = moe_intermediate_size
98
+ self.first_k_dense_replace = first_k_dense_replace
99
+ self.moe_layer_freq = moe_layer_freq
100
+ self.use_grouped_topk = use_grouped_topk
101
+ self.num_expert_group = num_expert_group
102
+ self.topk_group = topk_group
103
+ self.num_nextn_predict_layers = num_nextn_predict_layers
104
+
105
+ self.attn_res_block_size = attn_res_block_size
106
+ self.latent_moe_use_norm = latent_moe_use_norm
107
+ self.activation_situ_beta = activation_situ_beta
108
+ self.activation_situ_linear_beta = activation_situ_linear_beta
109
+ self.max_position_embeddings = max_position_embeddings
110
+ self.routed_expert_hidden_size = routed_expert_hidden_size
111
+ self.topk_method = topk_method
112
+
113
+ if linear_attn_config is not None:
114
+ assert linear_attn_config["kda_layers"] is not None
115
+ assert linear_attn_config["full_attn_layers"] is not None
116
+ self.linear_attn_config = linear_attn_config
117
+
118
+ super().__init__(
119
+ pad_token_id=pad_token_id,
120
+ bos_token_id=bos_token_id,
121
+ eos_token_id=eos_token_id,
122
+ tie_word_embeddings=tie_word_embeddings,
123
+ **kwargs,
124
+ )
125
+
126
+ @property
127
+ def is_mla(self):
128
+ return (
129
+ self.q_lora_rank is not None
130
+ or self.kv_lora_rank is not None
131
+ or self.qk_nope_head_dim is not None
132
+ or self.qk_rope_head_dim is not None
133
+ or self.v_head_dim is not None
134
+ or self.mla_use_nope is True
135
+ )
136
+
137
+ @property
138
+ def is_moe(self):
139
+ return self.num_experts is not None
140
+
141
+ @property
142
+ def is_linear_attn(self) -> bool:
143
+ return not (
144
+ self.linear_attn_config is None
145
+ or (
146
+ isinstance(self.linear_attn_config, dict)
147
+ and self.linear_attn_config["kda_layers"] is not None
148
+ and len(self.linear_attn_config["kda_layers"]) == 0
149
+ )
150
+ )
151
+
152
+ def is_kda_layer(self, layer_idx: int):
153
+ return (
154
+ self.linear_attn_config is not None
155
+ and (layer_idx + 1) in self.linear_attn_config["kda_layers"]
156
+ )
157
+
158
+
159
+ class KimiK3VisionConfig(PretrainedConfig):
160
+
161
+ def __init__(
162
+ self,
163
+ patch_size: int = 14,
164
+ init_pos_emb_height: int = 64,
165
+ init_pos_emb_width: int = 64,
166
+ init_pos_emb_time: int = 4,
167
+ pos_emb_type: str = 'divided_fixed',
168
+ vt_num_attention_heads: int = 12,
169
+ vt_num_hidden_layers: int = 27,
170
+ vt_hidden_size: int = 1024,
171
+ vt_intermediate_size: int = 4096,
172
+ merge_kernel_size: tuple = (2, 2),
173
+ merge_type: str = 'sd2_tpool',
174
+ _attn_implementation: str = 'flash_attention_2',
175
+ # MM Projector parameters
176
+ mm_projector_type: str = 'patchmergerv2',
177
+ mm_hidden_size: int | None = None,
178
+ projector_hidden_act: str = "gelu",
179
+ projector_ln_eps: float = 1e-5,
180
+ # vision tower parameters
181
+ qkv_hidden_size: int = 1536,
182
+ norm_type: str = 'rmsnorm',
183
+ attn_bias: bool = False,
184
+ patch_embed_proj_bias: bool = False,
185
+ mlp_type: str = 'mlp2',
186
+ linear_bias: bool = False,
187
+ activation_func: str = 'gelu_pytorch_tanh',
188
+ pos_emb_interpolation_mode: str = 'bilinear',
189
+ # Other parameters
190
+ ignore_index: int = -100,
191
+ media_placeholder_token_id: int = 163605,
192
+ pad_token_id: int = 0,
193
+ text_hidden_size=7168,
194
+ **kwargs):
195
+
196
+ self.patch_size = patch_size
197
+ self.init_pos_emb_height = init_pos_emb_height
198
+ self.init_pos_emb_width = init_pos_emb_width
199
+ self.init_pos_emb_time = init_pos_emb_time
200
+ self.pos_emb_type = pos_emb_type
201
+ self.vt_num_attention_heads = vt_num_attention_heads
202
+ self.vt_num_hidden_layers = vt_num_hidden_layers
203
+ self.vt_hidden_size = vt_hidden_size
204
+ self.vt_intermediate_size = vt_intermediate_size
205
+ self.merge_kernel_size = merge_kernel_size
206
+ self.merge_type = merge_type
207
+ self._attn_implementation = _attn_implementation
208
+
209
+ # MM Projector config
210
+ self.mm_projector_type = mm_projector_type
211
+ self.mm_hidden_size = mm_hidden_size if mm_hidden_size is not None else vt_hidden_size
212
+ self.projector_hidden_act = projector_hidden_act
213
+ self.projector_ln_eps = projector_ln_eps
214
+ self.text_hidden_size = text_hidden_size
215
+
216
+ # vision tower parameters
217
+ self.qkv_hidden_size = qkv_hidden_size
218
+ self.norm_type = norm_type
219
+ self.attn_bias = attn_bias
220
+ self.patch_embed_proj_bias = patch_embed_proj_bias
221
+ self.mlp_type = mlp_type
222
+ self.linear_bias = linear_bias
223
+ self.activation_func = activation_func
224
+ self.pos_emb_interpolation_mode = pos_emb_interpolation_mode
225
+
226
+ super().__init__(**kwargs)
227
+
228
+
229
+ class KimiK3Config(PretrainedConfig):
230
+ """Kimi-K3 model configuration.
231
+
232
+ Args:
233
+ text_config (dict | KimiLinearConfig): Configuration for the text model.
234
+
235
+ Vision Tower Parameters (from MoonViT3dConfig):
236
+ patch_size (int): Patch size for vision tower.
237
+ init_pos_emb_height (int): Initial position embedding height.
238
+ init_pos_emb_width (int): Initial position embedding width.
239
+ init_pos_emb_time (int): Initial position embedding time dimension.
240
+ pos_emb_type (str): Type of position embedding.
241
+ vt_num_attention_heads (int): Number of attention heads in vision tower.
242
+ vt_num_hidden_layers (int): Number of hidden layers in vision tower.
243
+ vt_hidden_size (int): Hidden size of vision tower.
244
+ vt_intermediate_size (int): Intermediate size in vision tower FFN.
245
+ merge_kernel_size (tuple): Kernel size for patch merging.
246
+ merge_type (str): Type of merge operation.
247
+ _attn_implementation (str): Attention implementation type.
248
+
249
+ MM Projector Parameters (from MultiModalProjectorConfig):
250
+ mm_projector_type (str): Type of multimodal projector.
251
+ mm_hidden_size (int): Hidden size from vision tower (should match vt_hidden_size).
252
+ projector_hidden_act (str): Activation function for projector.
253
+ projector_ln_eps (float): Layer norm epsilon for projector.
254
+
255
+ Other Parameters:
256
+ ignore_index (int): The ignore index for the loss function.
257
+ media_placeholder_token_id (int): The token ID to use for media placeholders.
258
+ pad_token_id (int): The token ID to use for padding.
259
+ """
260
+
261
+ model_type = "kimi_k3"
262
+
263
+ def __init__(
264
+ self,
265
+ text_config: dict | KimiLinearConfig = None,
266
+ vision_config: dict | KimiK3VisionConfig = None,
267
+ # Other parameters
268
+ ignore_index: int = -100,
269
+ media_placeholder_token_id: int = 163605,
270
+ pad_token_id: int = 0,
271
+ **kwargs,
272
+ ):
273
+ if isinstance(text_config, dict):
274
+ text_config = KimiLinearConfig(**text_config)
275
+ if isinstance(vision_config, dict):
276
+ vision_config = KimiK3VisionConfig(**vision_config)
277
+ self.text_config = text_config
278
+ self.vision_config = vision_config
279
+ # Other config
280
+ self.ignore_index = ignore_index
281
+ self.media_placeholder_token_id = media_placeholder_token_id
282
+ if getattr(self.text_config, "quantization_config", None) is not None:
283
+ self.quantization_config = self.text_config.quantization_config
284
+
285
+ super().__init__(pad_token_id=pad_token_id, **kwargs)
encoding_k3.py ADDED
@@ -0,0 +1,647 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Kimi K3 XTML encoding helpers.
2
+
3
+ This module keeps chat rendering in Python.
4
+ Callers that need token IDs should consume ``EncodeSegment`` objects directly:
5
+ structural markers may be encoded as tiktoken special tokens, while user/tool
6
+ text and attribute values are encoded as ordinary text.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from dataclasses import dataclass
13
+ from typing import Any, Iterable, Optional
14
+
15
+ OPEN_TOKEN = "<|open|>"
16
+ CLOSE_TOKEN = "<|close|>"
17
+ SEP_TOKEN = "<|sep|>"
18
+ END_OF_MSG_TOKEN = "<|end_of_msg|>"
19
+ IMAGE_PLACEHOLDER = "<|kimi_image_placeholder|>"
20
+
21
+ _VALID_THINKING_EFFORTS = {"low", "high", "max"}
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class EncodeSegment:
26
+ text: str
27
+ allow_special: bool = False
28
+
29
+
30
+ class _ImagePromptState:
31
+ def __init__(self, image_prompts: Optional[list[str]] = None):
32
+ self.image_prompts = image_prompts
33
+ self.index = 0
34
+
35
+ def next_prompt(self) -> str:
36
+ if self.image_prompts is None:
37
+ return IMAGE_PLACEHOLDER
38
+ if self.index >= len(self.image_prompts):
39
+ raise ValueError("More image placeholders than image prompts.")
40
+ prompt = self.image_prompts[self.index]
41
+ self.index += 1
42
+ return prompt
43
+
44
+ def assert_consumed(self) -> None:
45
+ if self.image_prompts is None:
46
+ return
47
+ if self.index != len(self.image_prompts):
48
+ raise ValueError(
49
+ f"image prompt count {len(self.image_prompts)} != "
50
+ f"consumed placeholder count {self.index}"
51
+ )
52
+
53
+
54
+ def _segment(text: Any, *, allow_special: bool = False) -> list[EncodeSegment]:
55
+ text = str(text)
56
+ if not text:
57
+ return []
58
+ return [EncodeSegment(text, allow_special=allow_special)]
59
+
60
+
61
+ def _control(text: str) -> list[EncodeSegment]:
62
+ return _segment(text, allow_special=True)
63
+
64
+
65
+ def _text(text: Any) -> list[EncodeSegment]:
66
+ return _segment(text, allow_special=False)
67
+
68
+
69
+ def _append_text(
70
+ segments: list[EncodeSegment],
71
+ text: Any,
72
+ image_state: _ImagePromptState,
73
+ ) -> None:
74
+ text = str(text)
75
+ if text == "":
76
+ return
77
+ if image_state.image_prompts is None or IMAGE_PLACEHOLDER not in text:
78
+ segments.extend(_text(text))
79
+ return
80
+
81
+ parts = text.split(IMAGE_PLACEHOLDER)
82
+ for i, part in enumerate(parts):
83
+ segments.extend(_text(part))
84
+ if i < len(parts) - 1:
85
+ segments.extend(_segment(image_state.next_prompt(),
86
+ allow_special=True))
87
+
88
+
89
+ def _escape_attr_value(value: Any) -> str:
90
+ return str(value).replace("&", "&amp;").replace('"', "&quot;")
91
+
92
+
93
+ def _attr(key: str, value: Any) -> list[EncodeSegment]:
94
+ return (
95
+ _text(f" {key}")
96
+ + _text('="')
97
+ + _text(_escape_attr_value(value))
98
+ + _text('"')
99
+ )
100
+
101
+
102
+ def _open_tag(tag: str, attrs: Iterable[tuple[str, Any]] = ()) -> list[EncodeSegment]:
103
+ segments: list[EncodeSegment] = []
104
+ segments.extend(_control(OPEN_TOKEN))
105
+ segments.extend(_text(tag))
106
+ for key, value in attrs:
107
+ segments.extend(_attr(key, value))
108
+ segments.extend(_control(SEP_TOKEN))
109
+ return segments
110
+
111
+ def _close_tag(tag: str) -> list[EncodeSegment]:
112
+ segments: list[EncodeSegment] = []
113
+ segments.extend(_control(CLOSE_TOKEN))
114
+ segments.extend(_text(tag))
115
+ segments.extend(_control(SEP_TOKEN))
116
+ return segments
117
+
118
+
119
+ def _end_of_msg() -> list[EncodeSegment]:
120
+ return _control(END_OF_MSG_TOKEN)
121
+
122
+
123
+ def _json_compact(value: Any) -> str:
124
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
125
+
126
+
127
+ def _is_mapping(value: Any) -> bool:
128
+ return isinstance(value, dict)
129
+
130
+
131
+ def _xtml_type(value: Any) -> str:
132
+ if isinstance(value, bool):
133
+ return "boolean"
134
+ if value is None:
135
+ return "null"
136
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
137
+ return "number"
138
+ if isinstance(value, str):
139
+ return "string"
140
+ if _is_mapping(value):
141
+ return "object"
142
+ return "array"
143
+
144
+
145
+ def _xtml_value(value: Any) -> str:
146
+ if isinstance(value, str):
147
+ return value
148
+ return json.dumps(value, ensure_ascii=False)
149
+
150
+
151
+ def _get_value(obj: Any, key: str, default: Any = None) -> Any:
152
+ if isinstance(obj, dict):
153
+ return obj.get(key, default)
154
+ return getattr(obj, key, default)
155
+
156
+
157
+ def extract_response_schema(response_format: Any) -> Any:
158
+ if response_format is None:
159
+ return None
160
+
161
+ json_schema = _get_value(response_format, "json_schema")
162
+ if json_schema is None:
163
+ return None
164
+
165
+ if isinstance(json_schema, dict):
166
+ return json_schema.get(
167
+ "schema",
168
+ json_schema.get("json_schema", json_schema),
169
+ )
170
+
171
+ schema = _get_value(json_schema, "schema")
172
+ if schema is not None:
173
+ return schema
174
+
175
+ schema = _get_value(json_schema, "json_schema")
176
+ if schema is not None:
177
+ return schema
178
+
179
+ return json_schema
180
+
181
+
182
+ def deep_sort_dict(obj: Any) -> Any:
183
+ if isinstance(obj, dict):
184
+ return {k: deep_sort_dict(v) for k, v in sorted(obj.items())}
185
+ if isinstance(obj, list):
186
+ return [deep_sort_dict(item) for item in obj]
187
+ return obj
188
+
189
+
190
+ def normalize_tool_arguments(arguments: Any) -> tuple[dict[str, Any], Optional[str]]:
191
+ if arguments is None:
192
+ return {}, None
193
+ if isinstance(arguments, dict):
194
+ return arguments, None
195
+ if isinstance(arguments, str):
196
+ if not arguments.strip():
197
+ return {}, None
198
+ try:
199
+ parsed = json.loads(arguments)
200
+ except json.JSONDecodeError:
201
+ return {}, arguments
202
+ if not isinstance(parsed, dict):
203
+ raise ValueError("Kimi K3 tool call arguments must be a JSON object.")
204
+ return parsed, None
205
+ raise TypeError(
206
+ "Kimi K3 tool call arguments must be a dict or a JSON object string."
207
+ )
208
+
209
+
210
+ def normalize_message(message: Any) -> Any:
211
+ if not isinstance(message, dict):
212
+ return message
213
+
214
+ normalized = dict(message)
215
+
216
+ tools = normalized.get("tools")
217
+ if tools is not None:
218
+ normalized["tools"] = deep_sort_dict(tools)
219
+
220
+ tool_calls = normalized.get("tool_calls")
221
+ if not tool_calls:
222
+ return normalized
223
+
224
+ normalized_calls = []
225
+ for tool_call in tool_calls:
226
+ if not isinstance(tool_call, dict):
227
+ normalized_calls.append(tool_call)
228
+ continue
229
+
230
+ tc = dict(tool_call)
231
+ function = tc.get("function")
232
+ if isinstance(function, dict):
233
+ fn = dict(function)
234
+ arguments, json_block = normalize_tool_arguments(fn.get("arguments"))
235
+ fn["arguments"] = arguments
236
+ if json_block is None:
237
+ fn.pop("_xtml_json_block", None)
238
+ else:
239
+ fn["_xtml_json_block"] = json_block
240
+ tc["function"] = fn
241
+ else:
242
+ arguments, json_block = normalize_tool_arguments(tc.get("arguments"))
243
+ tc["arguments"] = arguments
244
+ if json_block is None:
245
+ tc.pop("_xtml_json_block", None)
246
+ else:
247
+ tc["_xtml_json_block"] = json_block
248
+ normalized_calls.append(tc)
249
+
250
+ normalized["tool_calls"] = normalized_calls
251
+ return normalized
252
+
253
+
254
+ def normalize_conversation(conversation: Any) -> Any:
255
+ if not isinstance(conversation, list):
256
+ return conversation
257
+
258
+ def normalize_messages(messages: list[Any]) -> list[Any]:
259
+ return [normalize_message(message) for message in messages]
260
+
261
+ if conversation and isinstance(conversation[0], list):
262
+ return [normalize_messages(messages) for messages in conversation]
263
+ return normalize_messages(conversation)
264
+
265
+
266
+ def _tool_call_id_index(tool_calls: Any) -> dict:
267
+ """Map assistant ``tool_calls[].id`` to ``(1-based position, function name)``.
268
+
269
+ The position mirrors the chat template's enumeration over ``tool_calls``
270
+ (every entry advances the position, even an id-less one). Duplicate ids keep
271
+ their first occurrence.
272
+ """
273
+ index: dict = {}
274
+ if not isinstance(tool_calls, list):
275
+ return index
276
+ for position, tool_call in enumerate(tool_calls, start=1):
277
+ if not isinstance(tool_call, dict):
278
+ continue
279
+ call_id = tool_call.get("id")
280
+ if call_id is None:
281
+ continue
282
+ key = str(call_id)
283
+ if key in index:
284
+ continue
285
+ function = tool_call.get("function")
286
+ name = (
287
+ function.get("name") if isinstance(function, dict) else tool_call.get("name")
288
+ )
289
+ index[key] = (position, name)
290
+ return index
291
+
292
+
293
+ def normalize_xtml_tool_result_messages(messages: list[Any]) -> list[Any]:
294
+ """Re-sort K3 XTML tool results into assistant ``tool_calls`` order.
295
+
296
+ Serving frameworks generally deliver tool results already in call order. A
297
+ direct Transformers caller, however, may pass OpenAI-style tool messages in any
298
+ order, so each run of consecutive tool messages is matched against the most
299
+ recent preceding assistant ``tool_calls`` by opaque ``tool_call_id`` ==
300
+ ``tool_calls[].id`` (K3 drops the ``func:index`` format requirement) and
301
+ sorted by the matched 1-based position. The matched call is authoritative,
302
+ so each matched message's ``tool`` is set to that call's function name --
303
+ this keeps an explicit (and possibly stale) ``tool``/``name`` from drifting
304
+ out of sync with the reordered position. ``index`` is still derived from the
305
+ rendered position by the chat template. A run that cannot be fully matched is
306
+ left untouched. Re-running is idempotent.
307
+
308
+ This function is side-effect free: matched tool messages are shallow-copied
309
+ before their ``tool``/``name`` is rewritten, and every other message is
310
+ appended to the output as-is. The input list and its message objects are
311
+ never mutated.
312
+ """
313
+ if not isinstance(messages, list):
314
+ return messages
315
+
316
+ output: list[Any] = []
317
+ current_index: dict = {}
318
+ i = 0
319
+ n = len(messages)
320
+
321
+ while i < n:
322
+ message = messages[i]
323
+
324
+ if isinstance(message, dict) and message.get("role") == "assistant":
325
+ tool_calls = message.get("tool_calls")
326
+ current_index = _tool_call_id_index(tool_calls) if tool_calls else {}
327
+ output.append(message)
328
+ i += 1
329
+ continue
330
+
331
+ if not isinstance(message, dict) or message.get("role") != "tool":
332
+ output.append(message)
333
+ i += 1
334
+ continue
335
+
336
+ run: list[tuple] = [] # (position, original_offset, message, name)
337
+ unresolved = False
338
+ offset = 0
339
+ while (
340
+ i < n and isinstance(messages[i], dict) and messages[i].get("role") == "tool"
341
+ ):
342
+ tool_message = messages[i]
343
+ call_id = tool_message.get("tool_call_id", tool_message.get("id"))
344
+ matched = current_index.get(str(call_id)) if call_id is not None else None
345
+ if matched is None:
346
+ unresolved = True
347
+ run.append((None, offset, tool_message, None))
348
+ else:
349
+ position, name = matched
350
+ run.append((position, offset, tool_message, name))
351
+ offset += 1
352
+ i += 1
353
+
354
+ if unresolved:
355
+ output.extend(item[2] for item in run)
356
+ else:
357
+ run.sort(key=lambda item: (item[0], item[1]))
358
+ for _, _, tool_message, name in run:
359
+ if name is None:
360
+ output.append(tool_message)
361
+ continue
362
+ # The id-matched call is authoritative: align tool (and any
363
+ # explicit name) so the rendered XTML tool attribute cannot
364
+ # disagree with the reordered position. Copy first so the
365
+ # caller's message object is never mutated.
366
+ resolved = dict(tool_message)
367
+ resolved["tool"] = name
368
+ if "name" in resolved:
369
+ resolved["name"] = name
370
+ output.append(resolved)
371
+
372
+ return output
373
+
374
+
375
+ def is_batched_conversation(conversation: Any) -> bool:
376
+ return (
377
+ isinstance(conversation, list)
378
+ and bool(conversation)
379
+ and isinstance(conversation[0], list)
380
+ )
381
+
382
+
383
+ def _render_content_segments(
384
+ content: Any,
385
+ image_state: _ImagePromptState,
386
+ ) -> list[EncodeSegment]:
387
+ segments: list[EncodeSegment] = []
388
+ if isinstance(content, str):
389
+ _append_text(segments, content, image_state)
390
+ elif content is not None:
391
+ for part in content:
392
+ if part["type"] in ["image", "image_url"]:
393
+ segments.extend(
394
+ _segment(image_state.next_prompt(), allow_special=True))
395
+ else:
396
+ _append_text(segments, part["text"], image_state)
397
+ return segments
398
+
399
+
400
+ def _internal_system_message(message_type: str, body: str) -> list[EncodeSegment]:
401
+ segments: list[EncodeSegment] = []
402
+ segments.extend(_open_tag("message", [("role", "system"), ("type", message_type)]))
403
+ segments.extend(_text(body.strip()))
404
+ segments.extend(_close_tag("message"))
405
+ segments.extend(_end_of_msg())
406
+ return segments
407
+
408
+
409
+ def _render_assistant_segments(
410
+ message: dict[str, Any],
411
+ image_state: _ImagePromptState,
412
+ thinking: bool = True,
413
+ ) -> list[EncodeSegment]:
414
+ segments: list[EncodeSegment] = []
415
+ # The <think> channel is structural: in thinking mode every assistant
416
+ # message carries the open/close tags even when there is no reasoning
417
+ # content to fill in. In non-thinking mode the channel is dropped
418
+ # entirely.
419
+ if thinking:
420
+ reasoning_content = message.get("reasoning_content") or message.get(
421
+ "reasoning"
422
+ )
423
+ segments.extend(_open_tag("think"))
424
+ if reasoning_content is not None and str(reasoning_content).strip():
425
+ _append_text(segments, reasoning_content, image_state)
426
+ segments.extend(_close_tag("think"))
427
+
428
+ segments.extend(_open_tag("response"))
429
+ segments.extend(_render_content_segments(message.get("content"), image_state))
430
+ segments.extend(_close_tag("response"))
431
+
432
+ tool_calls = message.get("tool_calls")
433
+ if tool_calls:
434
+ segments.extend(_open_tag("tools"))
435
+ for index, tool_call in enumerate(tool_calls, start=1):
436
+ fn = tool_call.get("function", tool_call)
437
+ segments.extend(
438
+ _open_tag("call", [("tool", fn["name"]), ("index", index)])
439
+ )
440
+ args = fn.get("arguments", {})
441
+ json_block = fn.get("_xtml_json_block")
442
+ if json_block is not None:
443
+ segments.extend(_open_tag("json", [("type", "object")]))
444
+ _append_text(segments, json_block, image_state)
445
+ segments.extend(_close_tag("json"))
446
+ elif _is_mapping(args):
447
+ for key, value in args.items():
448
+ segments.extend(
449
+ _open_tag(
450
+ "argument",
451
+ [("key", key), ("type", _xtml_type(value))],
452
+ )
453
+ )
454
+ _append_text(segments, _xtml_value(value), image_state)
455
+ segments.extend(_close_tag("argument"))
456
+ segments.extend(_close_tag("call"))
457
+ segments.extend(_close_tag("tools"))
458
+
459
+ return segments
460
+
461
+
462
+ def _render_tool_declare(tools: Any, *, dynamic: bool = False) -> list[EncodeSegment]:
463
+ if dynamic:
464
+ body = (
465
+ "## New Tools Available\n"
466
+ "The system dynamically extends the toolset via lazy-loading.\n"
467
+ "You have access to all existing and extended tools.\n"
468
+ "Here are the specs for the extended tools.\n\n"
469
+ "```json\n"
470
+ f"{_json_compact(tools)}\n"
471
+ "```"
472
+ )
473
+ else:
474
+ body = (
475
+ "# Tools\n"
476
+ "Here are the available tools, described in JSONSchema.\n\n"
477
+ "```json\n"
478
+ f"{_json_compact(tools)}\n"
479
+ "```"
480
+ )
481
+ segments: list[EncodeSegment] = []
482
+ segments.extend(_open_tag("message", [("role", "system"), ("type", "tool-declare")]))
483
+ segments.extend(_text(body))
484
+ segments.extend(_close_tag("message"))
485
+ segments.extend(_end_of_msg())
486
+ return segments
487
+
488
+
489
+ def build_chat_segments(
490
+ messages: list[Any],
491
+ tools: Optional[list[dict]] = None,
492
+ *,
493
+ add_generation_prompt: bool = True,
494
+ thinking: bool = True,
495
+ image_prompts: Optional[list[str]] = None,
496
+ **kwargs: Any,
497
+ ) -> list[EncodeSegment]:
498
+ # Re-sort tool results by tool_call_id at the lowest layer so every caller
499
+ # (processor or direct tokenizer) gets correctly ordered XTML. The helper is
500
+ # side-effect free, so the caller's message objects are left untouched.
501
+ messages = normalize_xtml_tool_result_messages(messages)
502
+ messages = normalize_conversation(messages)
503
+ tools = deep_sort_dict(tools)
504
+
505
+ kwargs = dict(kwargs)
506
+ response_format = kwargs.get("response_format")
507
+ if "response_schema" not in kwargs:
508
+ response_schema = extract_response_schema(response_format)
509
+ if response_schema is not None:
510
+ kwargs["response_schema"] = response_schema
511
+ if kwargs.get("response_schema") is not None:
512
+ kwargs["response_schema"] = deep_sort_dict(kwargs["response_schema"])
513
+
514
+ image_state = _ImagePromptState(image_prompts)
515
+ segments: list[EncodeSegment] = []
516
+
517
+ tool_calls = None
518
+ tool_index = 0
519
+
520
+ if tools:
521
+ segments.extend(_render_tool_declare(tools))
522
+
523
+ thinking_effort = kwargs.get("thinking_effort")
524
+ if thinking and thinking_effort is not None:
525
+ assert thinking_effort in _VALID_THINKING_EFFORTS, (
526
+ f"Unsupported thinking_effort={thinking_effort!r}; "
527
+ f"supported values are {sorted(_VALID_THINKING_EFFORTS)}."
528
+ )
529
+ if thinking and thinking_effort in _VALID_THINKING_EFFORTS:
530
+ segments.extend(
531
+ _internal_system_message(
532
+ "thinking-effort",
533
+ "`thinking_effort` guides on how much to think in your "
534
+ "thinking channel (not including the response channel), "
535
+ "supported values include `low`, `medium`, `high`, and `max`.\n"
536
+ f"Now the system is invoked with `thinking_effort={thinking_effort}`.",
537
+ )
538
+ )
539
+
540
+ for message_index, message in enumerate(messages):
541
+ if not isinstance(message, dict):
542
+ continue
543
+
544
+ role = message["role"]
545
+ if role == "user":
546
+ attrs = [("role", "user")]
547
+ if message.get("name"):
548
+ attrs.append(("name", message["name"]))
549
+ segments.extend(_open_tag("message", attrs))
550
+ segments.extend(_render_content_segments(message.get("content"), image_state))
551
+ segments.extend(_close_tag("message"))
552
+ segments.extend(_end_of_msg())
553
+ elif role == "system" and message.get("tools"):
554
+ segments.extend(_render_tool_declare(message["tools"], dynamic=True))
555
+ elif role == "system":
556
+ attrs = [("role", "system")]
557
+ if message.get("name"):
558
+ attrs.append(("name", message["name"]))
559
+ segments.extend(_open_tag("message", attrs))
560
+ segments.extend(_render_content_segments(message.get("content"), image_state))
561
+ segments.extend(_close_tag("message"))
562
+ segments.extend(_end_of_msg())
563
+ elif role == "tool":
564
+ tool_index += 1
565
+ tool_name = message.get("tool", message.get("name"))
566
+ if (
567
+ tool_name is None
568
+ and tool_calls is not None
569
+ and tool_index <= len(tool_calls)
570
+ ):
571
+ tc = tool_calls[tool_index - 1]
572
+ fn = tc.get("function", tc)
573
+ tool_name = fn["name"]
574
+ if tool_name is None:
575
+ raise ValueError(
576
+ "Kimi K3 tool messages need a resolvable tool name: "
577
+ "carry `tool`/`name`, or match a preceding assistant "
578
+ "tool_call by order."
579
+ )
580
+ segments.extend(
581
+ _open_tag(
582
+ "message",
583
+ [("role", "tool"), ("tool", tool_name), ("index", tool_index)],
584
+ )
585
+ )
586
+ segments.extend(_render_content_segments(message.get("content"), image_state))
587
+ segments.extend(_close_tag("message"))
588
+ segments.extend(_end_of_msg())
589
+ elif role == "assistant":
590
+ tool_calls = message.get("tool_calls")
591
+ tool_index = 0
592
+ attrs = [("role", "assistant")]
593
+ if message.get("name"):
594
+ attrs.append(("name", message["name"]))
595
+ segments.extend(_open_tag("message", attrs))
596
+ segments.extend(_render_assistant_segments(message, image_state, thinking))
597
+ segments.extend(_close_tag("message"))
598
+ segments.extend(_end_of_msg())
599
+
600
+ tool_choice = kwargs.get("tool_choice")
601
+ if tool_choice == "required":
602
+ segments.extend(
603
+ _internal_system_message(
604
+ "tool-choice",
605
+ "The system is invoked with `tool_choice=required`.\n"
606
+ "You MUST call tools in the next message.",
607
+ )
608
+ )
609
+ elif tool_choice == "none":
610
+ segments.extend(
611
+ _internal_system_message(
612
+ "tool-choice",
613
+ "The system is invoked with `tool_choice=none`.\n"
614
+ "You MUST NOT call any tools in the next message.",
615
+ )
616
+ )
617
+
618
+ rf = kwargs.get("response_format")
619
+ rf_type = _get_value(rf, "type", rf) if isinstance(rf, dict) else rf
620
+ if rf_type == "json_object":
621
+ segments.extend(
622
+ _internal_system_message(
623
+ "response-format",
624
+ "The system is invoked with `response_format=json_object`.\n"
625
+ "Your response must be raw JSON data without markdown code "
626
+ "blocks (```json) or any additional formatting.",
627
+ )
628
+ )
629
+ elif rf_type == "json_schema":
630
+ schema = _json_compact(kwargs.get("response_schema"))
631
+ segments.extend(
632
+ _internal_system_message(
633
+ "response-format",
634
+ "The system is invoked with `response_format=json_schema`.\n"
635
+ "Your response must be raw JSON data without markdown code "
636
+ "blocks (```json) or any additional formatting.\n"
637
+ "The JSON data must match the following schema:\n"
638
+ f"```json\n{schema}\n```",
639
+ )
640
+ )
641
+
642
+ if add_generation_prompt:
643
+ segments.extend(_open_tag("message", [("role", "assistant")]))
644
+ segments.extend(_open_tag("think" if thinking else "response"))
645
+
646
+ image_state.assert_consumed()
647
+ return segments
generation_config.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "max_length": 1048576,
3
+ "eos_token_id": 163586
4
+ }
kimi_k3_processor.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Kimi-K3 processor: wraps vision processor + tokenizer into a single interface.
2
+
3
+ Chat rendering (including XTML tool-result ordering) is handled by the
4
+ tokenizer's Python encoder; this processor adds multimodal media preprocessing.
5
+ """
6
+
7
+ from transformers.feature_extraction_utils import BatchFeature
8
+ from transformers.processing_utils import ProcessorMixin
9
+ from transformers.utils import logging
10
+
11
+ from .media_utils import ensure_media_type
12
+
13
+ logger = logging.get_logger(__name__)
14
+
15
+ # ── KimiK3Processor ───────────────────────────────────────────────────
16
+
17
+
18
+ class KimiK3Processor(ProcessorMixin):
19
+ r"""
20
+ Constructs a KimiK3 processor which wraps a KimiK3 image processor
21
+ and a tokenizer into a single processor.
22
+
23
+ [`KimiK3Processor`] offers all the functionalities of
24
+ [`KimiK3VisionProcessor`] and [`TikTokenTokenizer`].
25
+
26
+ Args:
27
+ image_processor ([`KimiK3VisionProcessor`], *optional*):
28
+ The image processor is a required input.
29
+ tokenizer ([`TikTokenTokenizer`], *optional*):
30
+ The tokenizer is a required input.
31
+ chat_template (`str`, *optional*): Kept for ProcessorMixin
32
+ compatibility. Kimi K3 chat encoding is implemented in Python by
33
+ the tokenizer.
34
+ """
35
+
36
+ attributes = ["image_processor", "tokenizer"]
37
+ valid_kwargs = ["chat_template"]
38
+ image_processor_class = "AutoImageProcessor"
39
+ tokenizer_class = "AutoTokenizer"
40
+
41
+ def __init__(
42
+ self,
43
+ image_processor=None,
44
+ tokenizer=None,
45
+ chat_template=None,
46
+ **kwargs,
47
+ ):
48
+ super().__init__(image_processor,
49
+ tokenizer,
50
+ chat_template=chat_template)
51
+ self.media_processor = image_processor
52
+ self.image_placeholder = "<|kimi_image_placeholder|>"
53
+
54
+ # ── Media preprocessing ────────────────────────────────────────────
55
+
56
+ def update_raw_text(self, text: str, image_prompts: list[str]) -> str:
57
+ # Replace image placeholders
58
+ image_count = text.count(self.image_placeholder)
59
+ if image_count > 0:
60
+ assert image_count == len(image_prompts), (
61
+ f"image placeholder count {image_count} != "
62
+ f"image_prompts count {len(image_prompts)}")
63
+ text_parts = text.split(self.image_placeholder)
64
+ assert len(text_parts) == len(image_prompts) + 1
65
+ text = "".join([
66
+ text_parts[i] + image_prompts[i]
67
+ for i in range(len(image_prompts))
68
+ ])
69
+ text += text_parts[-1]
70
+
71
+ return text
72
+
73
+ def preprocess_medias(self,
74
+ medias: list[dict]) -> tuple[list[dict], list[str]]:
75
+ """Process media items and generate corresponding prompts.
76
+
77
+ Returns:
78
+ A tuple of (updated_medias, image_prompts).
79
+ """
80
+ updated_medias = []
81
+ image_prompts = []
82
+ for media in medias:
83
+ if media['type'] == 'image':
84
+ updated_medias.append(media)
85
+ img = ensure_media_type(
86
+ media,
87
+ transparent_bg_config=self.media_processor.
88
+ _transparent_bg_config,
89
+ transparent_bg_fill_stage=self.media_processor.
90
+ _transparent_bg_fill_stage,
91
+ )['image']
92
+ w, h = img.size
93
+ image_prompts.append(
94
+ self.media_processor.make_image_prompt(w, h))
95
+ else:
96
+ raise ValueError(f"unsupported media type: {media['type']}")
97
+ return updated_medias, image_prompts
98
+
99
+ # ── Main entry points ──────────────────────────────────────────────
100
+
101
+ def __call__(self,
102
+ messages: list[dict] = None,
103
+ medias: list[dict] = None,
104
+ text: str = None,
105
+ return_tensors: str = "pt",
106
+ **kwargs) -> BatchFeature:
107
+ """
108
+ Process multimodal inputs for Kimi-K3 model.
109
+
110
+ Args:
111
+ messages: List of message dicts with 'role' and 'content' fields.
112
+ If provided, medias and text will be extracted automatically.
113
+ medias: Pre-extracted list of media dicts.
114
+ text: Pre-formatted text string.
115
+ return_tensors: Format of returned tensors. Default: 'pt'.
116
+ **kwargs: Additional arguments passed to apply_chat_template.
117
+
118
+ Returns:
119
+ BatchFeature with fields: input_ids, attention_mask,
120
+ pixel_values, grid_thws.
121
+ """
122
+ if messages is None and (medias is None or text is None):
123
+ raise ValueError(
124
+ "Provide either 'messages' or both 'medias' and 'text'")
125
+
126
+ if medias is not None and text is not None:
127
+ updated_medias, image_prompts = (self.preprocess_medias(medias))
128
+ preprocessed = self.media_processor.preprocess(
129
+ updated_medias, return_tensors=return_tensors)
130
+ text = self.update_raw_text(text, image_prompts)
131
+ text_inputs = self.tokenizer(text, return_tensors=return_tensors)
132
+ return BatchFeature(data={**text_inputs, **preprocessed.data})
133
+
134
+ if medias is None:
135
+ medias = self._extract_medias_from_messages(messages)
136
+ updated_medias, image_prompts = (self.preprocess_medias(medias))
137
+ preprocessed = self.media_processor.preprocess(
138
+ updated_medias, return_tensors=return_tensors)
139
+
140
+ if text is None:
141
+ text_inputs = self.tokenizer.apply_chat_template(
142
+ messages,
143
+ tokenize=True,
144
+ return_tensors=return_tensors,
145
+ return_dict=True,
146
+ image_prompts=image_prompts,
147
+ **kwargs)
148
+ return BatchFeature(data={**text_inputs, **preprocessed.data})
149
+
150
+ text = self.update_raw_text(text, image_prompts)
151
+ text_inputs = self.tokenizer(text, return_tensors=return_tensors)
152
+ return BatchFeature(data={**text_inputs, **preprocessed.data})
153
+
154
+ @staticmethod
155
+ def _extract_medias_from_messages(messages: list[dict]) -> list[dict]:
156
+ """Extract media items from messages in a single pass."""
157
+ medias = []
158
+ for msg in messages:
159
+ if msg['role'] != 'user' or not msg.get('content'):
160
+ continue
161
+
162
+ for content_part in msg['content']:
163
+ if not isinstance(content_part, dict):
164
+ continue
165
+
166
+ content_type = content_part.get('type')
167
+ if content_type in ['image_url', 'image']:
168
+ image_data = content_part.get(content_type)
169
+ assert image_data is not None, f"image data is missing for content part: {content_part}"
170
+ medias.append({
171
+ 'type': 'image',
172
+ 'image': image_data,
173
+ })
174
+ return medias
175
+
176
+ def apply_chat_template(self, messages, **kwargs):
177
+ return self.tokenizer.apply_chat_template(messages, **kwargs)
178
+
179
+ def batch_decode(self, *args, **kwargs):
180
+ return self.tokenizer.batch_decode(*args, **kwargs)
181
+
182
+ def decode(self, *args, **kwargs):
183
+ return self.tokenizer.decode(*args, **kwargs)
184
+
185
+ @property
186
+ def model_input_names(self):
187
+ return ['input_ids', 'attention_mask', 'pixel_values', 'grid_thws']
kimi_k3_vision_processing.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Image processor class for Kimi-K3.
2
+ """
3
+
4
+ import json
5
+ from typing import Any, Dict, Optional, Union
6
+
7
+ import numpy as np
8
+ import torch
9
+ from PIL import Image
10
+ from transformers.image_processing_utils import (BaseImageProcessor,
11
+ BatchFeature)
12
+ from transformers.utils import TensorType
13
+
14
+ from .media_utils import (MediaInput, TransparentBgConfig, _to_tensor,
15
+ ensure_media_type, image_to_np, navit_patchify,
16
+ navit_resize_image, normalize)
17
+
18
+
19
+ class KimiK3VisionProcessor(BaseImageProcessor):
20
+ model_type = "kimi_k3"
21
+
22
+ def __init__(
23
+ self,
24
+ media_proc_cfg: dict,
25
+ **kwargs,
26
+ ):
27
+ super().__init__(**kwargs)
28
+ self.media_proc_cfg = media_proc_cfg
29
+
30
+ @property
31
+ def _transparent_bg_config(self) -> Optional[TransparentBgConfig]:
32
+ cfg = self.media_proc_cfg.get("transparent_bg_config")
33
+ if cfg is None:
34
+ return None
35
+ if isinstance(cfg, TransparentBgConfig):
36
+ return cfg
37
+ return TransparentBgConfig(**cfg)
38
+
39
+ @property
40
+ def _transparent_bg_fill_stage(self) -> str:
41
+ return self.media_proc_cfg.get("transparent_bg_fill_stage",
42
+ "before_resize")
43
+
44
+ def media_tokens_calculator(self, media: MediaInput):
45
+ media = ensure_media_type(
46
+ media,
47
+ transparent_bg_config=self._transparent_bg_config,
48
+ transparent_bg_fill_stage=self._transparent_bg_fill_stage,
49
+ )
50
+ ret = self.get_resize_config(media)
51
+ return ret['num_tokens']
52
+
53
+ @classmethod
54
+ def make_image_prompt(cls, width: int, height: int) -> str:
55
+ """Build the K3 image placeholder with resolution info."""
56
+ return (f"<|media_begin|>image {width}x{height}"
57
+ f"<|media_content|><|media_pad|><|media_end|>")
58
+
59
+ def get_resize_config(self, media_input: MediaInput) -> dict:
60
+ if media_input['type'] == 'image':
61
+ w, h = media_input['image'].size
62
+ ret = navit_resize_image(
63
+ w, h, self.media_proc_cfg['patch_size'],
64
+ self.media_proc_cfg['merge_kernel_size'],
65
+ self.media_proc_cfg['in_patch_limit'],
66
+ self.media_proc_cfg['patch_limit_on_one_side'],
67
+ self.media_proc_cfg['fixed_output_tokens'])
68
+ return ret
69
+ else:
70
+ raise ValueError("Unsupported type: {}".format(
71
+ media_input['type']))
72
+
73
+ def resize_image(self, image: Image.Image, new_width: int, new_height: int,
74
+ pad_width: int, pad_height: int) -> np.ndarray:
75
+ image_np = image_to_np(
76
+ image,
77
+ (new_width, new_height),
78
+ "resize",
79
+ transparent_bg_config=self._transparent_bg_config,
80
+ transparent_bg_fill_stage=self._transparent_bg_fill_stage,
81
+ )
82
+ image_np = np.pad(
83
+ image_np,
84
+ ((0, pad_height), (0, pad_width), (0, 0)),
85
+ mode="constant",
86
+ constant_values=0,
87
+ )
88
+ return image_np
89
+
90
+ def preprocess(
91
+ self,
92
+ medias: list[MediaInput],
93
+ return_tensors: Optional[Union[str, TensorType]] = None,
94
+ ) -> BatchFeature:
95
+ """
96
+ Preprocess a atom vision input (images) into model-ready tensors.
97
+
98
+ Args:
99
+ medias: List of MediaInput.
100
+ return_tensors: Desired output format ('pt', 'np', 'tf', or None).
101
+
102
+ Returns:
103
+ BatchFeature containing 'pixel_values' and 'grid_thws' tensors.
104
+ """
105
+ if not isinstance(medias, list):
106
+ medias = [medias]
107
+ if medias:
108
+ pixel_values = []
109
+ for item in medias:
110
+ item = ensure_media_type(
111
+ item,
112
+ transparent_bg_config=self._transparent_bg_config,
113
+ transparent_bg_fill_stage=self._transparent_bg_fill_stage,
114
+ )
115
+ resize_config = self.get_resize_config(item)
116
+ new_width, new_height, pad_width, pad_height = resize_config[
117
+ 'new_width'], resize_config['new_height'], resize_config[
118
+ 'pad_width'], resize_config['pad_height']
119
+ if item['type'] == 'image':
120
+ image = item['image']
121
+ image_np = self.resize_image(image, new_width, new_height,
122
+ pad_width, pad_height)
123
+ pixel_values.append(np.expand_dims(image_np, axis=0))
124
+ else:
125
+ raise ValueError("Unsupported type: {}".format(
126
+ item['type']))
127
+ normalized_pixel_values = []
128
+ image_std_inv = 1.0 / np.array(self.media_proc_cfg['image_std'])
129
+ image_mean = np.array(self.media_proc_cfg['image_mean'])
130
+ for pixels in pixel_values:
131
+ pixels = normalize(pixels, image_mean, image_std_inv)
132
+ pixels_and_thw = navit_patchify(
133
+ pixels,
134
+ self.media_proc_cfg['patch_size'],
135
+ )
136
+ normalized_pixel_values.append(pixels_and_thw)
137
+
138
+ pixel_values = torch.cat([
139
+ _to_tensor(pixel_value['pixel_values'])
140
+ for pixel_value in normalized_pixel_values
141
+ ])
142
+ grid_thws = torch.cat([
143
+ _to_tensor(pixel_value['grid_thw'],
144
+ dtype=torch.int64).unsqueeze(0)
145
+ for pixel_value in normalized_pixel_values
146
+ ])
147
+
148
+ data = {
149
+ 'pixel_values': pixel_values,
150
+ 'grid_thws': grid_thws,
151
+ }
152
+
153
+ else:
154
+ data = {}
155
+
156
+ return BatchFeature(data=data, tensor_type=return_tensors)
157
+
158
+ def __repr__(self):
159
+ return f"KimiK3VisionProcessor(media_proc_cfg={self.media_proc_cfg})"
160
+
161
+ def to_dict(self) -> Dict[str, Any]:
162
+ output = super().to_dict()
163
+ output["media_proc_cfg"] = self.media_proc_cfg
164
+ if "media_processor" in output:
165
+ del output["media_processor"]
166
+ return output
167
+
168
+ @classmethod
169
+ def from_dict(cls, config_dict: Dict[str, Any], **kwargs):
170
+ config = config_dict.copy()
171
+ media_proc_cfg = config.pop("media_proc_cfg", {})
172
+ return cls(media_proc_cfg=media_proc_cfg, **config, **kwargs)
173
+
174
+ def to_json_string(self):
175
+ dictionary = self.to_dict()
176
+ for key, value in dictionary.items():
177
+ if hasattr(value, 'tolist'):
178
+ dictionary[key] = value.tolist()
179
+ return json.dumps(dictionary, indent=2, sort_keys=True) + "\n"
media_utils.py ADDED
@@ -0,0 +1,376 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import functools
3
+ import io
4
+ import math
5
+ from dataclasses import dataclass
6
+ from typing import Literal, TypedDict
7
+
8
+ import numpy as np
9
+ from PIL import Image
10
+
11
+
12
+ class ImageInput(TypedDict):
13
+ type: Literal['image']
14
+ image: Image.Image
15
+
16
+
17
+ MediaInput = ImageInput
18
+
19
+
20
+ @dataclass
21
+ class TransparentBgConfig:
22
+ """The config of the transparent background."""
23
+
24
+ pattern: Literal["white", "black", "gray", "chessboard"] = "black"
25
+ """The pattern of the transparent background."""
26
+
27
+ chessboard_square_size: int = 16
28
+ """The size of the squares in the chessboard background."""
29
+
30
+ chessboard_square_on_top_left: bool = True
31
+ """Whether to start the chessboard with a white square on the top left."""
32
+
33
+ chessboard_white_value: int = 255
34
+ """The value of the white pixels in the background."""
35
+
36
+ chessboard_gray_value: int = 200
37
+ """The value of the gray pixels in the background."""
38
+
39
+
40
+ @functools.lru_cache(maxsize=256)
41
+ def _create_chessboard_background(
42
+ height: int,
43
+ width: int,
44
+ square_size: int,
45
+ square_on_top_left: bool,
46
+ white_value: int,
47
+ gray_value: int,
48
+ ) -> np.ndarray:
49
+ """Create a chessboard background."""
50
+ bg = np.ones((height, width, 3), dtype=np.uint8) * white_value
51
+ for y in range(0, height, square_size):
52
+ for x in range(0, width, square_size):
53
+ if (y // square_size + x // square_size) % 2 == (
54
+ 1 if square_on_top_left else 0):
55
+ bg[y:y + square_size, x:x + square_size] = gray_value
56
+ return bg
57
+
58
+
59
+ def fill_transparent_bg_with(
60
+ image: Image.Image,
61
+ transparent_bg_config: TransparentBgConfig | None = None,
62
+ ) -> Image.Image:
63
+ """Composite a (possibly) transparent image onto a configured background.
64
+
65
+ When ``transparent_bg_config`` is ``None``, the image is simply converted
66
+ to RGB (preserving the historical behavior). Otherwise the alpha channel
67
+ is alpha-composited over a background generated according to the config.
68
+ """
69
+ if transparent_bg_config is None:
70
+ return image.convert("RGB")
71
+
72
+ if image.mode == "RGB":
73
+ return image
74
+
75
+ has_alpha = "A" in image.getbands() or "transparency" in image.info
76
+ if not has_alpha:
77
+ return image.convert("RGB")
78
+
79
+ img = np.array(image.convert("RGBA"))
80
+ height, width = img.shape[:2]
81
+ bg_pattern = transparent_bg_config.pattern
82
+ if bg_pattern == "white":
83
+ bg = np.full((height, width, 3), 255, dtype=np.uint8)
84
+ elif bg_pattern == "black":
85
+ bg = np.zeros((height, width, 3), dtype=np.uint8)
86
+ elif bg_pattern == "gray":
87
+ bg = np.full((height, width, 3), 128, dtype=np.uint8)
88
+ elif bg_pattern == "chessboard":
89
+ bg = _create_chessboard_background(
90
+ height,
91
+ width,
92
+ transparent_bg_config.chessboard_square_size,
93
+ transparent_bg_config.chessboard_square_on_top_left,
94
+ transparent_bg_config.chessboard_white_value,
95
+ transparent_bg_config.chessboard_gray_value,
96
+ )
97
+ else:
98
+ raise ValueError(f"Invalid background pattern: {bg_pattern}")
99
+
100
+ alpha = img[:, :, 3]
101
+ img_rgb = img[:, :, :3]
102
+ alpha_normalized = alpha.astype(np.float32) / 255.0
103
+ alpha_3d = np.stack([alpha_normalized] * 3, axis=2)
104
+ result = alpha_3d * img_rgb + (1 - alpha_3d) * bg
105
+ result = result.astype(np.uint8)
106
+ return Image.fromarray(result)
107
+
108
+
109
+ def navit_resize_image(
110
+ width: int,
111
+ height: int,
112
+ patch_size: int,
113
+ merge_kernel_size: int,
114
+ in_patch_limit: int,
115
+ patch_limit_on_one_side: int,
116
+ fixed_output_tokens: int | None,
117
+ ):
118
+ # Apply the patch limits.
119
+ s1 = math.sqrt(
120
+ in_patch_limit /
121
+ (max(1.0, width // patch_size) * max(1.0, height // patch_size)))
122
+ s2 = patch_limit_on_one_side * patch_size / width
123
+ s3 = patch_limit_on_one_side * patch_size / height
124
+ scale = min(1.0, s1, s2, s3)
125
+ new_w, new_h = max(1, int(width * scale)), max(1, int(height * scale))
126
+ new_w = min(new_w, patch_limit_on_one_side * patch_size)
127
+ new_h = min(new_h, patch_limit_on_one_side * patch_size)
128
+
129
+ # Calculate the padding to make the height and width divisible by the merge kernel size and patch size.
130
+ factor = merge_kernel_size * patch_size
131
+
132
+ pad_height = (factor - new_h % factor) % factor
133
+ pad_width = (factor - new_w % factor) % factor
134
+
135
+ if fixed_output_tokens is not None:
136
+ num_tokens = fixed_output_tokens
137
+ else:
138
+ # Calculate new dimensions after padding and patching
139
+ token_height = (new_h + pad_height) // factor
140
+ token_width = (new_w + pad_width) // factor
141
+
142
+ assert token_height * merge_kernel_size <= patch_limit_on_one_side, (
143
+ f"token_height {token_height} * merge_kernel_size {merge_kernel_size} > patch_limit_on_one_side {patch_limit_on_one_side}"
144
+ )
145
+ assert token_width * merge_kernel_size <= patch_limit_on_one_side, (
146
+ f"token_width {token_width} * merge_kernel_size {merge_kernel_size} > patch_limit_on_one_side {patch_limit_on_one_side}"
147
+ )
148
+
149
+ num_tokens = token_height * token_width
150
+ return {
151
+ "num_tokens": num_tokens,
152
+ "new_width": new_w,
153
+ "new_height": new_h,
154
+ "pad_width": pad_width,
155
+ "pad_height": pad_height,
156
+ "sampled_nframes": 1,
157
+ }
158
+
159
+
160
+ def _to_pil(
161
+ data: str | bytes | Image.Image,
162
+ transparent_bg_config: TransparentBgConfig | None = None,
163
+ to_rgb: bool = True,
164
+ ) -> Image.Image:
165
+ """Load an image and (optionally) composite its transparent background.
166
+
167
+ Args:
168
+ data: A PIL Image, a base64 ``data:`` URL, a file path, or raw bytes.
169
+ transparent_bg_config: The config used to fill the transparent
170
+ background. ``None`` keeps the historical behavior of converting
171
+ to RGB without compositing.
172
+ to_rgb: If ``False`` the image is returned as-is (the
173
+ ``transparent_bg_config`` is ignored). The caller is then
174
+ expected to call :func:`fill_transparent_bg_with` later — e.g.
175
+ after a resize.
176
+ """
177
+ if isinstance(data, Image.Image):
178
+ image = data
179
+ elif isinstance(data, str):
180
+ if data.startswith("data:"):
181
+ raw_base64 = data.split(",")[1]
182
+ image = Image.open(io.BytesIO(base64.b64decode(raw_base64)))
183
+ else:
184
+ image = Image.open(data)
185
+ elif isinstance(data, bytes):
186
+ image = Image.open(io.BytesIO(data))
187
+ else:
188
+ raise ValueError(f"Unsupported data type: {type(data)}")
189
+
190
+ if not to_rgb:
191
+ return image
192
+
193
+ return fill_transparent_bg_with(image, transparent_bg_config)
194
+
195
+
196
+ def ensure_media_type(
197
+ media: MediaInput,
198
+ transparent_bg_config: TransparentBgConfig | None = None,
199
+ transparent_bg_fill_stage: Literal["before_resize",
200
+ "after_resize"] = "before_resize",
201
+ ) -> MediaInput:
202
+ if media['type'] == 'image':
203
+ media['image'] = _to_pil(
204
+ media['image'],
205
+ transparent_bg_config=transparent_bg_config,
206
+ to_rgb=transparent_bg_fill_stage == "before_resize",
207
+ )
208
+ return media
209
+ else:
210
+ raise ValueError(f"Unsupported media type: {media['type']}")
211
+
212
+
213
+ def image_to_np(
214
+ image: Image.Image,
215
+ resize_to: tuple[int, int] | None = None,
216
+ mode: str = "resize",
217
+ raise_error_for_ill_resize: bool = True,
218
+ transparent_bg_config: TransparentBgConfig | None = None,
219
+ transparent_bg_fill_stage: Literal["before_resize",
220
+ "after_resize"] = "before_resize",
221
+ ) -> np.ndarray:
222
+ """Convert an image to a numpy array.
223
+
224
+ Args:
225
+ content: The image to convert.
226
+ resize_to: The size to resize the image to.
227
+ mode: The mode to resize the image to.
228
+ raise_error_for_ill_resize: Whether to raise an error for ill-sized resize.
229
+ transparent_bg_config: The config of the transparent background. Only
230
+ used when ``transparent_bg_fill_stage == "after_resize"`` (the
231
+ caller is responsible for filling before resize otherwise).
232
+ transparent_bg_fill_stage: When to composite the transparent
233
+ background — before or after the resize step.
234
+
235
+ Returns:
236
+ A numpy array.
237
+ """
238
+ assert isinstance(image, Image.Image), "image must be a PIL Image"
239
+ if resize_to is not None:
240
+ if mode == "resize":
241
+ image = image.resize(resize_to, resample=Image.Resampling.BICUBIC)
242
+ if transparent_bg_fill_stage == "after_resize":
243
+ image = fill_transparent_bg_with(image, transparent_bg_config)
244
+
245
+ elif mode == "rescale_and_pad_to_center":
246
+ scale = min(resize_to[0] / image.width,
247
+ resize_to[1] / image.height, 1.0)
248
+ new_width = round(image.width * scale)
249
+ new_height = round(image.height * scale)
250
+ if new_width == 0 or new_height == 0:
251
+ if raise_error_for_ill_resize:
252
+ raise ValueError(
253
+ f"Invalid resize to: {resize_to}, from image size: {image.size}"
254
+ )
255
+ else:
256
+ return np.zeros((resize_to[1], resize_to[0], 3),
257
+ dtype=np.uint8)
258
+
259
+ image = image.resize((new_width, new_height),
260
+ resample=Image.Resampling.BICUBIC)
261
+ if transparent_bg_fill_stage == "after_resize":
262
+ image = fill_transparent_bg_with(image, transparent_bg_config)
263
+ padding_left = (resize_to[0] - new_width) // 2
264
+ padding_right = resize_to[0] - new_width - padding_left
265
+ padding_top = (resize_to[1] - new_height) // 2
266
+ padding_bottom = resize_to[1] - new_height - padding_top
267
+ image = np.asarray(image)
268
+ image = np.pad(
269
+ image,
270
+ ((padding_top, padding_bottom), (padding_left, padding_right),
271
+ (0, 0)),
272
+ mode="constant",
273
+ constant_values=0,
274
+ )
275
+ assert image.shape == (resize_to[1], resize_to[0], 3)
276
+
277
+ elif mode == "rescale_and_pad_to_rightbottom":
278
+ scale = min(resize_to[0] / image.width,
279
+ resize_to[1] / image.height, 1.0)
280
+ new_width = round(image.width * scale)
281
+ new_height = round(image.height * scale)
282
+ if new_width == 0 or new_height == 0:
283
+ if raise_error_for_ill_resize:
284
+ raise ValueError(
285
+ f"Invalid resize to: {resize_to}, from image size: {image.size}"
286
+ )
287
+ else:
288
+ return np.zeros((resize_to[1], resize_to[0], 3),
289
+ dtype=np.uint8)
290
+
291
+ image = image.resize((new_width, new_height),
292
+ resample=Image.Resampling.BICUBIC)
293
+ if transparent_bg_fill_stage == "after_resize":
294
+ image = fill_transparent_bg_with(image, transparent_bg_config)
295
+ padding_right = resize_to[0] - new_width
296
+ padding_bottom = resize_to[1] - new_height
297
+ image = np.asarray(image)
298
+ image = np.pad(
299
+ image,
300
+ ((0, padding_bottom), (0, padding_right), (0, 0)),
301
+ mode="constant",
302
+ constant_values=0,
303
+ )
304
+ assert image.shape == (resize_to[1], resize_to[0], 3)
305
+
306
+ else:
307
+ raise ValueError(f"Invalid mode: {mode}")
308
+
309
+ if isinstance(image, Image.Image):
310
+ return np.asarray(image)
311
+ else:
312
+ return image
313
+
314
+
315
+ def navit_patchify(pixel_values: np.ndarray,
316
+ patch_size: int) -> dict[str, np.ndarray]:
317
+ """Reshape the pixel values to a navit shape.
318
+
319
+ Args:
320
+ pixel_values: np.ndarray, shape (t, h, w, c)
321
+ patch_size: int
322
+
323
+ Returns:
324
+ dict[str, np.ndarray]
325
+ - patches: np.ndarray, shape (t * h//patch_size * w//patch_size, c, patch_size, patch_size)
326
+ - grid_thw: np.ndarray, (t, h//patch_size, w//patch_size)
327
+ """
328
+ T, H, W, C = pixel_values.shape
329
+ assert C == 3, "pixel_values must have 3 channels"
330
+
331
+ patches = pixel_values.reshape(T, H // patch_size, patch_size,
332
+ W // patch_size, patch_size, C)
333
+ # (T, H//patch_size, W//patch_size, C, patch_size, patch_size)
334
+ patches = patches.transpose(0, 1, 3, 5, 2, 4)
335
+ patches = patches.reshape(-1, C, patch_size, patch_size)
336
+ grid_thw = np.array([T, H // patch_size, W // patch_size])
337
+ return {"pixel_values": patches, "grid_thw": grid_thw}
338
+
339
+
340
+ def normalize(x: np.ndarray,
341
+ mean,
342
+ std_inv,
343
+ pixels_dtype: np.dtype = np.float32) -> np.ndarray:
344
+ """Normalize the image.
345
+
346
+ Args:
347
+ x: The image to normalize. The shape is (..., 3). The dtype is uint8. The range is [0, 255].
348
+ mean: The mean of the image.
349
+ std_inv: The inverse of the std of the image.
350
+ pixels_dtype: The dtype of the image.
351
+ Returns:
352
+ The normalized image. The shape is (..., 3). The dtype is determined by the pixels_dtype.
353
+ """
354
+ x = (x / 255.0).astype(pixels_dtype)
355
+ x -= mean
356
+ x *= std_inv
357
+ return x
358
+
359
+
360
+ def _to_tensor(data, **kwargs):
361
+ import torch
362
+
363
+ if isinstance(data, np.ndarray):
364
+ return torch.from_numpy(data).to(**kwargs)
365
+ elif isinstance(data, torch.Tensor):
366
+ return data.to(**kwargs)
367
+ elif isinstance(data, list):
368
+ return [_to_tensor(item, **kwargs) for item in data]
369
+ elif isinstance(data, tuple):
370
+ return tuple(_to_tensor(item, **kwargs) for item in data)
371
+ elif isinstance(data, dict):
372
+ return {k: _to_tensor(v, **kwargs) for k, v in data.items()}
373
+ elif data is None:
374
+ return None
375
+ else:
376
+ raise ValueError(f"Unsupported data type: {type(data)}")
model.safetensors.index.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:113c510e9fbdcd9183858705c177d56dd282d3c86b3ec327782dc64f89083104
3
+ size 91116256
modeling_kimi_k3.py ADDED
@@ -0,0 +1,1317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+ # Copyright 2025-2026 The Moonshot AI Team and HuggingFace Inc. team. All rights reserved.
3
+ #
4
+ # The code is based on llava (llava/modeling_llava.py), but modified for Kimi-K3.
5
+ #
6
+ # Licensing Information:
7
+ # - Code derived from llava (llava/modeling_llava.py) is licensed under the Apache License, Version 2.0.
8
+ # - Other parts of the code are licensed under the Kimi K3 License (see the LICENSE file in this repository).
9
+ #
10
+ # Apache License, Version 2.0:
11
+ # Licensed under the Apache License, Version 2.0 (the "License");
12
+ # you may not use this file except in compliance with the License.
13
+ # You may obtain a copy of the License at
14
+ #
15
+ # http://www.apache.org/licenses/LICENSE-2.0
16
+ #
17
+ # Unless required by applicable law or agreed to in writing, software
18
+ # distributed under the License is distributed on an "AS IS" BASIS,
19
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20
+ # See the License for the specific language governing permissions and
21
+ # limitations under the License.
22
+
23
+
24
+ # NOTE: Reference implementation for model architecture; see the model card for production deployment.
25
+ import math
26
+ from collections.abc import Sequence
27
+ from copy import deepcopy
28
+ from typing import Optional
29
+
30
+ import numpy as np
31
+ import torch
32
+ import torch.nn as nn
33
+ import torch.nn.functional as F
34
+ from transformers import activations
35
+
36
+ try:
37
+ from transformers.activations import PytorchGELUTanh
38
+ except ImportError:
39
+ from transformers.activations import GELUTanh
40
+ activations.PytorchGELUTanh = GELUTanh
41
+ PytorchGELUTanh = GELUTanh
42
+ from transformers.activations import PytorchGELUTanh
43
+ from transformers.configuration_utils import PretrainedConfig
44
+ from transformers.modeling_utils import PreTrainedModel
45
+ from transformers.models.llava.modeling_llava import \
46
+ LlavaCausalLMOutputWithPast
47
+ from transformers.utils import is_flash_attn_2_available
48
+
49
+ from .configuration_kimi_k3 import KimiK3Config
50
+ from .modeling_kimi_linear import KimiLinearForCausalLM
51
+
52
+ # Flash attention imports
53
+ if is_flash_attn_2_available():
54
+ from flash_attn import flash_attn_varlen_func
55
+ else:
56
+ flash_attn_varlen_func = None
57
+
58
+
59
+ def multihead_attention(
60
+ q: torch.Tensor,
61
+ k: torch.Tensor,
62
+ v: torch.Tensor,
63
+ q_cu_seqlens: torch.Tensor | None = None,
64
+ k_cu_seqlens: torch.Tensor | None = None,
65
+ max_seqlen_q: int | None = None,
66
+ max_seqlen_k: int | None = None,
67
+ deterministic: bool = False,
68
+ ):
69
+ """Multi-head attention using flash attention 2.
70
+
71
+ Args:
72
+ q, k, v: tensor of shape (batch_size, seqlen, num_heads, head_dim),
73
+ or (tot_seqlens, num_heads, head_dim) if packing.
74
+ q_cu_seqlens (torch.Tensor): cumulative sequence lengths of q.
75
+ The first element should be 0 and the last element should be q.shape[0].
76
+ k_cu_seqlens (torch.Tensor): cumulative sequence lengths of k.
77
+ The first element should be 0 and the last element should be k.shape[0].
78
+
79
+ Returns:
80
+ output: shape (batch_size, seqlen, dim) or (tot_seqlens, dim) if packing,
81
+ where dim = num_heads * head_dim
82
+ """
83
+ attn_out = flash_attn_varlen_func(
84
+ q,
85
+ k,
86
+ v,
87
+ q_cu_seqlens,
88
+ k_cu_seqlens,
89
+ max_seqlen_q,
90
+ max_seqlen_k,
91
+ causal=False,
92
+ deterministic=deterministic,
93
+ )
94
+ if isinstance(attn_out, tuple):
95
+ attn_out = attn_out[0]
96
+
97
+ attn_out = attn_out.flatten(start_dim=-2)
98
+
99
+ return attn_out
100
+
101
+
102
+ def eager_attention(
103
+ q: torch.Tensor,
104
+ k: torch.Tensor,
105
+ v: torch.Tensor,
106
+ q_cu_seqlens: Optional[torch.Tensor] = None,
107
+ k_cu_seqlens: Optional[torch.Tensor] = None,
108
+ **kwargs,
109
+ ) -> torch.Tensor:
110
+ seq_length = q.shape[0]
111
+ attention_mask = torch.zeros([1, seq_length, seq_length],
112
+ device=q.device,
113
+ dtype=torch.bool)
114
+ for i in range(1, len(q_cu_seqlens)):
115
+ attention_mask[
116
+ ...,
117
+ q_cu_seqlens[i - 1]:q_cu_seqlens[i],
118
+ q_cu_seqlens[i - 1]:q_cu_seqlens[i],
119
+ ] = True
120
+ q = q.transpose(0, 1)
121
+ k = k.transpose(0, 1)
122
+ v = v.transpose(0, 1)
123
+
124
+ attn_weight = q @ k.transpose(-2, -1) / math.sqrt(q.shape[-1])
125
+ attn_weight = attn_weight.masked_fill(
126
+ ~attention_mask, torch.finfo(attn_weight.dtype).min)
127
+ attn_weight = torch.softmax(attn_weight, dim=-1,
128
+ dtype=torch.float32).to(q.dtype)
129
+
130
+ attn_output = attn_weight @ v
131
+ attn_output = attn_output.transpose(0, 1)
132
+ attn_output = attn_output.reshape(seq_length, -1)
133
+ return attn_output
134
+
135
+
136
+ VL_VISION_ATTENTION_FUNCTIONS = {
137
+ "flash_attention_2": multihead_attention,
138
+ "eager": eager_attention,
139
+ }
140
+
141
+
142
+ def _apply_rope_input_validation(x, freqs_cis):
143
+ assert x.ndim == freqs_cis.ndim + 1, (x.shape, freqs_cis.shape)
144
+ assert x.shape[:-2] == freqs_cis.shape[:-1], (x.shape, freqs_cis.shape)
145
+ assert x.shape[-1] == 2 * freqs_cis.shape[-1], (x.shape, freqs_cis.shape)
146
+ assert freqs_cis.dtype == torch.complex64, freqs_cis.dtype
147
+
148
+
149
+ def get_rope_shape_decorate(func):
150
+ _get_rope_shape_first_call_flag = set()
151
+
152
+ def wrapper(org, interpolation_mode, shape):
153
+ key = (org.requires_grad, torch.is_grad_enabled(), interpolation_mode)
154
+ if key not in _get_rope_shape_first_call_flag:
155
+ _get_rope_shape_first_call_flag.add(key)
156
+ _ = func(org, interpolation_mode, shape=(64, 64))
157
+ return func(org, interpolation_mode, shape)
158
+
159
+ return wrapper
160
+
161
+
162
+ @get_rope_shape_decorate
163
+ @torch.compile(dynamic=True)
164
+ def get_rope_shape(org, interpolation_mode, shape):
165
+ return (F.interpolate(
166
+ org.permute((2, 0, 1)).unsqueeze(0),
167
+ size=shape,
168
+ mode=interpolation_mode,
169
+ ).squeeze(0).permute((1, 2, 0)).flatten(end_dim=1))
170
+
171
+
172
+ def apply_rope(xq: torch.Tensor, xk: torch.Tensor,
173
+ freqs_cis: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
174
+ """
175
+ Args: (The leading dimensions of all inputs should be the same)
176
+ xq: query, tensor of shape (..., num_heads, head_dim)
177
+ xk: key, tensor of shape (..., num_heads, head_dim)
178
+ freqs_cis: tensor of shape (..., head_dim/2), dtype=torch.complex64. It contains the precomputed cis(freqs) for each position in the 2D grid.
179
+ Returns:
180
+ xq_out, xk_out: tensors of shape (..., num_heads, head_dim)
181
+ """
182
+ _apply_rope_input_validation(xq, freqs_cis)
183
+ _apply_rope_input_validation(xk, freqs_cis)
184
+
185
+ freqs_cis = freqs_cis.unsqueeze(-2) # ..., 1, head_dim/2
186
+ # ..., num_heads, head_dim/2
187
+ xq_ = torch.view_as_complex(xq.float().view(*xq.shape[:-1], -1, 2))
188
+ xk_ = torch.view_as_complex(xk.float().view(*xq.shape[:-1], -1, 2))
189
+ xq_out = torch.view_as_real(xq_ * freqs_cis).flatten(
190
+ -2) # ..., num_heads, head_dim
191
+ xk_out = torch.view_as_real(xk_ * freqs_cis).flatten(
192
+ -2) # ..., num_heads, head_dim
193
+ return xq_out.type_as(xq), xk_out.type_as(xk)
194
+
195
+
196
+ def get_1d_sincos_pos_embed_from_grid(embed_dim, pos):
197
+ """
198
+ From:
199
+ https://github.com/OpenGVLab/InternVideo/blob/421f6d2361fc8f61a3394244571f2601a4e99e29/InternVideo2/multi_modality/models/backbones/internvideo2/pos_embed.py#L86
200
+ embed_dim: output dimension for each position
201
+ pos: a list of positions to be encoded: size (M,)
202
+ out: (M, D)
203
+ """
204
+ assert embed_dim % 2 == 0
205
+ omega = np.arange(embed_dim // 2, dtype=np.float32)
206
+ omega /= embed_dim / 2.0
207
+ omega = 1.0 / 10000**omega # (D/2,)
208
+
209
+ pos = pos.reshape(-1) # (M,)
210
+ out = np.einsum('m,d->md', pos, omega) # (M, D/2), outer product
211
+
212
+ emb_sin = np.sin(out) # (M, D/2)
213
+ emb_cos = np.cos(out) # (M, D/2)
214
+
215
+ emb = np.concatenate([emb_sin, emb_cos], axis=1) # (M, D)
216
+ return emb
217
+
218
+
219
+ def get_1d_sincos_pos_embed(embed_dim, t_size, cls_token=False):
220
+ """
221
+ t_size: int of the temporal size
222
+ return:
223
+ pos_embed: [t_size, embed_dim] or [1+t_size, embed_dim] (w/ or w/o cls_token)
224
+ """
225
+ grid_t = np.arange(t_size, dtype=np.float32)
226
+ pos_embed = get_1d_sincos_pos_embed_from_grid(embed_dim, grid_t)
227
+ if cls_token:
228
+ pos_embed = np.concatenate([np.zeros([1, embed_dim]), pos_embed],
229
+ axis=0)
230
+ return pos_embed
231
+
232
+
233
+ class Learnable2DInterpPosEmbDivided_fixed(nn.Module):
234
+
235
+ def __init__(self,
236
+ height: int,
237
+ width: int,
238
+ num_frames: int,
239
+ dim: int,
240
+ interpolation_mode: str = 'bicubic') -> None:
241
+ super().__init__()
242
+ self.height = height
243
+ self.width = width
244
+ self.num_frames = num_frames
245
+ self.dim = dim
246
+ self.interpolation_mode = interpolation_mode
247
+ self.weight = nn.Parameter(torch.empty(height, width, dim))
248
+ self.register_buffer('time_weight',
249
+ torch.from_numpy(
250
+ get_1d_sincos_pos_embed(
251
+ self.dim,
252
+ self.num_frames)).float().unsqueeze(1),
253
+ persistent=False)
254
+
255
+ self.reset_parameters()
256
+
257
+ def reset_parameters(self):
258
+ nn.init.normal_(self.weight)
259
+
260
+ def forward(self, x: torch.Tensor,
261
+ grid_thws: torch.Tensor) -> torch.Tensor:
262
+ pos_embs = []
263
+ for t, h, w in grid_thws.tolist():
264
+ assert t <= self.num_frames, f't:{t} > self.num_frames:{self.num_frames}'
265
+ if (h, w) == self.weight.shape[:-1]:
266
+ pos_emb_2d = self.weight.flatten(end_dim=1)
267
+ else:
268
+ pos_emb_2d = get_rope_shape(
269
+ self.weight,
270
+ interpolation_mode=self.interpolation_mode,
271
+ shape=(h, w),
272
+ )
273
+
274
+ if t == 1:
275
+ pos_emb_3d = pos_emb_2d
276
+ else:
277
+ pos_emb_3d = pos_emb_2d.unsqueeze(0).repeat(
278
+ t, 1, 1) + self.time_weight[0:t]
279
+
280
+ pos_embs.append(pos_emb_3d.reshape(-1, pos_emb_3d.shape[-1]))
281
+
282
+ out = x + torch.cat(pos_embs)
283
+ return out
284
+
285
+
286
+ class MoonVision3dPatchEmbed(nn.Module):
287
+
288
+ def __init__(self,
289
+ out_dim: int,
290
+ in_dim: int = 3,
291
+ patch_size: int | tuple[int, int] = (14, 14),
292
+ pos_emb_height: int = 14,
293
+ pos_emb_width: int = 14,
294
+ pos_emb_time: int = 4,
295
+ pos_emb_type: str = 'divided_fixed',
296
+ patch_embed_proj_bias: bool = True,
297
+ pos_emb_interpolation_mode: str = 'bicubic'):
298
+ super().__init__()
299
+ assert isinstance(
300
+ patch_size,
301
+ int | Sequence), f'Invalid patch_size type: {type(patch_size)}'
302
+ if isinstance(patch_size, int):
303
+ patch_size = (patch_size, patch_size)
304
+ assert (len(patch_size) == 2
305
+ ), f'Expected patch_size to be a tuple of 2, got {patch_size}'
306
+ self.patch_size = patch_size
307
+
308
+ self.proj = nn.Conv2d(in_dim,
309
+ out_dim,
310
+ kernel_size=patch_size,
311
+ stride=patch_size,
312
+ bias=patch_embed_proj_bias)
313
+
314
+ if pos_emb_type == 'divided_fixed':
315
+ self.pos_emb = Learnable2DInterpPosEmbDivided_fixed(
316
+ height=pos_emb_height,
317
+ width=pos_emb_width,
318
+ num_frames=pos_emb_time,
319
+ dim=out_dim,
320
+ interpolation_mode=pos_emb_interpolation_mode)
321
+ else:
322
+ raise NotImplementedError(
323
+ f'Not support pos_emb_type: {pos_emb_type}')
324
+
325
+ def forward(self, x: torch.Tensor,
326
+ grid_thws: torch.Tensor) -> torch.Tensor:
327
+ """
328
+ Args:
329
+ x (L, Channels): input tensor
330
+ grid_hws (N, 3): temporal, height and width
331
+
332
+ Returns:
333
+ (L, Cout) tensor
334
+ """
335
+ x = self.proj(x).view(x.size(0), -1)
336
+ # apply positional embedding
337
+ x = self.pos_emb(x, grid_thws)
338
+ return x
339
+
340
+
341
+ class Rope2DPosEmbRepeated(nn.Module):
342
+ """2D rotary position embedding with multi-resolution support.
343
+
344
+ This class is intended to be used in the following way:
345
+ 1. Before training, create an instance of Rope2DPosEmb. This instance will hold the precomputed cis.
346
+ 2. Before each forward pass, call `get_freqs_cis_by_*` to get the `freqs_cis` tensor for this iteration.
347
+ 3. During the forward pass, pass the `freqs_cis` tensor to each attention layer, and call `apply` just before each attention operation.
348
+ The rope is shared across all attention layers and all heads.
349
+
350
+ Refs:
351
+ - RoFormer: https://arxiv.org/abs/2104.09864
352
+ - VisionLLaMA: https://arxiv.org/abs/2403.00522
353
+ - https://github.com/Meituan-AutoML/VisionLLaMA/blob/main/dit/models.py
354
+
355
+ Args:
356
+ dim (int): usually the multi-head attention dimension, should be divisible by 4 (TODO: relax this constraint if needed)
357
+ max_height (int): the maximum height of the 2D grid
358
+ max_width (int): the maximum width of the 2D grid
359
+ theta_base (float): the base of the theta
360
+ device (str): the device to store the precomputed cis
361
+ """
362
+
363
+ def __init__(self,
364
+ dim: int,
365
+ max_height: int,
366
+ max_width: int,
367
+ theta_base=10000):
368
+ super().__init__()
369
+ self.dim = dim
370
+ assert self.dim % 4 == 0, 'dim must be divisible by 4'
371
+ self.max_height = max_height
372
+ self.max_width = max_width
373
+ self.theta_base = theta_base
374
+
375
+ def extra_repr(self):
376
+ return f'dim={self.dim}, max_height={self.max_height}, max_width={self.max_width}, theta_base={self.theta_base}'
377
+
378
+ def _precompute_freqs_cis(self, device: torch.device) -> torch.Tensor:
379
+ """Calculate the cis(freqs) for each position in the 2D grid.
380
+
381
+ Return: complex tensor of shape (max_height, max_width, dim//2) and value:
382
+ height axis: ret[h, w, 2*i] = cis(h * theta_base**(-4*i/dim))
383
+ weight axis: ret[h, w, 2*i+1] = cis(w * theta_base**(-4*i/dim)) with (i in [0, dim//4))
384
+ note: `cis` is a mathematical notation defined by cis x = cos x + i sin x,
385
+ """
386
+ N = self.max_height * self.max_width
387
+ flat_pos = torch.arange(0, N).float().to(device)
388
+ x_pos = flat_pos % self.max_width
389
+ y_pos = flat_pos // self.max_width
390
+ dim_range = (torch.arange(0, self.dim,
391
+ 4)[:(self.dim // 4)].float().to(device)
392
+ ) # C/4
393
+ freqs = 1.0 / (self.theta_base**(dim_range / self.dim))
394
+ x_freqs = torch.outer(x_pos, freqs).float() # N, C/4
395
+ y_freqs = torch.outer(y_pos, freqs).float() # N, C/4
396
+ x_cis = torch.polar(torch.ones_like(x_freqs), x_freqs) # N, C/4
397
+ y_cis = torch.polar(torch.ones_like(y_freqs), y_freqs) # N, C/4
398
+ # N, C/4, 2
399
+ freqs_cis = torch.cat(
400
+ [x_cis.unsqueeze(dim=-1),
401
+ y_cis.unsqueeze(dim=-1)], dim=-1)
402
+ # max_height, max_width, C/2
403
+ freqs_cis = freqs_cis.reshape(self.max_height, self.max_width, -1)
404
+ return freqs_cis
405
+
406
+ def get_freqs_cis(self, grid_thws: torch.Tensor,
407
+ device: torch.device) -> torch.Tensor:
408
+ """
409
+ Args:
410
+ grid_thws (torch.Tensor): grid time, height and width
411
+
412
+ Returns:
413
+ freqs_cis: tensor of shape (sum(t * height * width), dim//2)
414
+ """
415
+ if not hasattr(self, 'freqs_cis'):
416
+ self.register_buffer('freqs_cis',
417
+ self._precompute_freqs_cis(device),
418
+ persistent=False)
419
+
420
+ shapes = grid_thws.tolist()
421
+ assert all(1 <= h <= self.max_height and 1 <= w <= self.max_width
422
+ for t, h, w in shapes), (
423
+ shapes,
424
+ self.max_height,
425
+ self.max_width,
426
+ )
427
+ freqs_cis = torch.cat(
428
+ [
429
+ self.freqs_cis[:h, :w].reshape(-1, self.dim // 2).repeat(t, 1)
430
+ for t, h, w in shapes
431
+ ],
432
+ dim=0,
433
+ )
434
+ return freqs_cis
435
+
436
+
437
+ class MLP2(nn.Module):
438
+ """
439
+ Args:
440
+ dims: [in_dim, hidden_dim, out_dim]
441
+ bias: whether to use bias in linear layer.
442
+ """
443
+
444
+ def __init__(self, dims: list[int], activation, bias=True):
445
+ super().__init__()
446
+ assert len(dims) == 3
447
+ self.fc0 = nn.Linear(dims[0], dims[1], bias=bias)
448
+ self.fc1 = nn.Linear(dims[1], dims[2], bias=bias)
449
+ self.activation = activation
450
+ for m in [self.fc0, self.fc1]:
451
+ nn.init.trunc_normal_(m.weight, std=math.sqrt(2 / m.in_features))
452
+ if m.bias is not None:
453
+ nn.init.zeros_(m.bias)
454
+
455
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
456
+ x = self.fc0(x)
457
+ x = self.activation(x)
458
+ return self.fc1(x)
459
+
460
+
461
+ class MoonViTEncoderLayer(nn.Module):
462
+
463
+ def __init__(
464
+ self,
465
+ num_heads: int,
466
+ hidden_dim: int,
467
+ mlp_dim: int,
468
+ qkv_hidden_size: int | None = None,
469
+ norm_type: str = 'layernorm',
470
+ mlp_type: str = 'mlp2',
471
+ *,
472
+ attn_implementation: str = 'flash_attention_2',
473
+ activation=F.gelu,
474
+ attn_bias: bool = False,
475
+ linear_bias: bool = True,
476
+ use_deterministic_attn: bool = False,
477
+ ):
478
+ super().__init__()
479
+ self.num_heads = num_heads
480
+ self.hidden_dim = hidden_dim
481
+ self.qkv_hidden_size = hidden_dim if qkv_hidden_size is None else qkv_hidden_size
482
+ self.hidden_size_per_attention_head = self.qkv_hidden_size // self.num_heads
483
+ self.attn_implementation = attn_implementation
484
+ self.use_deterministic_attn = use_deterministic_attn
485
+
486
+ if norm_type == "layernorm":
487
+ self.norm0 = nn.LayerNorm(hidden_dim)
488
+ self.norm1 = nn.LayerNorm(hidden_dim)
489
+ elif norm_type == "rmsnorm":
490
+ self.norm0 = nn.RMSNorm(hidden_dim)
491
+ self.norm1 = nn.RMSNorm(hidden_dim)
492
+ else:
493
+ raise NotImplementedError(f"Not support norm_type: {norm_type}")
494
+
495
+ if mlp_type == "mlp2":
496
+ self.mlp = MLP2([hidden_dim, mlp_dim, hidden_dim],
497
+ activation,
498
+ bias=linear_bias)
499
+ else:
500
+ raise NotImplementedError(f"Not support mlp_type: {mlp_type}")
501
+
502
+ self.wqkv = nn.Linear(hidden_dim,
503
+ self.qkv_hidden_size * 3,
504
+ bias=attn_bias)
505
+ self.wo = nn.Linear(self.qkv_hidden_size, hidden_dim, bias=attn_bias)
506
+
507
+ def attention_qkvpacked(
508
+ self,
509
+ x: torch.Tensor,
510
+ cu_seqlens: torch.Tensor,
511
+ max_seqlen: torch.Tensor,
512
+ rope_freqs_cis: torch.Tensor | None = None,
513
+ ):
514
+ """
515
+ Args:
516
+ x (torch.Tensor): (batch_size, seqlen, hidden_dim)
517
+ cu_seqlens (torch.Tensor):
518
+ """
519
+ xqkv = self.wqkv(x)
520
+
521
+ qkv_shape = xqkv.size()[:-1] + (
522
+ 3,
523
+ self.num_heads,
524
+ self.hidden_size_per_attention_head,
525
+ )
526
+ # xqkv: (batch_size, seqlen, 3, nheads, headdim)
527
+ xqkv = xqkv.view(*qkv_shape)
528
+ xq, xk, xv = torch.unbind(xqkv, dim=-3)
529
+
530
+ xq, xk = apply_rope(xq, xk, rope_freqs_cis)
531
+
532
+ attn_func = VL_VISION_ATTENTION_FUNCTIONS[self.attn_implementation]
533
+ attn_out = attn_func(xq,
534
+ xk,
535
+ xv,
536
+ q_cu_seqlens=cu_seqlens,
537
+ k_cu_seqlens=cu_seqlens,
538
+ max_seqlen_k=max_seqlen,
539
+ max_seqlen_q=max_seqlen,
540
+ deterministic=self.use_deterministic_attn)
541
+
542
+ attn_out = self.wo(attn_out)
543
+ return attn_out
544
+
545
+ def forward(
546
+ self,
547
+ hidden_states: torch.Tensor,
548
+ cu_seqlens: torch.Tensor,
549
+ max_seqlen: int,
550
+ rope_freqs_cis: torch.Tensor | None = None,
551
+ ):
552
+ residual = hidden_states
553
+ hidden_states = self.norm0(hidden_states)
554
+
555
+ hidden_states = self.attention_qkvpacked(hidden_states, cu_seqlens,
556
+ max_seqlen, rope_freqs_cis)
557
+ hidden_states = residual + hidden_states
558
+
559
+ residual = hidden_states
560
+ hidden_states = self.norm1(hidden_states)
561
+ hidden_states = self.mlp(hidden_states)
562
+ hidden_states = residual + hidden_states
563
+
564
+ return hidden_states
565
+
566
+
567
+ class MoonViT3dEncoder(nn.Module):
568
+
569
+ def __init__(self,
570
+ hidden_dim: int,
571
+ num_layers: int,
572
+ block_cfg: dict,
573
+ use_deterministic_attn: bool = False) -> None:
574
+ super().__init__()
575
+ self.use_deterministic_attn = use_deterministic_attn
576
+
577
+ qkv_hidden_size = block_cfg['hidden_dim'] if block_cfg.get(
578
+ 'qkv_hidden_size') is None else block_cfg['qkv_hidden_size']
579
+ self.rope_2d = Rope2DPosEmbRepeated(
580
+ qkv_hidden_size // block_cfg['num_heads'], 512, 512)
581
+ self.blocks = nn.ModuleList([
582
+ MoonViTEncoderLayer(
583
+ **block_cfg,
584
+ use_deterministic_attn=self.use_deterministic_attn)
585
+ for _ in range(num_layers)
586
+ ])
587
+ norm_type = block_cfg.get('norm_type', 'layernorm')
588
+ if norm_type == "layernorm":
589
+ self.final_layernorm = nn.LayerNorm(hidden_dim)
590
+ elif norm_type == "rmsnorm":
591
+ self.final_layernorm = nn.RMSNorm(hidden_dim)
592
+ else:
593
+ raise NotImplementedError(f"Not support norm_type: {norm_type}")
594
+
595
+ def forward(
596
+ self,
597
+ hidden_states: torch.Tensor,
598
+ grid_thws: torch.Tensor,
599
+ ) -> torch.Tensor:
600
+ rope_freqs_cis = self.rope_2d.get_freqs_cis(
601
+ grid_thws=grid_thws, device=hidden_states.device)
602
+
603
+ lengths = torch.cat((
604
+ torch.zeros(1, dtype=grid_thws.dtype, device=grid_thws.device),
605
+ grid_thws[:, 0] * grid_thws[:, 1] * grid_thws[:, 2],
606
+ ))
607
+
608
+ max_seqlen = lengths.max()
609
+ cu_seqlens = lengths.to(hidden_states.device).cumsum(dim=0,
610
+ dtype=torch.int32)
611
+ for block in self.blocks:
612
+ hidden_states = block(hidden_states,
613
+ cu_seqlens,
614
+ max_seqlen,
615
+ rope_freqs_cis=rope_freqs_cis)
616
+
617
+ hidden_states = self.final_layernorm(hidden_states)
618
+ return hidden_states
619
+
620
+
621
+ def tpool_patch_merger(
622
+ x: torch.Tensor,
623
+ grid_thws: torch.Tensor,
624
+ merge_kernel_size: tuple[int, int] = (2, 2),
625
+ ) -> list[torch.Tensor]:
626
+ d_model = x.size(-1)
627
+
628
+ outputs = []
629
+ pre_sum = 0
630
+ for t, h, w in grid_thws.tolist():
631
+ # Get the current sequence
632
+ seq = x[pre_sum:pre_sum + t * h * w]
633
+ # Reshape along self.merge_kernel_size and concat to the last dimension
634
+ kernel_height, kernel_width = merge_kernel_size
635
+ new_height, new_width = h // kernel_height, w // kernel_width
636
+ reshaped_seq = seq.view(t, new_height, kernel_height, new_width,
637
+ kernel_width, d_model)
638
+ reshaped_seq = reshaped_seq.permute(0, 1,
639
+ 3, 2, 4, 5).contiguous().mean(
640
+ dim=0) # temporal pooling
641
+ padded_seq = reshaped_seq.view(new_height * new_width,
642
+ kernel_height * kernel_width, -1)
643
+ outputs.append(padded_seq)
644
+ pre_sum += t * h * w
645
+
646
+ return outputs
647
+
648
+
649
+ class MoonViT3dPretrainedModel(PreTrainedModel):
650
+ config_class = None
651
+ model_type = 'moonvit3d'
652
+ _no_split_modules = ['MoonViTEncoderLayer']
653
+ _supports_flash_attn_2 = True
654
+ _supports_sdpa = True
655
+
656
+ def __init__(self, config, *inputs, **kwargs):
657
+ super().__init__(config, *inputs, **kwargs)
658
+ config = deepcopy(config)
659
+ self.merge_kernel_size = config.merge_kernel_size
660
+ self.patch_size = config.patch_size
661
+ self.merge_type = config.merge_type
662
+
663
+ self.patch_embed = MoonVision3dPatchEmbed(
664
+ out_dim=config.hidden_size,
665
+ patch_size=config.patch_size,
666
+ pos_emb_height=config.init_pos_emb_height,
667
+ pos_emb_width=config.init_pos_emb_width,
668
+ pos_emb_time=config.init_pos_emb_time,
669
+ pos_emb_type=config.pos_emb_type,
670
+ patch_embed_proj_bias=getattr(config, 'patch_embed_proj_bias',
671
+ True),
672
+ pos_emb_interpolation_mode=getattr(
673
+ config, 'pos_emb_interpolation_mode', 'bicubic'),
674
+ )
675
+
676
+ self.encoder = MoonViT3dEncoder(
677
+ hidden_dim=config.hidden_size,
678
+ num_layers=config.num_hidden_layers,
679
+ block_cfg={
680
+ 'num_heads': config.num_attention_heads,
681
+ 'hidden_dim': config.hidden_size,
682
+ 'qkv_hidden_size': getattr(config, 'qkv_hidden_size', None),
683
+ 'mlp_dim': config.intermediate_size,
684
+ 'norm_type': getattr(config, 'norm_type', 'layernorm'),
685
+ 'mlp_type': getattr(config, 'mlp_type', 'mlp2'),
686
+ 'activation': PytorchGELUTanh(),
687
+ 'attn_bias': getattr(config, 'attn_bias', True),
688
+ 'linear_bias': getattr(config, 'linear_bias', True),
689
+ 'attn_implementation': config._attn_implementation,
690
+ },
691
+ use_deterministic_attn=getattr(self, 'use_deterministic_attn',
692
+ False))
693
+
694
+ def forward(self, pixel_values: torch.Tensor,
695
+ grid_thws: torch.Tensor) -> torch.Tensor:
696
+ """
697
+ Args:
698
+ pixel_values (torch.Tensor): The input pixel values.
699
+ grid_thws (torch.Tensor): Temporal, height and width.
700
+
701
+ Returns:
702
+ torch.Tensor: The output tokens.
703
+ """
704
+ # grid_thws = grid_thws.to('cpu')
705
+ assert grid_thws.ndim == 2, f'grid_thws should be 2D, got {grid_thws.ndim}'
706
+ assert grid_thws.size(1) == 3, f'No support for thw: {grid_thws}'
707
+ hidden_states = self.patch_embed(pixel_values, grid_thws)
708
+ hidden_states = self.encoder(hidden_states, grid_thws)
709
+ if self.merge_type == 'sd2_tpool': # spatial downsampling 2x with temporal pooling all
710
+ hidden_states = tpool_patch_merger(
711
+ hidden_states,
712
+ grid_thws,
713
+ merge_kernel_size=self.merge_kernel_size)
714
+ else:
715
+ raise NotImplementedError(f'Not support {self.merge_type}')
716
+
717
+ return hidden_states
718
+
719
+
720
+ # ============================================================================
721
+ # MM Projector Helper Classes (from mm_projector/modeling_mm_projectors.py)
722
+ # ============================================================================
723
+
724
+
725
+ class IdentityMap(nn.Module):
726
+
727
+ def __init__(self):
728
+ super().__init__()
729
+
730
+ def forward(self, x, *args, **kwargs):
731
+ return x
732
+
733
+
734
+ class MLP(nn.Module):
735
+
736
+ def __init__(self, config):
737
+ super().__init__()
738
+ # TODO, use faster LayerNorm
739
+ self.pre_norm = nn.LayerNorm(config.mm_hidden_size)
740
+ self.proj = nn.Sequential(
741
+ nn.Linear(config.mm_hidden_size, config.hidden_size), nn.GELU(),
742
+ nn.Linear(config.hidden_size, config.hidden_size))
743
+
744
+ def forward(self, x, *args, **kwargs):
745
+ assert isinstance(x,
746
+ list | tuple), f'x is not a list or tuple: {type(x)}'
747
+ lengths = [item.shape[0] for item in x]
748
+ x = torch.cat(x, dim=0)
749
+ x = self.pre_norm(x)
750
+ x = self.proj(x)
751
+ x = torch.split(x, lengths, dim=0)
752
+
753
+ return x
754
+
755
+
756
+ class PatchMergerMLP(nn.Module):
757
+
758
+ def __init__(self, config):
759
+ super().__init__()
760
+ eps = config.projector_ln_eps
761
+ self.hidden_size = config.mm_hidden_size * (
762
+ config.merge_kernel_size[0] * config.merge_kernel_size[1])
763
+ self.pre_norm = nn.LayerNorm(config.mm_hidden_size, eps=eps)
764
+ self.proj = nn.Sequential(
765
+ nn.Linear(self.hidden_size, self.hidden_size),
766
+ nn.GELU(),
767
+ nn.Linear(self.hidden_size, config.hidden_size),
768
+ )
769
+
770
+ def forward(self, x, *args, **kwargs):
771
+ if isinstance(x, list) or isinstance(x, tuple):
772
+ x = [
773
+ self.proj(self.pre_norm(item).view(item.shape[0], -1))
774
+ for item in x
775
+ ]
776
+ else:
777
+ # B, N, N_k, C = x.shape
778
+ B = x.shape[0]
779
+ x = self.proj(self.pre_norm(x).view(B, -1, self.hidden_size))
780
+ return x
781
+
782
+
783
+ class PatchMergerMLPV2(nn.Module):
784
+
785
+ def __init__(self, config):
786
+ super().__init__()
787
+ eps = config.projector_ln_eps
788
+ self.hidden_size = config.mm_hidden_size * (
789
+ config.merge_kernel_size[0] * config.merge_kernel_size[1])
790
+ self.proj = nn.Sequential(
791
+ nn.Linear(self.hidden_size, self.hidden_size, bias=False),
792
+ nn.GELU(),
793
+ nn.Linear(self.hidden_size, config.hidden_size, bias=False),
794
+ )
795
+ self.post_norm = nn.RMSNorm(config.hidden_size, eps=eps)
796
+ for m in self.proj.modules():
797
+ if isinstance(m, nn.Linear):
798
+ nn.init.trunc_normal_(m.weight,
799
+ std=math.sqrt(2 / m.in_features))
800
+ if m.bias is not None:
801
+ nn.init.zeros_(m.bias)
802
+
803
+ def forward(self, x, *args, **kwargs):
804
+ if isinstance(x, list) or isinstance(x, tuple):
805
+ lengths = [item.shape[0] for item in x]
806
+ x = torch.concat([item.view(item.shape[0], -1) for item in x],
807
+ dim=0)
808
+ x = self.post_norm(self.proj(x))
809
+ x = torch.split(x, lengths, dim=0)
810
+ else:
811
+ # B, N, N_k, C = x.shape
812
+ B = x.shape[0]
813
+ x = self.proj(x.view(B, -1, self.hidden_size))
814
+ x = self.post_norm(x)
815
+ return x
816
+
817
+
818
+ class KimiK3PreTrainedModel(PreTrainedModel):
819
+ config_class = KimiK3Config
820
+ base_model_prefix = "model"
821
+ _no_split_modules = [
822
+ "MoonViT3dPretrainedModel",
823
+ "MoonViTEncoderLayer",
824
+ "KimiDecoderLayer",
825
+ "PatchMergerMLP",
826
+ "PatchMergerMLPV2",
827
+ ]
828
+ _skip_keys_device_placement = "past_key_values"
829
+ _supports_flash_attn_2 = True
830
+ _supports_sdpa = False
831
+
832
+ def _init_weights(self, module):
833
+ # important: this ported version of Llava isn't meant for training from scratch - only
834
+ # inference and fine-tuning - so the proper init weights code has been removed - the original codebase
835
+ # https://github.com/haotian-liu/LLaVA/tree/main/llava should serve for that purpose
836
+ std = (self.config.initializer_range if hasattr(
837
+ self.config, "initializer_range") else
838
+ self.config.text_config.initializer_range)
839
+
840
+ if hasattr(module, "class_embedding"):
841
+ module.class_embedding.data.normal_(mean=0.0, std=std)
842
+
843
+ if isinstance(module, (nn.Linear, nn.Conv2d)):
844
+ module.weight.data.normal_(mean=0.0, std=std)
845
+ if module.bias is not None:
846
+ module.bias.data.zero_()
847
+ elif isinstance(module, nn.Embedding):
848
+ module.weight.data.normal_(mean=0.0, std=std)
849
+ if module.padding_idx is not None:
850
+ module.weight.data[module.padding_idx].zero_()
851
+
852
+
853
+ class VisionTowerConfig(PretrainedConfig):
854
+ model_type = 'moonvit3d'
855
+
856
+ def __init__(self, config: KimiK3Config, **kwargs):
857
+ super().__init__(**kwargs)
858
+ self.patch_size = config.patch_size
859
+ self.init_pos_emb_height = config.init_pos_emb_height
860
+ self.init_pos_emb_width = config.init_pos_emb_width
861
+ self.init_pos_emb_time = config.init_pos_emb_time
862
+ self.pos_emb_type = config.pos_emb_type
863
+ self.num_attention_heads = config.vt_num_attention_heads
864
+ self.num_hidden_layers = config.vt_num_hidden_layers
865
+ self.hidden_size = config.vt_hidden_size
866
+ self.intermediate_size = config.vt_intermediate_size
867
+ self.merge_kernel_size = config.merge_kernel_size
868
+ self.merge_type = config.merge_type
869
+ self._attn_implementation = config._attn_implementation
870
+ self.qkv_hidden_size = getattr(config, 'qkv_hidden_size', None)
871
+ self.norm_type = getattr(config, 'norm_type', 'layernorm')
872
+ self.attn_bias = getattr(config, 'attn_bias', True)
873
+ self.patch_embed_proj_bias = getattr(config, 'patch_embed_proj_bias',
874
+ True)
875
+ self.mlp_type = getattr(config, 'mlp_type', 'mlp2')
876
+ self.linear_bias = getattr(config, 'linear_bias', True)
877
+ self.pos_emb_interpolation_mode = getattr(
878
+ config, 'pos_emb_interpolation_mode', 'bilinear')
879
+
880
+
881
+ class ProjectorConfig:
882
+
883
+ def __init__(self, config: KimiK3Config):
884
+ self.mm_projector_type = config.mm_projector_type
885
+ self.mm_hidden_size = config.mm_hidden_size
886
+ self.hidden_size = config.text_hidden_size
887
+ self.merge_kernel_size = config.merge_kernel_size
888
+ self.projector_hidden_act = config.projector_hidden_act
889
+ self.projector_ln_eps = config.projector_ln_eps
890
+
891
+
892
+ # ref https://github.com/huggingface/transformers/blob/78b2929c0554b79e0489b451ce4ece14d265ead2/src/transformers/models/llava/modeling_llava.py#L240
893
+ class KimiK3ForConditionalGeneration(KimiK3PreTrainedModel):
894
+
895
+ @classmethod
896
+ def _supports_default_dynamic_cache(cls) -> bool:
897
+ return False
898
+
899
+ def __init__(self, config: KimiK3Config):
900
+ super().__init__(config)
901
+
902
+ vt_config = VisionTowerConfig(config.vision_config)
903
+ self.vision_tower = MoonViT3dPretrainedModel(vt_config)
904
+
905
+ proj_config = ProjectorConfig(config.vision_config)
906
+ if proj_config.mm_projector_type == 'identity':
907
+ self.mm_projector = IdentityMap()
908
+ elif proj_config.mm_projector_type == 'mlp':
909
+ self.mm_projector = MLP(proj_config)
910
+ elif proj_config.mm_projector_type == 'patchmerger':
911
+ self.mm_projector = PatchMergerMLP(proj_config)
912
+ elif proj_config.mm_projector_type == 'patchmergerv2':
913
+ self.mm_projector = PatchMergerMLPV2(proj_config)
914
+ else:
915
+ raise ValueError(
916
+ f"Unsupported mm_projector_type: {proj_config.mm_projector_type}"
917
+ )
918
+
919
+ self.language_model = KimiLinearForCausalLM(config.text_config)
920
+ self.post_init()
921
+
922
+ if hasattr(self.language_model, 'dtype'):
923
+ target_dtype = self.language_model.dtype
924
+ self.vision_tower = self.vision_tower.to(dtype=target_dtype)
925
+ self.mm_projector = self.mm_projector.to(dtype=target_dtype)
926
+
927
+ def get_input_embeddings(self):
928
+ return self.language_model.get_input_embeddings()
929
+
930
+ def set_input_embeddings(self, value):
931
+ self.language_model.set_input_embeddings(value)
932
+
933
+ def get_output_embeddings(self):
934
+ return self.language_model.get_output_embeddings()
935
+
936
+ def set_output_embeddings(self, new_embeddings):
937
+ self.language_model.set_output_embeddings(new_embeddings)
938
+
939
+ def set_decoder(self, decoder):
940
+ self.language_model.set_decoder(decoder)
941
+
942
+ def get_decoder(self):
943
+ return self.language_model.get_decoder()
944
+
945
+ def tie_weights(self):
946
+ return self.language_model.tie_weights()
947
+
948
+ def resize_token_embeddings(self,
949
+ new_num_tokens: int | None = None,
950
+ pad_to_multiple_of=None) -> nn.Embedding:
951
+ model_embeds = self.language_model.resize_token_embeddings(
952
+ new_num_tokens, pad_to_multiple_of)
953
+ # update vocab size
954
+ self.config.text_config.vocab_size = model_embeds.num_embeddings
955
+ self.vocab_size = model_embeds.num_embeddings
956
+ return model_embeds
957
+
958
+ def _merge_input_ids_with_image_features(
959
+ self,
960
+ image_features: list[torch.Tensor],
961
+ inputs_embeds: torch.Tensor,
962
+ input_ids: torch.Tensor,
963
+ attention_mask: torch.Tensor,
964
+ labels: torch.Tensor | None = None,
965
+ ):
966
+ """
967
+ Args:
968
+ image_features (:obj:`torch.Tensor` of shape :obj:`(num_image_tokens, embed_dim)`):
969
+ The image features to merge with the input embeddings.
970
+ inputs_embeds (:obj:`torch.Tensor` of shape :obj:`(batch_size, sequence_length, embed_dim)`):
971
+ The input embeddings.
972
+ input_ids (:obj:`torch.Tensor` of shape :obj:`(batch_size, sequence_length)`):
973
+ The input ids.
974
+ attention_mask (:obj:`torch.Tensor` of shape :obj:`(batch_size, sequence_length)`):
975
+ The attention mask.
976
+ labels (:obj:`torch.Tensor` of shape :obj:`(batch_size, sequence_length)`, *optional*):
977
+ The labels.
978
+ """
979
+ _, embed_dim = image_features[0].shape
980
+ feature_lengths = [x.shape[0] for x in image_features]
981
+ image_features = torch.cat(image_features, dim=0)
982
+
983
+ image_token_index: int = self.config.media_placeholder_token_id
984
+ pad_token_id: int = self.config.pad_token_id
985
+ ignore_index: int = self.config.ignore_index
986
+
987
+ batch_size, sequence_length = input_ids.shape
988
+ left_padding = not torch.sum(
989
+ input_ids[:, -1] == torch.tensor(pad_token_id))
990
+
991
+ # 1. Create a mask to know where special image tokens are
992
+ _token_occupation_table = torch.ones_like(input_ids.flatten())
993
+ _token_occupation_table[input_ids.flatten() ==
994
+ image_token_index] = torch.tensor(
995
+ feature_lengths,
996
+ dtype=torch.long,
997
+ device=input_ids.device)
998
+ _token_occupation_table = _token_occupation_table.reshape(
999
+ input_ids.shape)
1000
+
1001
+ max_embed_dim = _token_occupation_table.sum(-1).max().item()
1002
+ assert (
1003
+ max_embed_dim >= sequence_length
1004
+ ), f"The maximum embedding dimension ({max_embed_dim}) is less than the sequence length ({sequence_length})"
1005
+ batch_indices, non_image_indices = torch.where(
1006
+ input_ids != image_token_index)
1007
+
1008
+ # 2. Compute the positions where text should be written
1009
+ # Calculate new positions for text tokens in merged image-text sequence.
1010
+ new_token_positions = torch.cumsum(_token_occupation_table, -1) - 1
1011
+ nb_image_pad = max_embed_dim - 1 - new_token_positions[:, -1]
1012
+ if left_padding:
1013
+ new_token_positions += nb_image_pad[:,
1014
+ None] # offset for left padding
1015
+ text_to_overwrite = new_token_positions[batch_indices,
1016
+ non_image_indices]
1017
+
1018
+ # 3. Create the full embedding, already padded to the maximum position
1019
+ final_embedding = torch.zeros(
1020
+ batch_size,
1021
+ max_embed_dim,
1022
+ embed_dim,
1023
+ dtype=inputs_embeds.dtype,
1024
+ device=inputs_embeds.device,
1025
+ )
1026
+ final_attention_mask = torch.zeros(batch_size,
1027
+ max_embed_dim,
1028
+ dtype=attention_mask.dtype,
1029
+ device=inputs_embeds.device)
1030
+ if labels is not None:
1031
+ final_labels = torch.full(
1032
+ (batch_size, max_embed_dim),
1033
+ ignore_index,
1034
+ dtype=input_ids.dtype,
1035
+ device=input_ids.device,
1036
+ )
1037
+ # In case the Vision model or the Language model has been offloaded to CPU, we need to manually
1038
+ # set the corresponding tensors into their correct target device.
1039
+ target_device = inputs_embeds.device
1040
+ batch_indices, non_image_indices, text_to_overwrite = (
1041
+ batch_indices.to(target_device),
1042
+ non_image_indices.to(target_device),
1043
+ text_to_overwrite.to(target_device),
1044
+ )
1045
+ attention_mask = attention_mask.to(target_device)
1046
+
1047
+ # 4. Fill the embeddings based on the mask.
1048
+ final_embedding[batch_indices,
1049
+ text_to_overwrite] = inputs_embeds[batch_indices,
1050
+ non_image_indices]
1051
+ final_attention_mask[batch_indices,
1052
+ text_to_overwrite] = attention_mask[
1053
+ batch_indices, non_image_indices]
1054
+ if labels is not None:
1055
+ final_labels[batch_indices,
1056
+ text_to_overwrite] = labels[batch_indices,
1057
+ non_image_indices]
1058
+
1059
+ # 5. Fill the embeddings corresponding to the images. Anything that is not `text_positions` needs filling (#29835)
1060
+ image_to_overwrite = torch.full((batch_size, max_embed_dim),
1061
+ True,
1062
+ dtype=torch.bool,
1063
+ device=inputs_embeds.device)
1064
+ image_to_overwrite[batch_indices, text_to_overwrite] = False
1065
+ image_to_overwrite &= image_to_overwrite.cumsum(
1066
+ -1) - 1 >= nb_image_pad[:, None].to(target_device)
1067
+
1068
+ if image_to_overwrite.sum() != image_features.shape[:-1].numel():
1069
+ raise ValueError(
1070
+ f"The input provided to the model are wrong. The number of image tokens is {image_to_overwrite.sum()} while"
1071
+ f" the number of image features given to the model is {image_features.shape[:-1].numel()}. "
1072
+ "This prevents correct indexing and breaks batch generation.")
1073
+
1074
+ final_embedding[image_to_overwrite] = (
1075
+ image_features.contiguous().reshape(-1,
1076
+ embed_dim).to(target_device))
1077
+ final_attention_mask |= image_to_overwrite
1078
+ position_ids = (final_attention_mask.cumsum(-1) - 1).masked_fill_(
1079
+ (final_attention_mask == 0), 1)
1080
+
1081
+ # 6. Mask out the embedding at padding positions, as we later use the past_key_value value to determine the non-attended tokens.
1082
+ batch_indices, pad_indices = torch.where(input_ids == pad_token_id)
1083
+ indices_to_mask = new_token_positions[batch_indices, pad_indices]
1084
+
1085
+ final_embedding[batch_indices, indices_to_mask] = 0
1086
+
1087
+ if labels is None:
1088
+ final_labels = None
1089
+
1090
+ return final_embedding, final_attention_mask, final_labels, position_ids
1091
+
1092
+ def _extract_image_features(self, pixel_values: torch.Tensor,
1093
+ grid_thws: torch.Tensor) -> list[torch.Tensor]:
1094
+ """
1095
+ Args:
1096
+ pixel_values (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, num_channels, height, width)`):
1097
+ The pixel values of the images processed by image processor.
1098
+ grid_thws (:obj:`torch.Tensor` of shape :obj:`(batch_size, 3)`):
1099
+ The grid, height, width of the images.
1100
+
1101
+ Returns:
1102
+ selected_image_feature (:obj:`torch.FloatTensor` of shape :obj:`(num_image_tokens, embed_dim)`):
1103
+ The selected image features to use as input to the projector head.
1104
+
1105
+ """
1106
+
1107
+ target_dtype = self.vision_tower.patch_embed.proj.weight.dtype
1108
+ pixel_values = pixel_values.to(target_dtype)
1109
+
1110
+ image_features = self.vision_tower(pixel_values, grid_thws)
1111
+ return image_features
1112
+
1113
+ def forward(
1114
+ self,
1115
+ input_ids: torch.LongTensor | None = None,
1116
+ pixel_values: torch.FloatTensor | list[torch.FloatTensor]
1117
+ | None = None,
1118
+ grid_thws: torch.Tensor | None = None,
1119
+ attention_mask: torch.Tensor | None = None,
1120
+ position_ids: torch.LongTensor | None = None,
1121
+ past_key_values: list[torch.FloatTensor] | None = None,
1122
+ inputs_embeds: torch.FloatTensor | None = None,
1123
+ labels: torch.LongTensor | None = None,
1124
+ use_cache: bool | None = None,
1125
+ output_attentions: bool | None = None,
1126
+ output_hidden_states: bool | None = None,
1127
+ return_dict: bool | None = None,
1128
+ ) -> tuple | LlavaCausalLMOutputWithPast:
1129
+ r"""
1130
+ Args:
1131
+ labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*):
1132
+ Labels for computing the masked language modeling loss. Indices should either be in `[0, ...,
1133
+ config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored
1134
+ (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`.
1135
+
1136
+ ```"""
1137
+ assert self.vision_tower is not None, "vision_tower is not loaded"
1138
+ output_attentions = (output_attentions if output_attentions is not None
1139
+ else self.config.output_attentions)
1140
+ output_hidden_states = (output_hidden_states
1141
+ if output_hidden_states is not None else
1142
+ self.config.output_hidden_states)
1143
+ return_dict = return_dict if return_dict is not None else self.config.use_return_dict
1144
+
1145
+ if inputs_embeds is None:
1146
+ # 1. Extra the input embeddings
1147
+ inputs_embeds = self.get_input_embeddings()(input_ids)
1148
+
1149
+ # 2. Merge text and images
1150
+ if pixel_values is not None and len(
1151
+ pixel_values) > 0 and input_ids.shape[1] != 1:
1152
+ image_features = self._extract_image_features(
1153
+ pixel_values, grid_thws)
1154
+ if self.mm_projector:
1155
+ image_features = self.mm_projector(image_features)
1156
+
1157
+ inputs_embeds = inputs_embeds.to(
1158
+ image_features[0].dtype) # num_tokens, embed_dim
1159
+ inputs_embeds, attention_mask, labels, position_ids = (
1160
+ self._merge_input_ids_with_image_features(
1161
+ image_features,
1162
+ inputs_embeds,
1163
+ input_ids,
1164
+ attention_mask,
1165
+ labels,
1166
+ ))
1167
+
1168
+ # In case input_ids.shape[1] == 1 & pixel_values==None & past_key_values != None, we are in the case of
1169
+ # generation with cache
1170
+ elif (past_key_values is not None and pixel_values is not None
1171
+ and input_ids.shape[1] == 1):
1172
+ # Retrieve the first layer to inspect the logits and mask out the hidden states
1173
+ # that are set to 0
1174
+ first_layer_past_key_value = past_key_values[0][0][:, :, :, 0]
1175
+
1176
+ # Sum all dimensions of head_dim (-2) to avoid random errors such as: https://github.com/huggingface/transformers/pull/28032#issuecomment-1863691941
1177
+ batch_index, non_attended_tokens = torch.where(
1178
+ first_layer_past_key_value.float().sum(-2) == 0)
1179
+
1180
+ # Get the target length
1181
+ target_length = input_ids.shape[1]
1182
+ past_length = first_layer_past_key_value.shape[-1]
1183
+
1184
+ extended_attention_mask = torch.ones(
1185
+ (attention_mask.shape[0], past_length),
1186
+ dtype=attention_mask.dtype,
1187
+ device=attention_mask.device,
1188
+ )
1189
+
1190
+ # Filter out only the tokens that can be un-attended, this can happen
1191
+ # if one uses Llava + Fused modules where the cache on the
1192
+ # first iteration is already big enough, or if one passes custom cache
1193
+ valid_indices = non_attended_tokens < extended_attention_mask.size(
1194
+ -1)
1195
+ new_batch_index = batch_index[valid_indices]
1196
+ new_non_attended_tokens = non_attended_tokens[valid_indices]
1197
+
1198
+ # Zero-out the places where we don't need to attend
1199
+ extended_attention_mask[new_batch_index,
1200
+ new_non_attended_tokens] = 0
1201
+
1202
+ attention_mask = torch.cat(
1203
+ (extended_attention_mask, attention_mask[:,
1204
+ -target_length:]),
1205
+ dim=1)
1206
+ position_ids = torch.sum(attention_mask,
1207
+ dim=1).unsqueeze(-1) - 1
1208
+
1209
+ outputs = self.language_model(
1210
+ attention_mask=attention_mask,
1211
+ position_ids=position_ids,
1212
+ past_key_values=past_key_values,
1213
+ inputs_embeds=inputs_embeds,
1214
+ use_cache=use_cache,
1215
+ output_attentions=output_attentions,
1216
+ output_hidden_states=output_hidden_states,
1217
+ return_dict=return_dict,
1218
+ )
1219
+
1220
+ logits = outputs[0]
1221
+
1222
+ loss = None
1223
+ if labels is not None:
1224
+ # Shift so that tokens < n predict n
1225
+ if attention_mask is not None:
1226
+ shift_attention_mask = attention_mask[..., 1:]
1227
+ shift_logits = logits[..., :-1, :][shift_attention_mask.to(
1228
+ logits.device) != 0].contiguous()
1229
+ shift_labels = labels[..., 1:][shift_attention_mask.to(
1230
+ labels.device) != 0].contiguous()
1231
+ else:
1232
+ shift_logits = logits[..., :-1, :].contiguous()
1233
+ shift_labels = labels[..., 1:].contiguous()
1234
+ # Flatten the tokens
1235
+ loss_fct = nn.CrossEntropyLoss()
1236
+ loss = loss_fct(
1237
+ shift_logits.view(-1, shift_logits.size(-1)),
1238
+ shift_labels.view(-1).to(shift_logits.device),
1239
+ )
1240
+
1241
+ if not return_dict:
1242
+ output = (logits, ) + outputs[1:]
1243
+ return (loss, ) + output if loss is not None else output
1244
+
1245
+ return LlavaCausalLMOutputWithPast(
1246
+ loss=loss,
1247
+ logits=logits,
1248
+ past_key_values=outputs.past_key_values,
1249
+ hidden_states=outputs.hidden_states,
1250
+ attentions=outputs.attentions,
1251
+ )
1252
+
1253
+ def prepare_inputs_for_generation(
1254
+ self,
1255
+ input_ids,
1256
+ past_key_values=None,
1257
+ inputs_embeds=None,
1258
+ pixel_values=None,
1259
+ grid_thws=None,
1260
+ attention_mask=None,
1261
+ **kwargs,
1262
+ ):
1263
+ if past_key_values is not None:
1264
+ if hasattr(past_key_values, "get_seq_length"):
1265
+ cache_length = past_key_values.get_seq_length()
1266
+ past_length = getattr(past_key_values, 'seen_tokens',
1267
+ cache_length)
1268
+ else:
1269
+ cache_length = past_length = past_key_values[0][0].shape[2]
1270
+
1271
+ # Keep only the unprocessed tokens:
1272
+ # 1 - If the length of the attention_mask exceeds the length of input_ids, then we are in a setting where
1273
+ # some of the inputs are exclusively passed as part of the cache (e.g. when passing input_embeds as
1274
+ # input)
1275
+ if attention_mask is not None and attention_mask.shape[
1276
+ 1] > input_ids.shape[1]:
1277
+ input_ids = input_ids[:, -(attention_mask.shape[1] -
1278
+ past_length):]
1279
+ # 2 - If the past_length is smaller than input_ids', then input_ids holds all input tokens. We can discard
1280
+ # input_ids based on the past_length.
1281
+ elif past_length < input_ids.shape[1]:
1282
+ input_ids = input_ids[:, past_length:]
1283
+ # 3 - Otherwise (past_length >= input_ids.shape[1]), let's assume input_ids only has unprocessed tokens.
1284
+ elif self.config.media_placeholder_token_id in input_ids:
1285
+ input_ids = input_ids[:, input_ids.shape[1] - 1:]
1286
+ # If the cache has seen more tokens than it can hold, then the cache has a size limit. Let's discard the
1287
+ # older attention values, as their corresponding values are not part of the input.
1288
+ if cache_length < past_length and attention_mask is not None:
1289
+ attention_mask = attention_mask[:, -(cache_length +
1290
+ input_ids.shape[1]):]
1291
+
1292
+ position_ids = kwargs.get("position_ids", None)
1293
+ if attention_mask is not None and position_ids is None:
1294
+ # create position_ids on the fly for batch generation
1295
+ position_ids = attention_mask.long().cumsum(-1) - 1
1296
+ position_ids.masked_fill_(attention_mask == 0, 1)
1297
+ if past_key_values:
1298
+ position_ids = position_ids[:, -input_ids.shape[1]:]
1299
+
1300
+ # if `inputs_embeds` are passed, we only want to use them in the 1st generation step
1301
+ if inputs_embeds is not None and past_key_values is None:
1302
+ model_inputs = {"inputs_embeds": inputs_embeds}
1303
+ else:
1304
+ model_inputs = {"input_ids": input_ids}
1305
+
1306
+ model_inputs.update({
1307
+ "position_ids": position_ids,
1308
+ "past_key_values": past_key_values,
1309
+ "use_cache": kwargs.get("use_cache"),
1310
+ "attention_mask": attention_mask,
1311
+ "pixel_values": pixel_values,
1312
+ "grid_thws": grid_thws,
1313
+ })
1314
+ return model_inputs
1315
+
1316
+ def _reorder_cache(self, *args, **kwargs):
1317
+ return self.language_model._reorder_cache(*args, **kwargs)
modeling_kimi_linear.py ADDED
@@ -0,0 +1,1314 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+ # Copyright 2025-2026 The Moonshot AI Team, DeepSeek-AI, and HuggingFace Inc. team. All rights reserved.
3
+ #
4
+ # The multi-head latent attention, MoE gating and sparse MoE block in this file are
5
+ # adapted from DeepSeek-V3 (DeepSeek-V3/modeling_deepseek.py). They have been
6
+ # extensively modified and extended for the Kimi-Linear architecture.
7
+ #
8
+ # Licensing Information:
9
+ # - Code adapted from DeepSeek-V3 (DeepSeek-V3/modeling_deepseek.py) is licensed under the Apache License, Version 2.0.
10
+ # - Other parts of the code are licensed under the Kimi K3 License (see the LICENSE file in this repository).
11
+ #
12
+ # Apache License, Version 2.0:
13
+ # Licensed under the Apache License, Version 2.0 (the "License");
14
+ # you may not use this file except in compliance with the License.
15
+ # You may obtain a copy of the License at
16
+ #
17
+ # http://www.apache.org/licenses/LICENSE-2.0
18
+ #
19
+ # Unless required by applicable law or agreed to in writing, software
20
+ # distributed under the License is distributed on an "AS IS" BASIS,
21
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22
+ # See the License for the specific language governing permissions and
23
+ # limitations under the License.
24
+ import math
25
+ from collections.abc import Callable
26
+ from typing import Any
27
+
28
+ import torch
29
+ import torch.nn.functional as F
30
+ import transformers
31
+ from einops import rearrange
32
+ from packaging import version
33
+ from torch import nn
34
+ from transformers.activations import ACT2FN
35
+ from transformers.cache_utils import Cache
36
+ from transformers.generation import GenerationMixin
37
+ from transformers.masking_utils import create_causal_mask
38
+ from transformers.modeling_flash_attention_utils import FlashAttentionKwargs
39
+ from transformers.modeling_outputs import BaseModelOutputWithPast, CausalLMOutputWithPast
40
+ from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel
41
+ from transformers.processing_utils import Unpack
42
+ from transformers.pytorch_utils import ALL_LAYERNORM_LAYERS
43
+ from transformers.utils import TransformersKwargs, auto_docstring, can_return_tuple, logging
44
+ from transformers.utils.generic import OutputRecorder, check_model_inputs
45
+
46
+ try:
47
+ from fla.modules import FusedRMSNormGated, ShortConvolution
48
+ from fla.ops.kda import chunk_kda, fused_recurrent_kda
49
+ # from fla.ops.kda.gate import fused_kda_gate # deprecated, gate is now computed inside chunk_kda/fused_recurrent_kda
50
+ from fla.ops.utils.index import prepare_cu_seqlens_from_mask, prepare_lens_from_mask
51
+ from fla.utils import tensor_cache
52
+ except ImportError:
53
+ raise ImportError("Plese run `pip install -U fla-core`")
54
+
55
+ from .configuration_kimi_k3 import KimiLinearConfig
56
+
57
+ assert version.parse(transformers.__version__) >= version.parse("4.56.0"), \
58
+ "Please upgrade transformers to >= 4.56.0"
59
+
60
+ logger = logging.get_logger(__name__)
61
+
62
+
63
+ # Register Moonshot-specific activation functions
64
+ class SituAndMul(nn.Module):
65
+ """
66
+ SituAndMul activation: beta * tanh(gate / beta) * sigmoid(gate) * up
67
+ When linear_beta is set, up is also transformed by linear_beta * tanh(up / linear_beta).
68
+ """
69
+
70
+ def __init__(self, beta: float = 1.0, linear_beta: float | None = None):
71
+ super().__init__()
72
+ self.beta = beta
73
+ self.linear_beta = linear_beta
74
+
75
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
76
+ d = x.shape[-1] // 2
77
+ gate = x[..., :d].to(torch.float32)
78
+ up = x[..., d:].to(torch.float32)
79
+ situ_a = self.beta * torch.tanh(gate / self.beta) * torch.sigmoid(gate)
80
+ if self.linear_beta is not None:
81
+ up = self.linear_beta * torch.tanh(up / self.linear_beta)
82
+ return (situ_a * up).to(x.dtype)
83
+
84
+
85
+ ACT2FN["situ"] = SituAndMul
86
+
87
+
88
+ def _get_situ_activation_params(config: KimiLinearConfig):
89
+ beta = getattr(config, "activation_situ_beta", None)
90
+ linear_beta = getattr(config, "activation_situ_linear_beta", None)
91
+ return beta or 1.0, linear_beta
92
+
93
+
94
+ def index_first_axis(x, indices):
95
+ return x[indices]
96
+
97
+
98
+ @tensor_cache
99
+ def get_unpad_data(
100
+ attention_mask: torch.Tensor,
101
+ ) -> tuple[torch.Tensor, torch.Tensor, int]:
102
+ lens = prepare_lens_from_mask(attention_mask)
103
+ indices = torch.nonzero(attention_mask.flatten(), as_tuple=False).flatten()
104
+ max_seqlen_in_batch = lens.max().item()
105
+ cu_seqlens = prepare_cu_seqlens_from_mask(attention_mask)
106
+ return indices, cu_seqlens, max_seqlen_in_batch
107
+
108
+
109
+ def pad_input(
110
+ hidden_states: torch.Tensor,
111
+ indices: torch.LongTensor,
112
+ batch_size: int,
113
+ seq_len: int,
114
+ ) -> torch.Tensor:
115
+ out = hidden_states.new_zeros((batch_size * seq_len, *hidden_states.shape[1:]))
116
+ out[indices] = hidden_states
117
+ return out.view(batch_size, seq_len, *hidden_states.shape[1:])
118
+
119
+
120
+ class KimiDynamicCache:
121
+ """
122
+ Dynamic cache for Kimi model.
123
+ Inspired by Qwen3-Next
124
+ """
125
+ is_compileable = False
126
+
127
+ def __init__(self, config: KimiLinearConfig):
128
+ super().__init__()
129
+ self.config = config
130
+
131
+ if config.linear_attn_config is not None:
132
+ self.layer_types = []
133
+ for i in range(config.num_hidden_layers):
134
+ if config.is_kda_layer(i):
135
+ self.layer_types.append("linear_attention")
136
+ else:
137
+ self.layer_types.append("full_attention")
138
+ else:
139
+ self.layer_types = ["full_attention"] * config.num_hidden_layers
140
+
141
+ self.transformer_layers = [
142
+ i for i in range(config.num_hidden_layers) if self.layer_types[i] == "full_attention"
143
+ ]
144
+
145
+ linear_layers = [i for i in range(
146
+ config.num_hidden_layers) if self.layer_types[i] == "linear_attention"]
147
+ self.last_linear_layer = linear_layers[-1] if linear_layers else -1
148
+
149
+ self.conv_states = [None for _ in range(config.num_hidden_layers)]
150
+ self.recurrent_states = [None for _ in range(config.num_hidden_layers)]
151
+ self.key_cache = [None for _ in range(config.num_hidden_layers)]
152
+ self.value_cache = [None for _ in range(config.num_hidden_layers)]
153
+
154
+ def __len__(self):
155
+ return len(self.layer_types)
156
+
157
+ def update(
158
+ self,
159
+ key_states: torch.Tensor,
160
+ value_states: torch.Tensor,
161
+ layer_idx: int,
162
+ cache_kwargs: dict[str, Any] | None = None,
163
+ ) -> tuple[torch.Tensor, torch.Tensor]:
164
+ if self.key_cache[layer_idx] is None:
165
+ self.key_cache[layer_idx] = key_states
166
+ self.value_cache[layer_idx] = value_states
167
+ else:
168
+ self.key_cache[layer_idx] = torch.cat(
169
+ [self.key_cache[layer_idx], key_states], dim=2)
170
+ self.value_cache[layer_idx] = torch.cat(
171
+ [self.value_cache[layer_idx], value_states], dim=2)
172
+
173
+ return self.key_cache[layer_idx], self.value_cache[layer_idx]
174
+
175
+ def reorder_cache(self, beam_idx: torch.LongTensor):
176
+ """Reorders the cache for beam search, given the selected beam indices."""
177
+ for layer_idx in range(len(self.key_cache)):
178
+ if self.key_cache[layer_idx] is not None:
179
+ device = self.key_cache[layer_idx].device
180
+ beam_idx = beam_idx.to(device)
181
+ self.key_cache[layer_idx] = self.key_cache[layer_idx].index_select(
182
+ 0, beam_idx)
183
+ self.value_cache[layer_idx] = self.value_cache[layer_idx].index_select(
184
+ 0, beam_idx)
185
+
186
+ if self.conv_states[layer_idx] is not None:
187
+ device = self.conv_states[layer_idx][0].device
188
+ beam_idx = beam_idx.to(device)
189
+ q_conv, k_conv, v_conv = self.conv_states[layer_idx]
190
+ self.conv_states[layer_idx] = (
191
+ q_conv.index_select(0, beam_idx),
192
+ k_conv.index_select(0, beam_idx),
193
+ v_conv.index_select(0, beam_idx),
194
+ )
195
+ self.recurrent_states[layer_idx] = self.recurrent_states[layer_idx].index_select(
196
+ 0, beam_idx)
197
+
198
+ def get_seq_length(self, layer_idx: int | None = 0) -> int:
199
+ """Returns the sequence length of the cached states. A layer index can be optionally passed."""
200
+ # take any layer that contains cache and not empty tensor
201
+ layer_idx = self.transformer_layers[0] if layer_idx not in self.transformer_layers else layer_idx
202
+ if len(self.key_cache) <= layer_idx or self.key_cache[layer_idx] is None:
203
+ return 0
204
+ return self.key_cache[layer_idx].shape[-2]
205
+
206
+ def get_mask_sizes(self, cache_position: torch.Tensor, layer_idx: int) -> tuple[int, int]:
207
+ """
208
+ Return a tuple (kv_length, kv_offset) corresponding to the length and offset that will be returned for
209
+ the given layer at `layer_idx`.
210
+ The masks are then prepared according to the given lengths (kv_length, kv_offset) and patterns for each layer.
211
+ """
212
+ kv_offset = 0
213
+ query_length = cache_position.shape[0]
214
+ past_seen_tokens = self.get_seq_length(layer_idx)
215
+ kv_length = query_length + past_seen_tokens
216
+ return kv_length, kv_offset
217
+
218
+ @property
219
+ def has_previous_state(self):
220
+ """We have a previous state if the last linear (conv) layer was already updated."""
221
+ if self.last_linear_layer == -1:
222
+ return False
223
+ return self.conv_states[self.last_linear_layer] is not None
224
+
225
+
226
+ class KimiRMSNorm(nn.Module):
227
+ def __init__(self, hidden_size, eps=1e-6):
228
+ super().__init__()
229
+ self.weight = nn.Parameter(torch.ones(hidden_size))
230
+ self.variance_epsilon = eps
231
+
232
+ def forward(self, hidden_states):
233
+ dtype = hidden_states.dtype
234
+ x = hidden_states.float()
235
+ x = x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.variance_epsilon)
236
+ return self.weight * x.to(dtype)
237
+
238
+
239
+ ALL_LAYERNORM_LAYERS.append(KimiRMSNorm)
240
+
241
+
242
+ class KimiBlockSparseMLP(nn.Module):
243
+ def __init__(self, config: KimiLinearConfig, hidden_size=None, intermediate_size=None):
244
+ super().__init__()
245
+ self.config = config
246
+ self.ffn_dim = config.intermediate_size if intermediate_size is None else intermediate_size
247
+ self.hidden_dim = config.hidden_size if hidden_size is None else hidden_size
248
+
249
+ self.w1 = nn.Linear(self.hidden_dim, self.ffn_dim, bias=False) # gate
250
+ self.w2 = nn.Linear(self.ffn_dim, self.hidden_dim, bias=False) # down
251
+ self.w3 = nn.Linear(self.hidden_dim, self.ffn_dim, bias=False) # up
252
+
253
+ if config.hidden_act == "situ":
254
+ beta, linear_beta = _get_situ_activation_params(config)
255
+ self.act_fn = SituAndMul(
256
+ beta=beta,
257
+ linear_beta=linear_beta,
258
+ )
259
+ else:
260
+ self.act_fn = ACT2FN[config.hidden_act]
261
+
262
+ def forward(self, hidden_states):
263
+ if self.config.hidden_act == "situ":
264
+ gate_up = torch.cat([self.w1(hidden_states), self.w3(hidden_states)], dim=-1)
265
+ current_hidden_states = self.act_fn(gate_up)
266
+ else:
267
+ current_hidden_states = self.act_fn(
268
+ self.w1(hidden_states)) * self.w3(hidden_states)
269
+ current_hidden_states = self.w2(current_hidden_states)
270
+ return current_hidden_states
271
+
272
+
273
+ class KimiMLP(nn.Module):
274
+ def __init__(self, config: KimiLinearConfig, hidden_size=None, intermediate_size=None):
275
+ super().__init__()
276
+ self.config = config
277
+ self.hidden_size = config.hidden_size if hidden_size is None else hidden_size
278
+ self.intermediate_size = config.intermediate_size if intermediate_size is None else intermediate_size
279
+ self.gate_proj = nn.Linear(
280
+ self.hidden_size, self.intermediate_size, bias=False)
281
+ self.up_proj = nn.Linear(
282
+ self.hidden_size, self.intermediate_size, bias=False)
283
+ self.down_proj = nn.Linear(
284
+ self.intermediate_size, self.hidden_size, bias=False)
285
+ if config.hidden_act == "situ":
286
+ beta, linear_beta = _get_situ_activation_params(config)
287
+ self.act_fn = SituAndMul(
288
+ beta=beta,
289
+ linear_beta=linear_beta,
290
+ )
291
+ else:
292
+ self.act_fn = ACT2FN[config.hidden_act]
293
+
294
+ def forward(self, x):
295
+ if self.config.hidden_act == "situ":
296
+ gate_up = torch.cat([self.gate_proj(x), self.up_proj(x)], dim=-1)
297
+ down_proj = self.down_proj(self.act_fn(gate_up))
298
+ else:
299
+ down_proj = self.down_proj(self.act_fn(
300
+ self.gate_proj(x)) * self.up_proj(x))
301
+ return down_proj
302
+
303
+
304
+ def repeat_kv(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor:
305
+ """Expand the key/value heads from `num_key_value_heads` to `num_attention_heads`."""
306
+ if n_rep == 1:
307
+ return hidden_states
308
+ return torch.repeat_interleave(hidden_states, dim=1, repeats=n_rep)
309
+
310
+
311
+ def eager_attention_forward(
312
+ module: nn.Module,
313
+ query: torch.Tensor,
314
+ key: torch.Tensor,
315
+ value: torch.Tensor,
316
+ attention_mask: torch.Tensor | None,
317
+ scaling: float,
318
+ dropout: float = 0.0,
319
+ **kwargs: Unpack[TransformersKwargs],
320
+ ):
321
+ key = repeat_kv(key, module.num_key_value_groups)
322
+ value = repeat_kv(value, module.num_key_value_groups)
323
+
324
+ scores = torch.einsum("bhqd,bhkd->bhqk", query, key) * scaling
325
+ if attention_mask is not None:
326
+ scores = scores + attention_mask[:, :, :, : key.shape[-2]]
327
+
328
+ probs = F.softmax(scores, dim=-1, dtype=torch.float32).to(query.dtype)
329
+ probs = F.dropout(probs, p=dropout, training=module.training)
330
+ out = torch.einsum("bhqk,bhkd->bhqd", probs, value).transpose(1, 2).contiguous()
331
+
332
+ return out, probs
333
+
334
+
335
+ class KimiMLAAttention(nn.Module):
336
+ """
337
+ Multi-Latent Attention adapted from deepseek-v3
338
+ """
339
+
340
+ def __init__(self, config: KimiLinearConfig, layer_idx: int):
341
+ nn.Module.__init__(self)
342
+ self.config = config
343
+ self.layer_idx = layer_idx
344
+ self.hidden_size = config.hidden_size
345
+ self.num_heads = config.num_attention_heads
346
+ self.num_key_value_heads = config.num_key_value_heads
347
+ self.num_key_value_groups = self.num_heads // self.num_key_value_heads
348
+
349
+ self.attention_dropout = getattr(config, "attention_dropout", 0.0)
350
+
351
+ try:
352
+ self.q_lora_rank = config.q_lora_rank
353
+ self.qk_rope_head_dim = config.qk_rope_head_dim
354
+ self.kv_lora_rank = config.kv_lora_rank
355
+ self.v_head_dim = config.v_head_dim
356
+ self.qk_nope_head_dim = config.qk_nope_head_dim
357
+ self.q_head_dim = self.qk_nope_head_dim + self.qk_rope_head_dim
358
+ self.use_nope = config.mla_use_nope
359
+ self.scaling = self.q_head_dim ** (-0.5)
360
+ except Exception as e:
361
+ raise ValueError(
362
+ f"Kimi MLA config is not found or not properly formatted: {e}")
363
+
364
+ if self.q_lora_rank is not None:
365
+ self.q_a_proj = nn.Linear(
366
+ self.hidden_size, self.q_lora_rank, bias=False,
367
+ )
368
+ self.q_a_layernorm = KimiRMSNorm(self.q_lora_rank)
369
+ self.q_b_proj = nn.Linear(
370
+ self.q_lora_rank,
371
+ self.num_heads * self.q_head_dim,
372
+ bias=False,
373
+ )
374
+ else:
375
+ self.q_proj = nn.Linear(
376
+ self.hidden_size, self.num_heads * self.q_head_dim, bias=False,
377
+ )
378
+ self.kv_a_proj_with_mqa = nn.Linear(
379
+ self.hidden_size,
380
+ self.kv_lora_rank + self.qk_rope_head_dim,
381
+ bias=False,
382
+ )
383
+ self.kv_a_layernorm = KimiRMSNorm(self.kv_lora_rank)
384
+ self.kv_b_proj = nn.Linear(
385
+ self.kv_lora_rank,
386
+ self.num_heads
387
+ * (self.q_head_dim - self.qk_rope_head_dim + self.v_head_dim),
388
+ bias=False,
389
+ )
390
+ self.o_proj = nn.Linear(
391
+ self.num_heads * self.v_head_dim,
392
+ self.hidden_size,
393
+ bias=False,
394
+ )
395
+ self.is_causal = True
396
+ assert self.use_nope
397
+
398
+ self.use_output_gate = getattr(config, "mla_use_output_gate", False)
399
+ if self.use_output_gate:
400
+ projection_size = self.num_heads * self.v_head_dim
401
+ self.g_proj = nn.Linear(self.hidden_size, projection_size, bias=False)
402
+
403
+ self.rotary_emb = None
404
+
405
+ def forward(
406
+ self,
407
+ hidden_states: torch.Tensor,
408
+ attention_mask: torch.Tensor | None = None,
409
+ position_ids: torch.LongTensor | None = None,
410
+ past_key_values: Cache | None = None,
411
+ **kwargs,
412
+ ) -> tuple[torch.Tensor, torch.Tensor | None, tuple[torch.Tensor] | None]:
413
+ batch_size, seq_length = hidden_states.shape[:-1]
414
+ query_shape = (batch_size, seq_length, -1, self.q_head_dim)
415
+ key_shape = (batch_size, seq_length, -1,
416
+ self.qk_nope_head_dim + self.v_head_dim)
417
+
418
+ if self.q_lora_rank is not None:
419
+ q_states = self.q_b_proj(self.q_a_layernorm(self.q_a_proj(hidden_states)))
420
+ else:
421
+ q_states = self.q_proj(hidden_states)
422
+ q_states = q_states.view(query_shape).transpose(1, 2)
423
+ q_pass, q_rot = torch.split(
424
+ q_states, [self.qk_nope_head_dim, self.qk_rope_head_dim], dim=-1)
425
+
426
+ compressed_kv = self.kv_a_proj_with_mqa(hidden_states)
427
+ k_pass, k_rot = torch.split(
428
+ compressed_kv, [self.kv_lora_rank, self.qk_rope_head_dim], dim=-1)
429
+
430
+ k_pass = self.kv_b_proj(self.kv_a_layernorm(
431
+ k_pass)).view(key_shape).transpose(1, 2)
432
+ k_pass, value_states = torch.split(
433
+ k_pass, [self.qk_nope_head_dim, self.v_head_dim], dim=-1)
434
+
435
+ k_rot = k_rot.view(batch_size, 1, seq_length, self.qk_rope_head_dim)
436
+
437
+ k_rot = k_rot.expand(*k_pass.shape[:-1], -1)
438
+
439
+ query_states = torch.cat((q_pass, q_rot), dim=-1)
440
+ key_states = torch.cat((k_pass, k_rot), dim=-1)
441
+
442
+ if past_key_values is not None:
443
+ key_states, value_states = past_key_values.update(
444
+ key_states, value_states, self.layer_idx)
445
+
446
+ if self.config._attn_implementation == "flash_attention_2" and self.q_head_dim != self.v_head_dim:
447
+ value_states = F.pad(
448
+ value_states, [0, self.q_head_dim - self.v_head_dim])
449
+
450
+ attention_interface: Callable = eager_attention_forward
451
+ if self.config._attn_implementation != "eager":
452
+ attention_interface = ALL_ATTENTION_FUNCTIONS[self.config._attn_implementation]
453
+
454
+ attn_output, _ = attention_interface(
455
+ self,
456
+ query_states,
457
+ key_states,
458
+ value_states,
459
+ attention_mask,
460
+ dropout=0.0 if not self.training else self.attention_dropout,
461
+ scaling=self.scaling,
462
+ **kwargs,
463
+ )
464
+
465
+ if self.config._attn_implementation == "flash_attention_2" and self.q_head_dim != self.v_head_dim:
466
+ attn_output = attn_output[:, :, :, : self.v_head_dim]
467
+
468
+ attn_output = attn_output.reshape(
469
+ batch_size, seq_length, -1).contiguous()
470
+ if self.use_output_gate:
471
+ g = self.g_proj(hidden_states).sigmoid()
472
+ attn_output = attn_output * g
473
+ attn_output = self.o_proj(attn_output)
474
+ return attn_output
475
+
476
+
477
+ class KimiDeltaAttention(nn.Module):
478
+ def __init__(self, config: KimiLinearConfig, layer_idx: int):
479
+ super().__init__()
480
+ self.config = config
481
+ self.mode = "chunk"
482
+
483
+ self.hidden_size = config.hidden_size
484
+ self.conv_size = config.linear_attn_config["short_conv_kernel_size"]
485
+ self.head_dim = config.linear_attn_config["head_dim"]
486
+ self.num_heads = config.linear_attn_config["num_heads"]
487
+ self.head_k_dim = self.head_dim
488
+ self.num_k_heads = self.num_heads
489
+
490
+ self.layer_idx = layer_idx
491
+
492
+ assert self.mode in [
493
+ 'chunk', 'fused_recurrent'], f"Not supported mode `{self.mode}`."
494
+
495
+ projection_k_size = self.head_k_dim * self.num_k_heads
496
+ projection_size = self.head_dim * self.num_heads
497
+
498
+ self.q_proj = nn.Linear(
499
+ self.hidden_size, projection_k_size, bias=False)
500
+ self.k_proj = nn.Linear(
501
+ self.hidden_size, projection_k_size, bias=False)
502
+ self.v_proj = nn.Linear(self.hidden_size, projection_size, bias=False)
503
+
504
+ self.q_conv1d = ShortConvolution(
505
+ hidden_size=projection_k_size,
506
+ kernel_size=self.conv_size,
507
+ activation='silu',
508
+ )
509
+ self.k_conv1d = ShortConvolution(
510
+ hidden_size=projection_k_size,
511
+ kernel_size=self.conv_size,
512
+ activation='silu',
513
+ )
514
+ self.v_conv1d = ShortConvolution(
515
+ hidden_size=projection_size,
516
+ kernel_size=self.conv_size,
517
+ activation='silu',
518
+ )
519
+
520
+ self.A_log = torch.nn.Parameter(torch.log(torch.empty(
521
+ self.num_heads, dtype=torch.float32).uniform_(1, 16)))
522
+
523
+ self.f_a_proj = nn.Linear(self.hidden_size, self.head_dim, bias=False)
524
+ self.f_b_proj = nn.Linear(self.head_dim, projection_size, bias=False)
525
+
526
+ self.dt_bias = nn.Parameter(
527
+ torch.empty(projection_size, dtype=torch.float32))
528
+
529
+ self.b_proj = nn.Linear(self.hidden_size, self.num_heads, bias=False)
530
+
531
+ self.use_full_rank_gate = config.linear_attn_config.get("use_full_rank_gate", False)
532
+ self.gate_lower_bound = config.linear_attn_config.get("gate_lower_bound", None)
533
+ if self.use_full_rank_gate:
534
+ self.g_proj = nn.Linear(self.hidden_size, projection_size, bias=False)
535
+ else:
536
+ self.g_a_proj = nn.Linear(self.hidden_size, self.head_dim, bias=False)
537
+ self.g_b_proj = nn.Linear(self.head_dim, projection_size, bias=False)
538
+
539
+ self.o_norm = FusedRMSNormGated(
540
+ self.head_dim, eps=config.rms_norm_eps, activation='sigmoid')
541
+ self.o_proj = nn.Linear(projection_size, self.hidden_size, bias=False)
542
+
543
+ def forward(
544
+ self,
545
+ hidden_states: torch.Tensor,
546
+ attention_mask: torch.Tensor | None = None,
547
+ cache_params: KimiDynamicCache | None = None,
548
+ **kwargs: Unpack[dict],
549
+ ) -> tuple[torch.Tensor, torch.Tensor | None, Cache | None]:
550
+ if attention_mask is not None:
551
+ if attention_mask.dim() != 2:
552
+ attention_mask = kwargs.get("padding_mask")
553
+
554
+ if attention_mask is not None and attention_mask.dim() != 2:
555
+ raise ValueError(
556
+ "attention_mask must be a 0-1 matrix of shape [batch_size, seq_len] "
557
+ "(0 = padding). 3D masks are not supported here.",
558
+ )
559
+ use_cache = cache_params is not None
560
+ batch_size, q_len, _ = hidden_states.shape
561
+ mode = 'fused_recurrent' if use_cache and q_len == 1 else self.mode
562
+ if self.training:
563
+ assert mode == 'chunk', "Only chunk mode is supported in training."
564
+
565
+ cu_seqlens = kwargs.get('cu_seqlens')
566
+ indices = None
567
+ if attention_mask is not None:
568
+ indices, cu_seqlens, _ = get_unpad_data(attention_mask[:, -q_len:])
569
+ hidden_states = index_first_axis(
570
+ rearrange(hidden_states, "b s ... -> (b s) ..."), indices).unsqueeze(0)
571
+
572
+ conv_state_q, conv_state_k, conv_state_v = None, None, None
573
+ recurrent_state = None
574
+ if cache_params is not None:
575
+ if cache_params.conv_states[self.layer_idx] is not None:
576
+ conv_state_q, conv_state_k, conv_state_v = cache_params.conv_states[
577
+ self.layer_idx]
578
+ recurrent_state = cache_params.recurrent_states[self.layer_idx]
579
+
580
+ q_proj_states = self.q_proj(hidden_states)
581
+ k_proj_states = self.k_proj(hidden_states)
582
+ v_proj_states = self.v_proj(hidden_states)
583
+ q, conv_state_q = self.q_conv1d(
584
+ x=q_proj_states,
585
+ cache=conv_state_q,
586
+ output_final_state=use_cache,
587
+ cu_seqlens=cu_seqlens,
588
+ )
589
+ k, conv_state_k = self.k_conv1d(
590
+ x=k_proj_states,
591
+ cache=conv_state_k,
592
+ output_final_state=use_cache,
593
+ cu_seqlens=cu_seqlens,
594
+ )
595
+ v, conv_state_v = self.v_conv1d(
596
+ x=v_proj_states,
597
+ cache=conv_state_v,
598
+ output_final_state=use_cache,
599
+ cu_seqlens=cu_seqlens,
600
+ )
601
+ g = self.f_b_proj(self.f_a_proj(hidden_states))
602
+ g = rearrange(g, '... (h d) -> ... h d', d=self.head_dim)
603
+ beta = self.b_proj(hidden_states).float()
604
+
605
+ q, k = map(lambda x: rearrange(
606
+ x, '... (h d) -> ... h d', d=self.head_k_dim), (q, k))
607
+ v = rearrange(v, '... (h d) -> ... h d', d=self.head_dim)
608
+
609
+ if mode == 'chunk':
610
+ o, recurrent_state = chunk_kda(
611
+ q=q,
612
+ k=k,
613
+ v=v,
614
+ g=g,
615
+ beta=beta,
616
+ A_log=self.A_log,
617
+ dt_bias=self.dt_bias,
618
+ initial_state=recurrent_state,
619
+ output_final_state=True,
620
+ use_qk_l2norm_in_kernel=True,
621
+ use_gate_in_kernel=True,
622
+ use_beta_sigmoid_in_kernel=True,
623
+ safe_gate=self.gate_lower_bound is not None,
624
+ lower_bound=self.gate_lower_bound,
625
+ transpose_state_layout=True,
626
+ cu_seqlens=cu_seqlens,
627
+ )
628
+ else:
629
+ o, recurrent_state = fused_recurrent_kda(
630
+ q=q,
631
+ k=k,
632
+ v=v,
633
+ g=g,
634
+ beta=beta,
635
+ A_log=self.A_log,
636
+ dt_bias=self.dt_bias,
637
+ initial_state=recurrent_state,
638
+ output_final_state=True,
639
+ use_qk_l2norm_in_kernel=True,
640
+ use_gate_in_kernel=True,
641
+ use_beta_sigmoid_in_kernel=True,
642
+ lower_bound=self.gate_lower_bound,
643
+ transpose_state_layout=True,
644
+ cu_seqlens=cu_seqlens,
645
+ )
646
+ if cache_params is not None:
647
+ cache_params.recurrent_states[self.layer_idx] = recurrent_state
648
+ cache_params.conv_states[self.layer_idx] = (
649
+ conv_state_q, conv_state_k, conv_state_v)
650
+
651
+ if self.use_full_rank_gate:
652
+ g = self.g_proj(hidden_states)
653
+ else:
654
+ g = self.g_b_proj(self.g_a_proj(hidden_states))
655
+ g = rearrange(g, '... (h d) -> ... h d', d=self.head_dim)
656
+ o = self.o_norm(o, g)
657
+
658
+ o = rearrange(o, 'b t h d -> b t (h d)')
659
+ o = self.o_proj(o)
660
+ if attention_mask is not None:
661
+ o = pad_input(o.squeeze(0), indices, batch_size, q_len)
662
+
663
+ return o
664
+
665
+
666
+ class KimiMoEGate(nn.Module):
667
+ """
668
+ MoEGate adapted from Deepseek-V3.
669
+ Parameter correspondences:
670
+ num_experts -> n_routed_experts
671
+ num_experts_per_token -> num_experts_per_tok
672
+ num_expert_group -> n_group
673
+ moe_router_activation_func -> scoring_func
674
+ """
675
+
676
+ def __init__(self, config: KimiLinearConfig):
677
+ super().__init__()
678
+ self.config = config
679
+ self.top_k = config.num_experts_per_token
680
+ self.num_experts = config.num_experts
681
+ self.routed_scaling_factor = config.routed_scaling_factor
682
+ self.moe_router_activation_func = config.moe_router_activation_func
683
+ self.num_expert_group = getattr(config, "num_expert_group", 1)
684
+ self.topk_group = getattr(config, "topk_group", 1)
685
+
686
+ # topk selection algorithm
687
+ self.moe_renormalize = config.moe_renormalize
688
+ self.gating_dim = config.hidden_size
689
+ self.weight = nn.Parameter(
690
+ torch.empty((self.num_experts, self.gating_dim)),
691
+ )
692
+
693
+ self.e_score_correction_bias = nn.Parameter(
694
+ torch.empty(self.num_experts),
695
+ )
696
+ self.reset_parameters()
697
+
698
+ def reset_parameters(self) -> None:
699
+ import torch.nn.init as init
700
+
701
+ init.kaiming_uniform_(self.weight, a=math.sqrt(5))
702
+
703
+ def forward(self, hidden_states):
704
+ bsz, seq_len, h = hidden_states.shape
705
+ # compute gating score
706
+ hidden_states = hidden_states.view(-1, h)
707
+ logits = F.linear(
708
+ hidden_states.type(torch.float32), self.weight.type(
709
+ torch.float32), None,
710
+ )
711
+ if self.moe_router_activation_func == "sigmoid":
712
+ scores = logits.sigmoid()
713
+ elif self.moe_router_activation_func == "softmax":
714
+ scores = logits.softmax(dim=1)
715
+ else:
716
+ raise NotImplementedError(
717
+ f"insupportable scoring function for MoE gating: {self.moe_router_activation_func}",
718
+ )
719
+
720
+ # select top-k experts
721
+ assert not self.training
722
+ scores = scores.view(bsz * seq_len, -1)
723
+ scores_for_choice = scores + self.e_score_correction_bias.unsqueeze(0)
724
+ if self.num_expert_group > 1 and self.num_expert_group > self.topk_group:
725
+ group_scores = (
726
+ scores_for_choice.view(
727
+ bsz * seq_len, self.num_expert_group, -1).topk(2, dim=-1)[0].sum(dim=-1)
728
+ ) # [n, num_expert_group]
729
+ group_idx = torch.topk(
730
+ group_scores, k=self.topk_group, dim=-1, sorted=False,
731
+ )[
732
+ 1
733
+ ] # [n, top_k_group]
734
+ group_mask = torch.zeros_like(group_scores) # [n, num_expert_group]
735
+ group_mask.scatter_(1, group_idx, 1) # [n, num_expert_group]
736
+ score_mask = (
737
+ group_mask.unsqueeze(-1)
738
+ .expand(
739
+ bsz * seq_len, self.num_expert_group, self.num_experts // self.num_expert_group,
740
+ )
741
+ .reshape(bsz * seq_len, -1)
742
+ ) # [n, e]
743
+ tmp_scores = scores_for_choice.masked_fill(
744
+ ~score_mask.bool(), float("-inf")) # [n, e]
745
+ else:
746
+ tmp_scores = scores_for_choice
747
+ _, topk_idx = torch.topk(
748
+ tmp_scores, k=self.top_k, dim=-1, sorted=False,
749
+ )
750
+ topk_weight = scores.gather(1, topk_idx)
751
+
752
+ # norm gate to sum 1
753
+ if self.top_k > 1 and self.moe_renormalize:
754
+ denominator = topk_weight.sum(dim=-1, keepdim=True) + 1e-20
755
+ topk_weight = topk_weight / denominator
756
+ # must multiply the scaling factor
757
+ topk_weight = topk_weight * self.routed_scaling_factor
758
+
759
+ return topk_idx, topk_weight
760
+
761
+
762
+ class KimiSparseMoeBlock(nn.Module):
763
+ """
764
+ Adapted from Deepseek-V3's MOE implementation
765
+ The namings are consistent with Kimi's version.
766
+ """
767
+
768
+ def __init__(self, config: KimiLinearConfig):
769
+ super().__init__()
770
+ self.config = config
771
+ self.hidden_dim = config.hidden_size
772
+ self.num_experts = config.num_experts
773
+ self.top_k = config.num_experts_per_token
774
+ self.moe_renormalize = config.moe_renormalize
775
+
776
+ self.use_latent_moe = getattr(config, "routed_expert_hidden_size", None) is not None
777
+ self.moe_hidden_size = (
778
+ config.routed_expert_hidden_size
779
+ if self.use_latent_moe else config.hidden_size
780
+ )
781
+ self.latent_moe_use_norm = getattr(config, "latent_moe_use_norm", False)
782
+
783
+ self.ep_size = 1
784
+ self.experts_per_rank = config.num_experts
785
+ self.ep_rank = 0
786
+ self.experts = nn.ModuleList(
787
+ [
788
+ KimiBlockSparseMLP(
789
+ config,
790
+ hidden_size=self.moe_hidden_size,
791
+ intermediate_size=config.moe_intermediate_size,
792
+ )
793
+ for _ in range(config.num_experts)
794
+ ],
795
+ )
796
+ self.gate = KimiMoEGate(config)
797
+ if config.num_shared_experts is not None:
798
+ intermediate_size = config.moe_intermediate_size * config.num_shared_experts
799
+ self.shared_experts = KimiMLP(
800
+ config=config, intermediate_size=intermediate_size,
801
+ )
802
+
803
+ if self.use_latent_moe:
804
+ self.routed_expert_down_proj = nn.Linear(
805
+ config.hidden_size, self.moe_hidden_size, bias=False,
806
+ )
807
+ self.routed_expert_up_proj = nn.Linear(
808
+ self.moe_hidden_size, config.hidden_size, bias=False,
809
+ )
810
+ if self.latent_moe_use_norm:
811
+ self.routed_expert_norm = KimiRMSNorm(
812
+ self.moe_hidden_size, eps=config.rms_norm_eps,
813
+ )
814
+
815
+ def forward(self, hidden_states):
816
+ identity = hidden_states
817
+ orig_shape = hidden_states.shape
818
+ topk_idx, topk_weight = self.gate(hidden_states)
819
+ hidden_states = hidden_states.view(-1, hidden_states.shape[-1])
820
+
821
+ if self.use_latent_moe:
822
+ hidden_states = self.routed_expert_down_proj(hidden_states)
823
+
824
+ if not self.training:
825
+ y = self.moe_infer(hidden_states, topk_idx, topk_weight)
826
+ else:
827
+ raise NotImplementedError("Training mode is not supported in KimiSparseMoeBlock")
828
+
829
+ if self.use_latent_moe:
830
+ if self.latent_moe_use_norm:
831
+ y = self.routed_expert_norm(y)
832
+ y = self.routed_expert_up_proj(y)
833
+
834
+ y = y.view(*orig_shape)
835
+
836
+ if self.config.num_shared_experts is not None:
837
+ y = y + self.shared_experts(identity)
838
+ return y
839
+
840
+ @torch.no_grad()
841
+ def moe_infer(self, x, topk_ids, topk_weight):
842
+ cnts = topk_ids.new_zeros((topk_ids.shape[0], len(self.experts)))
843
+ cnts.scatter_(1, topk_ids, 1)
844
+ tokens_per_expert = cnts.sum(dim=0)
845
+ idxs = topk_ids.view(-1).argsort()
846
+ sorted_tokens = x[idxs // topk_ids.shape[1]]
847
+
848
+ tokens_per_expert = tokens_per_expert.cpu().numpy()
849
+
850
+ outputs = []
851
+ start_idx = 0
852
+ for i, num_tokens in enumerate(tokens_per_expert):
853
+ end_idx = start_idx + num_tokens
854
+ if num_tokens == 0:
855
+ continue
856
+ expert = self.experts[i + self.ep_rank * self.experts_per_rank]
857
+ tokens_for_this_expert = sorted_tokens[start_idx:end_idx]
858
+ expert_out = expert(tokens_for_this_expert)
859
+ outputs.append(expert_out)
860
+ start_idx = end_idx
861
+
862
+ outs = torch.cat(outputs, dim=0) if len(
863
+ outputs) else sorted_tokens.new_empty(0)
864
+
865
+ new_x = torch.empty_like(outs)
866
+ new_x[idxs] = outs
867
+ final_out = (
868
+ new_x.view(*topk_ids.shape, -1)
869
+ .type(topk_weight.dtype)
870
+ .mul_(topk_weight.unsqueeze(dim=-1))
871
+ .sum(dim=1)
872
+ .type(new_x.dtype)
873
+ )
874
+ return final_out
875
+
876
+
877
+ class KimiDecoderLayer(nn.Module):
878
+ def __init__(self, config: KimiLinearConfig, layer_idx: int):
879
+ super().__init__()
880
+ self.hidden_size = config.hidden_size
881
+ self.config = config
882
+ self.layer_idx = layer_idx
883
+ if config.is_kda_layer(layer_idx):
884
+ self.is_linear_attn = True
885
+ self.self_attn = KimiDeltaAttention(
886
+ config=config, layer_idx=layer_idx)
887
+ elif config.is_mla:
888
+ self.is_linear_attn = False
889
+ self.self_attn = KimiMLAAttention(
890
+ config=config, layer_idx=layer_idx)
891
+ else:
892
+ raise NotImplementedError
893
+ if (
894
+ config.num_experts is not None
895
+ and layer_idx >= config.first_k_dense_replace
896
+ and layer_idx % getattr(config, "moe_layer_freq", 1) == 0
897
+ ):
898
+ self.block_sparse_moe = KimiSparseMoeBlock(config)
899
+ else:
900
+ self.mlp = KimiMLP(config)
901
+ self.input_layernorm = KimiRMSNorm(
902
+ config.hidden_size, eps=config.rms_norm_eps)
903
+ self.post_attention_layernorm = KimiRMSNorm(
904
+ config.hidden_size, eps=config.rms_norm_eps)
905
+
906
+ # Attention residual
907
+ self.use_attn_residuals = getattr(config, "attn_res_block_size", None) is not None
908
+ if self.use_attn_residuals:
909
+ self.attn_res_block_size = config.attn_res_block_size
910
+ self.self_attention_res_norm = KimiRMSNorm(
911
+ config.hidden_size, eps=config.rms_norm_eps)
912
+ self.mlp_res_norm = KimiRMSNorm(
913
+ config.hidden_size, eps=config.rms_norm_eps)
914
+ self.self_attention_res_proj = nn.Linear(
915
+ config.hidden_size, 1, bias=False)
916
+ self.mlp_res_proj = nn.Linear(
917
+ config.hidden_size, 1, bias=False)
918
+
919
+ def forward(
920
+ self,
921
+ hidden_states: torch.Tensor,
922
+ attention_mask: torch.Tensor | None = None,
923
+ position_ids: torch.LongTensor | None = None,
924
+ past_key_values: tuple[torch.Tensor] | None = None,
925
+ output_attentions: bool | None = False,
926
+ use_cache: bool | None = False,
927
+ block_residual: torch.Tensor | None = None,
928
+ **kwargs: Unpack[FlashAttentionKwargs],
929
+ ):
930
+ if self.use_attn_residuals:
931
+ return self._forward_attn_residual(
932
+ hidden_states, attention_mask, position_ids,
933
+ past_key_values, output_attentions, use_cache,
934
+ block_residual, **kwargs)
935
+
936
+ residual = hidden_states
937
+
938
+ hidden_states = self.input_layernorm(hidden_states)
939
+
940
+ # Self Attention
941
+ if self.is_linear_attn is False:
942
+ hidden_states = self.self_attn(
943
+ hidden_states=hidden_states,
944
+ attention_mask=attention_mask,
945
+ position_ids=position_ids,
946
+ past_key_values=past_key_values,
947
+ output_attentions=output_attentions,
948
+ use_cache=use_cache,
949
+ **kwargs,
950
+ )
951
+ else:
952
+ hidden_states = self.self_attn(
953
+ hidden_states=hidden_states,
954
+ attention_mask=attention_mask,
955
+ cache_params=past_key_values,
956
+ output_attentions=output_attentions,
957
+ use_cache=use_cache,
958
+ **kwargs,
959
+ )
960
+ hidden_states = residual + hidden_states
961
+
962
+ # Fully Connected
963
+ residual = hidden_states
964
+ hidden_states = self.post_attention_layernorm(hidden_states)
965
+ if hasattr(self, "block_sparse_moe"):
966
+ hidden_states = self.block_sparse_moe(hidden_states)
967
+ else:
968
+ hidden_states = self.mlp(hidden_states)
969
+ hidden_states = residual + hidden_states
970
+
971
+ return hidden_states
972
+
973
+ def _forward_attn_residual(
974
+ self,
975
+ hidden_states: torch.Tensor,
976
+ attention_mask: torch.Tensor | None = None,
977
+ position_ids: torch.LongTensor | None = None,
978
+ past_key_values: tuple[torch.Tensor] | None = None,
979
+ output_attentions: bool | None = False,
980
+ use_cache: bool | None = False,
981
+ block_residual: torch.Tensor | None = None,
982
+ **kwargs: Unpack[FlashAttentionKwargs],
983
+ ):
984
+ batch_size, seq_len, hidden_size = hidden_states.shape
985
+ prefix_sum = hidden_states
986
+
987
+ if block_residual is not None and block_residual.shape[1] > 0:
988
+ hidden_states = _apply_attn_res(
989
+ prefix_sum.view(-1, hidden_size),
990
+ block_residual,
991
+ self.self_attention_res_proj,
992
+ self.self_attention_res_norm,
993
+ ).view(batch_size, seq_len, hidden_size)
994
+
995
+ if self.layer_idx % self.attn_res_block_size == 0:
996
+ block_residual = torch.cat(
997
+ [block_residual, prefix_sum.view(-1, hidden_size).unsqueeze(1)], dim=1)
998
+ prefix_sum = None
999
+
1000
+ hidden_states = self.input_layernorm(hidden_states)
1001
+
1002
+ # Self Attention
1003
+ if self.is_linear_attn is False:
1004
+ hidden_states = self.self_attn(
1005
+ hidden_states=hidden_states,
1006
+ attention_mask=attention_mask,
1007
+ position_ids=position_ids,
1008
+ past_key_values=past_key_values,
1009
+ output_attentions=output_attentions,
1010
+ use_cache=use_cache,
1011
+ **kwargs,
1012
+ )
1013
+ else:
1014
+ hidden_states = self.self_attn(
1015
+ hidden_states=hidden_states,
1016
+ attention_mask=attention_mask,
1017
+ cache_params=past_key_values,
1018
+ output_attentions=output_attentions,
1019
+ use_cache=use_cache,
1020
+ **kwargs,
1021
+ )
1022
+
1023
+ if prefix_sum is not None:
1024
+ prefix_sum = prefix_sum + hidden_states
1025
+ else:
1026
+ prefix_sum = hidden_states
1027
+
1028
+ hidden_states = _apply_attn_res(
1029
+ prefix_sum.view(-1, hidden_size),
1030
+ block_residual,
1031
+ self.mlp_res_proj,
1032
+ self.mlp_res_norm,
1033
+ ).view(batch_size, seq_len, hidden_size)
1034
+
1035
+ hidden_states = self.post_attention_layernorm(hidden_states)
1036
+ if hasattr(self, "block_sparse_moe"):
1037
+ hidden_states = self.block_sparse_moe(hidden_states)
1038
+ else:
1039
+ hidden_states = self.mlp(hidden_states)
1040
+
1041
+ if prefix_sum is None:
1042
+ prefix_sum = hidden_states
1043
+ else:
1044
+ prefix_sum = prefix_sum + hidden_states
1045
+
1046
+ return prefix_sum, block_residual
1047
+
1048
+
1049
+ class KimiPreTrainedModel(PreTrainedModel):
1050
+ config_class = KimiLinearConfig
1051
+ base_model_prefix = "model"
1052
+ supports_gradient_checkpointing = True
1053
+ _no_split_modules = ["KimiDecoderLayer"]
1054
+ _skip_keys_device_placement = "past_key_values"
1055
+ _supports_flash_attn_2 = True
1056
+ _can_record_outputs = {
1057
+ "router_logits": OutputRecorder(KimiBlockSparseMLP, index=1),
1058
+ "hidden_states": KimiDecoderLayer,
1059
+ "attentions": KimiMLAAttention,
1060
+ }
1061
+ _is_stateful = True
1062
+
1063
+ def _init_weights(self, module):
1064
+ std = self.config.initializer_range
1065
+ if isinstance(module, nn.Linear):
1066
+ module.weight.data.normal_(mean=0.0, std=std)
1067
+ if module.bias is not None:
1068
+ module.bias.data.zero_()
1069
+ elif isinstance(module, nn.Embedding):
1070
+ module.weight.data.normal_(mean=0.0, std=std)
1071
+ if module.padding_idx is not None:
1072
+ module.weight.data[module.padding_idx].zero_()
1073
+
1074
+
1075
+ def _apply_attn_res(prefix_sum, block_residual, proj, norm):
1076
+ """
1077
+ prefix_sum: (num_tokens, hidden_size)
1078
+ block_residual: (num_tokens, num_blocks, hidden_size)
1079
+ """
1080
+ v = torch.cat((block_residual, prefix_sum.unsqueeze(1)), dim=1)
1081
+ v_float = v.float()
1082
+ variance = v_float.pow(2).mean(-1, keepdim=True)
1083
+ k = v_float * torch.rsqrt(variance + norm.variance_epsilon)
1084
+ score_weight = norm.weight.float() * proj.weight.squeeze(0).float()
1085
+ scores = (k * score_weight).sum(-1)
1086
+ probs = scores.softmax(-1).unsqueeze(1)
1087
+ hidden_states = torch.matmul(probs, v_float).squeeze(1)
1088
+ return hidden_states.to(v.dtype)
1089
+
1090
+ class KimiLinearModel(KimiPreTrainedModel):
1091
+ def __init__(self, config: KimiLinearConfig):
1092
+ super().__init__(config)
1093
+ self.padding_idx = config.pad_token_id
1094
+ self.vocab_size = config.vocab_size
1095
+
1096
+ self.embed_tokens = nn.Embedding(
1097
+ config.vocab_size, config.hidden_size, self.padding_idx)
1098
+ self.layers = nn.ModuleList([KimiDecoderLayer(
1099
+ config, layer_idx) for layer_idx in range(config.num_hidden_layers)])
1100
+ self.norm = KimiRMSNorm(
1101
+ config.hidden_size, eps=config.rms_norm_eps)
1102
+
1103
+ self.use_attn_residuals = getattr(config, "attn_res_block_size", None) is not None
1104
+ if self.use_attn_residuals:
1105
+ self.output_attn_res_norm = KimiRMSNorm(
1106
+ config.hidden_size, eps=config.rms_norm_eps)
1107
+ self.output_attn_res_proj = nn.Linear(
1108
+ config.hidden_size, 1, bias=False)
1109
+
1110
+ if getattr(config, "_attn_implementation", None) is not None:
1111
+ if config._attn_implementation != "flash_attention_2":
1112
+ logger.warning_once(
1113
+ f"Ignoring the provided attention implementation {config._attn_implementation}")
1114
+ logger.warning_once("Using flash_attention_2 backend instead.")
1115
+ config._attn_implementation = "flash_attention_2"
1116
+ else:
1117
+ config._attn_implementation = "flash_attention_2"
1118
+
1119
+ self._use_flash_attention_2 = config._attn_implementation == "flash_attention_2"
1120
+ self.gradient_checkpointing = False
1121
+ # Initialize weights and apply final processing
1122
+ self.post_init()
1123
+
1124
+ def _update_linear_attn_mask(self, attention_mask, cache_position):
1125
+ """
1126
+ NOTE: Left-padding is used for linear attention mask.
1127
+ No need for zeroing states when
1128
+ 1. Cached forward
1129
+ 2. Attending to all inputs
1130
+ """
1131
+ linear_attn_mask = attention_mask
1132
+ if cache_position[0] > 0 or (attention_mask is not None and torch.all(attention_mask == 1)):
1133
+ linear_attn_mask = None
1134
+ return linear_attn_mask
1135
+
1136
+ @check_model_inputs
1137
+ # @auto_docstring
1138
+ def forward(
1139
+ self,
1140
+ input_ids: torch.LongTensor = None,
1141
+ attention_mask: torch.Tensor | None = None,
1142
+ position_ids: torch.LongTensor | None = None,
1143
+ past_key_values: Cache | None = None,
1144
+ inputs_embeds: torch.FloatTensor | None = None,
1145
+ cache_position: torch.LongTensor | None = None,
1146
+ use_cache: bool | None = None,
1147
+ **kwargs: Unpack[TransformersKwargs],
1148
+ ) -> tuple | BaseModelOutputWithPast:
1149
+
1150
+ use_cache = use_cache if use_cache is not None else self.config.use_cache
1151
+
1152
+ if (input_ids is None) and (inputs_embeds is None):
1153
+ raise ValueError(
1154
+ "You must specify exactly one of input_ids or inputs_embeds")
1155
+
1156
+ # Get inputs_embeds
1157
+ if inputs_embeds is None:
1158
+ inputs_embeds = self.embed_tokens(input_ids)
1159
+
1160
+ if use_cache and past_key_values is None:
1161
+ past_key_values = KimiDynamicCache(config=self.config)
1162
+
1163
+ if cache_position is None:
1164
+ past_seen_tokens = past_key_values.get_seq_length(
1165
+ ) if past_key_values is not None else 0
1166
+ cache_position: torch.Tensor = torch.arange(
1167
+ past_seen_tokens, past_seen_tokens + inputs_embeds.shape[1], device=inputs_embeds.device,
1168
+ )
1169
+
1170
+ if position_ids is None:
1171
+ position_ids = cache_position.unsqueeze(0)
1172
+
1173
+ causal_mask = create_causal_mask(
1174
+ config=self.config,
1175
+ input_embeds=inputs_embeds,
1176
+ attention_mask=attention_mask,
1177
+ cache_position=cache_position,
1178
+ past_key_values=past_key_values,
1179
+ position_ids=position_ids,
1180
+ )
1181
+ linear_attn_mask = self._update_linear_attn_mask(
1182
+ attention_mask, cache_position)
1183
+
1184
+ hidden_states = inputs_embeds
1185
+ if past_key_values is not None:
1186
+ assert isinstance(past_key_values, KimiDynamicCache)
1187
+
1188
+ block_residual = None
1189
+ if self.use_attn_residuals:
1190
+ block_residual = hidden_states.new_zeros(
1191
+ hidden_states.shape[0] * hidden_states.shape[1], 0,
1192
+ hidden_states.shape[2])
1193
+
1194
+ for decoder_layer in self.layers:
1195
+ layer_mask = linear_attn_mask if decoder_layer.is_linear_attn else causal_mask
1196
+
1197
+ if self.use_attn_residuals:
1198
+ hidden_states, block_residual = decoder_layer(
1199
+ hidden_states,
1200
+ attention_mask=layer_mask,
1201
+ past_key_values=past_key_values,
1202
+ cache_position=cache_position,
1203
+ block_residual=block_residual,
1204
+ **kwargs,
1205
+ )
1206
+ else:
1207
+ hidden_states = decoder_layer(
1208
+ hidden_states,
1209
+ attention_mask=layer_mask,
1210
+ past_key_values=past_key_values,
1211
+ cache_position=cache_position,
1212
+ **kwargs,
1213
+ )
1214
+
1215
+ if self.use_attn_residuals:
1216
+ hidden_states = self._apply_output_attn_res(
1217
+ hidden_states, block_residual)
1218
+
1219
+ hidden_states = self.norm(hidden_states)
1220
+
1221
+ return BaseModelOutputWithPast(
1222
+ last_hidden_state=hidden_states,
1223
+ past_key_values=past_key_values,
1224
+ )
1225
+
1226
+ def _apply_output_attn_res(self, hidden_states, block_residual):
1227
+ batch_size, seq_len, hidden_size = hidden_states.shape
1228
+ return _apply_attn_res(
1229
+ hidden_states.view(-1, hidden_size),
1230
+ block_residual,
1231
+ self.output_attn_res_proj,
1232
+ self.output_attn_res_norm,
1233
+ ).view(batch_size, seq_len, hidden_size)
1234
+
1235
+
1236
+ class KimiLinearForCausalLM(KimiPreTrainedModel, GenerationMixin):
1237
+ @classmethod
1238
+ def _supports_default_dynamic_cache(cls) -> bool:
1239
+ return False
1240
+
1241
+ _tied_weights_keys = ["lm_head.weight"]
1242
+
1243
+ def __init__(self, config):
1244
+ super().__init__(config)
1245
+ self.model = KimiLinearModel(config)
1246
+ self.vocab_size = config.vocab_size
1247
+ self.lm_head = nn.Linear(
1248
+ config.hidden_size, config.vocab_size, bias=False)
1249
+
1250
+ # Initialize weights and apply final processing
1251
+ self.post_init()
1252
+
1253
+ @can_return_tuple
1254
+ # @auto_docstring
1255
+ def forward(
1256
+ self,
1257
+ input_ids: torch.LongTensor = None,
1258
+ attention_mask: torch.Tensor | None = None,
1259
+ position_ids: torch.LongTensor | None = None,
1260
+ past_key_values: list[torch.FloatTensor] | None = None,
1261
+ inputs_embeds: torch.FloatTensor | None = None,
1262
+ labels: torch.LongTensor | None = None,
1263
+ use_cache: bool | None = None,
1264
+ output_attentions: bool | None = None,
1265
+ output_hidden_states: bool | None = None,
1266
+ generation_mode: bool | None = None,
1267
+ return_dict: bool | None = None,
1268
+ cache_position: torch.LongTensor | None = None,
1269
+ **kwargs: Unpack[TransformersKwargs],
1270
+ ) -> tuple | CausalLMOutputWithPast:
1271
+ r"""
1272
+ Args:
1273
+ labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*):
1274
+ Labels for computing the masked language modeling loss. Indices should either be in `[0, ...,
1275
+ config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored
1276
+ (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`.
1277
+ """
1278
+
1279
+ output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
1280
+ output_hidden_states = (
1281
+ output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
1282
+ )
1283
+ return_dict = return_dict if return_dict is not None else self.config.use_return_dict
1284
+
1285
+ outputs = self.model(
1286
+ input_ids=input_ids,
1287
+ attention_mask=attention_mask,
1288
+ position_ids=position_ids,
1289
+ past_key_values=past_key_values,
1290
+ inputs_embeds=inputs_embeds,
1291
+ use_cache=use_cache,
1292
+ output_attentions=output_attentions,
1293
+ output_hidden_states=output_hidden_states,
1294
+ return_dict=return_dict,
1295
+ cache_position=cache_position,
1296
+ )
1297
+
1298
+ logits = outputs[0]
1299
+ if generation_mode:
1300
+ logits = logits[:, -1:]
1301
+ logits = self.lm_head(logits)
1302
+
1303
+ loss = None
1304
+ if labels is not None:
1305
+ loss = self.loss_function(
1306
+ logits, labels, self.vocab_size, **kwargs)
1307
+
1308
+ return CausalLMOutputWithPast(
1309
+ loss=loss,
1310
+ logits=logits,
1311
+ past_key_values=outputs.past_key_values,
1312
+ hidden_states=outputs.hidden_states,
1313
+ attentions=outputs.attentions,
1314
+ )
nvfp4_conversion_manifest.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "mode": "requantized",
3
+ "source_repo": "moonshotai/Kimi-K3",
4
+ "source_revision": "9f62e4e9fffbd0a83ddd60e1c209d828994b3569",
5
+ "shard_provenance": {
6
+ "verified": true,
7
+ "source_revision": "9f62e4e9fffbd0a83ddd60e1c209d828994b3569",
8
+ "method": "pinned-staging-config-and-commit-window",
9
+ "staging_config_revision": "9f62e4e9fffbd0a83ddd60e1c209d828994b3569",
10
+ "staging_commit_time": "2026-07-27T19:56:45+00:00",
11
+ "verified_existing_shards": 93
12
+ },
13
+ "expected_shards": 96,
14
+ "present_shards": 96,
15
+ "weight_map_tensors": 744516,
16
+ "scale_tensors": 247296,
17
+ "tensor_bytes": 1645946964992,
18
+ "lossless_over_span_policy": "fail; never silently requantize",
19
+ "runtime_load_verified": false
20
+ }
preprocessor_config.json ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "media_proc_cfg": {
3
+ "in_patch_limit": 65536,
4
+ "patch_size": 14,
5
+ "image_mean": [
6
+ 0.5,
7
+ 0.5,
8
+ 0.5
9
+ ],
10
+ "image_std": [
11
+ 0.5,
12
+ 0.5,
13
+ 0.5
14
+ ],
15
+ "merge_kernel_size": 2,
16
+ "fixed_output_tokens": null,
17
+ "patch_limit_on_one_side": 512,
18
+ "in_patch_limit_each_frame": 16384,
19
+ "in_patch_limit_video": 655360,
20
+ "sample_fps": 8.0,
21
+ "max_num_frames_each_video": null,
22
+ "temporal_merge_kernel_size": 4,
23
+ "timestamp_mode": "hh:mm:ss.fff",
24
+ "transparent_bg_config": {
25
+ "pattern": "chessboard",
26
+ "chessboard_square_size": 8,
27
+ "chessboard_square_on_top_left": true,
28
+ "chessboard_white_value": 255,
29
+ "chessboard_gray_value": 180
30
+ },
31
+ "transparent_bg_fill_stage": "after_resize",
32
+ "config_type": "media_proc.processors.moonvit.MoonViTMediaProcessorConfig"
33
+ },
34
+ "auto_map": {
35
+ "AutoProcessor": "kimi_k3_processor.KimiK3Processor",
36
+ "AutoImageProcessor": "kimi_k3_vision_processing.KimiK3VisionProcessor"
37
+ }
38
+ }
tiktoken.model ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b6c497a7469b33ced9c38afb1ad6e47f03f5e5dc05f15930799210ec050c5103
3
+ size 2795286
tokenization_kimi.py ADDED
@@ -0,0 +1,408 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from logging import getLogger
3
+ from pathlib import Path
4
+ from shutil import copyfile
5
+ from typing import Dict, Iterator, List, Optional, Tuple, Union, cast
6
+
7
+ import tiktoken
8
+ from tiktoken.load import load_tiktoken_bpe
9
+ from tokenizers import AddedToken
10
+ from transformers.convert_slow_tokenizer import bytes_to_unicode
11
+ from transformers.tokenization_utils import PreTrainedTokenizer
12
+
13
+ try:
14
+ from .encoding_k3 import build_chat_segments, is_batched_conversation
15
+ except ImportError: # pragma: no cover - supports direct file execution/import.
16
+ from encoding_k3 import build_chat_segments, is_batched_conversation
17
+
18
+ logger = getLogger(__name__)
19
+ VOCAB_FILES_NAMES = {"vocab_file": "tiktoken.model"}
20
+
21
+
22
+ class TikTokenTokenizer(PreTrainedTokenizer):
23
+ """
24
+ Tokenizing and encoding/decoding text using the Tiktoken tokenizer. See megatron/tokenizer/tiktoken_tokenizer.py.
25
+
26
+ This tokenizer inherits from [`PreTrainedTokenizer`] which contains most of the main methods. Users should refer to
27
+ this superclass for more information regarding those methods.
28
+
29
+ Args:
30
+ vocab_file (`str`):
31
+ The path to the Tiktoken model file.
32
+ bos_token (`str` or `tokenizers.AddedToken`, *optional*, defaults to `"<|begin_of_text|>",`):
33
+ The beginning of sequence token that was used during pretraining. Can be used a sequence classifier token.
34
+ eos_token (`str` or `tokenizers.AddedToken`, *optional*, defaults to `"<|end_of_text|>"`):
35
+ The end of sequence token.
36
+ unk_token (`str` or `tokenizers.AddedToken`, *optional*, defaults to `"<|reserved_special_token_249|>"`):
37
+ The unknown token. A token that is not in the vocabulary cannot be converted to an ID and is set to be this
38
+ token instead. The second to last item in special_tokens.
39
+ pad_token (`str` or `tokenizers.AddedToken`, *optional*, defaults to `"<|reserved_special_token_250|>"`):
40
+ The token used for padding, for example when batching sequences of different lengths.
41
+ additional_special_tokens (list of `str`, *optional*):
42
+ A tuple or a list of additional tokens, which will be marked as `special`, meaning that they will be
43
+ skipped when decoding if `skip_special_tokens` is set to `True`.
44
+ """
45
+
46
+ vocab_files_names = VOCAB_FILES_NAMES
47
+
48
+ model_input_names = ["input_ids", "attention_mask"]
49
+
50
+ special_tokens: Dict[str, int]
51
+
52
+ num_reserved_special_tokens = 256
53
+
54
+ pat_str = "|".join([
55
+ r"""[\p{Han}]+""",
56
+ r"""[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]*[\p{Ll}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]+(?i:'s|'t|'re|'ve|'m|'ll|'d)?""",
57
+ r"""[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]+[\p{Ll}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]*(?i:'s|'t|'re|'ve|'m|'ll|'d)?""",
58
+ r"""\p{N}{1,3}""",
59
+ r""" ?[^\s\p{L}\p{N}]+[\r\n]*""",
60
+ r"""\s*[\r\n]+""",
61
+ r"""\s+(?!\S)""",
62
+ r"""\s+""",
63
+ ])
64
+
65
+ def __init__(
66
+ self,
67
+ vocab_file,
68
+ bos_token: Union[str, AddedToken] = "[BOS]",
69
+ eos_token: Union[str, AddedToken] = "[EOS]",
70
+ unk_token: Union[str, AddedToken, None] = None,
71
+ pad_token: Union[str, AddedToken, None] = None,
72
+ additional_special_tokens: List[str] = None,
73
+ added_tokens_decoder: Optional[dict] = None,
74
+ **kwargs,
75
+ ):
76
+ assert os.path.isfile(vocab_file), vocab_file
77
+
78
+ if additional_special_tokens is None:
79
+ additional_special_tokens = [
80
+ "<|im_end|>",
81
+ "<|im_user|>",
82
+ "<|im_assistant|>",
83
+ "<|start_header_id|>",
84
+ "<|end_header_id|>",
85
+ "[EOT]",
86
+ "<|im_system|>",
87
+ "<|im_middle|>",
88
+ ]
89
+
90
+ if added_tokens_decoder:
91
+ special_tokens_mapping = {
92
+ i: added_tokens_decoder[i].content
93
+ for i in added_tokens_decoder
94
+ }
95
+ else:
96
+ special_tokens_mapping = {}
97
+
98
+ self.vocab_file = vocab_file
99
+ mergeable_ranks = load_tiktoken_bpe(vocab_file)
100
+ num_base_tokens = len(mergeable_ranks)
101
+ self.special_tokens = {
102
+ special_tokens_mapping.get(i, f"<|reserved_token_{i}|>"): i
103
+ for i in range(num_base_tokens, num_base_tokens +
104
+ self.num_reserved_special_tokens)
105
+ }
106
+
107
+ self.model = tiktoken.Encoding(
108
+ name=Path(vocab_file).name,
109
+ pat_str=self.pat_str,
110
+ mergeable_ranks=mergeable_ranks,
111
+ special_tokens=self.special_tokens,
112
+ )
113
+ logger.info(f"Reloaded tiktoken model from {vocab_file}")
114
+
115
+ self.n_words: int = self.model.n_vocab
116
+ # BOS / EOS token IDs
117
+ self.bos_id: int = self.special_tokens[str(bos_token)]
118
+ self.eos_id: int = self.special_tokens[str(eos_token)]
119
+ logger.info(
120
+ f"#words: {self.n_words} - BOS ID: {self.bos_id} - EOS ID: {self.eos_id}"
121
+ )
122
+
123
+ self.pad_id: int = self.special_tokens[str(pad_token)]
124
+ self.unk_id: int = self.special_tokens[str(unk_token)]
125
+
126
+ self.byte_encoder = bytes_to_unicode()
127
+ self.byte_decoder = {v: k for k, v in self.byte_encoder.items()}
128
+
129
+ self.decoder = {}
130
+ for i in range(self.n_words):
131
+ # Taken from https://gist.github.com/xenova/a452a6474428de0182b17605a98631ee
132
+ decoding = ''.join([
133
+ self.byte_encoder[ord(char)] for char in
134
+ self.model.decode_single_token_bytes(i).decode('latin-1')
135
+ ])
136
+ self.decoder[i] = decoding
137
+
138
+ self.encoder = {}
139
+ for i in range(self.n_words):
140
+ if i in self.decoder:
141
+ self.encoder[self.decoder[i]] = i
142
+
143
+ super().__init__(
144
+ bos_token=bos_token,
145
+ eos_token=eos_token,
146
+ unk_token=unk_token,
147
+ pad_token=pad_token,
148
+ additional_special_tokens=additional_special_tokens,
149
+ added_tokens_decoder=added_tokens_decoder,
150
+ **kwargs,
151
+ )
152
+ self.all_special_ids_set = set(self.all_special_ids)
153
+
154
+ def _encode_text_piece(self, text: str,
155
+ allow_special_tokens: bool = True) -> List[int]:
156
+ # The tiktoken tokenizer can handle <=400k chars without
157
+ # pyo3_runtime.PanicException.
158
+ TIKTOKEN_MAX_ENCODE_CHARS = 400_000
159
+
160
+ # https://github.com/openai/tiktoken/issues/195
161
+ # Here we iterate over subsequences and split if we exceed the limit
162
+ # of max consecutive non-whitespace or whitespace characters.
163
+ MAX_NO_WHITESPACES_CHARS = 25_000
164
+
165
+ t: List[int] = []
166
+ for i in range(0, len(text), TIKTOKEN_MAX_ENCODE_CHARS):
167
+ for substr in self._split_whitespaces_or_nonwhitespaces(
168
+ text[i:i + TIKTOKEN_MAX_ENCODE_CHARS],
169
+ MAX_NO_WHITESPACES_CHARS,
170
+ ):
171
+ if allow_special_tokens:
172
+ t.extend(
173
+ # structural markers: encode <|...|> as their special token IDs
174
+ self.model.encode(
175
+ substr,
176
+ allowed_special="all",
177
+ ))
178
+ else:
179
+ t.extend(
180
+ # user/tool text: encode any <|...|> as ordinary BPE tokens (never as control tokens)
181
+ self.model.encode(
182
+ substr,
183
+ disallowed_special=(),
184
+ ))
185
+
186
+ return t
187
+
188
+ def encode(self,
189
+ text: str,
190
+ allow_special_tokens: bool = True,
191
+ **kwargs) -> List[int]:
192
+ """
193
+ Encodes a string into a list of token IDs.
194
+
195
+ Args:
196
+ text (str): The input string to be encoded.
197
+
198
+ Returns:
199
+ list[int]: A list of token IDs.
200
+ """
201
+ # If there are other args, we should call super().encode because there are a lot of code
202
+ # to handle those args. supper().encode finally will call _tokenize and _convert_token_to_id.
203
+ # NOTE: our encode method is not compatible with the super().encode method,
204
+ # e.g. split_special_tokens' default is True in our encode method.
205
+ if len(kwargs) > 0:
206
+ logger.warning(f"Calling super().encode with {kwargs}")
207
+ return super().encode(text, **kwargs)
208
+
209
+ assert type(text) is str
210
+ return self._encode_text_piece(text,
211
+ allow_special_tokens=allow_special_tokens)
212
+
213
+ def decode(self, token_ids: Union[int, List[int]], **kwargs) -> str:
214
+ """
215
+ Decodes a list of token IDs into a string.
216
+
217
+ Args:
218
+ token_ids (List[int]): The list of token IDs to be decoded.
219
+
220
+ Returns:
221
+ str: The decoded string.
222
+ """
223
+ # If there are other args, we should call super().decode because there are a lot of code
224
+ # to handle those args. supper().encode finally will call convert_tokens_to_string and _convert_id_to_token.
225
+ if len(kwargs) > 0:
226
+ return super().decode(token_ids, **kwargs)
227
+
228
+ if type(token_ids) is int:
229
+ token_ids = [token_ids]
230
+
231
+ return self.model.decode(cast(List[int], token_ids))
232
+
233
+ @staticmethod
234
+ def _split_whitespaces_or_nonwhitespaces(
235
+ s: str, max_consecutive_slice_len: int) -> Iterator[str]:
236
+ """
237
+ Splits the string `s` so that each substring contains no more than `max_consecutive_slice_len`
238
+ consecutive whitespaces or consecutive non-whitespaces.
239
+ """
240
+ current_slice_len = 0
241
+ current_slice_is_space = s[0].isspace() if len(s) > 0 else False
242
+ slice_start = 0
243
+
244
+ for i in range(len(s)):
245
+ is_now_space = s[i].isspace()
246
+
247
+ if current_slice_is_space ^ is_now_space:
248
+ current_slice_len = 1
249
+ current_slice_is_space = is_now_space
250
+ else:
251
+ current_slice_len += 1
252
+ if current_slice_len > max_consecutive_slice_len:
253
+ yield s[slice_start:i]
254
+ slice_start = i
255
+ current_slice_len = 1
256
+ yield s[slice_start:]
257
+
258
+ def _encode_chat_segments(self, segments) -> List[int]:
259
+ token_ids: List[int] = []
260
+ for segment in segments:
261
+ token_ids.extend(
262
+ self._encode_text_piece(
263
+ segment.text,
264
+ allow_special_tokens=segment.allow_special,
265
+ ))
266
+ return token_ids
267
+
268
+ @staticmethod
269
+ def _truncate(ids: List[int],
270
+ truncation: bool = False,
271
+ max_length: Optional[int] = None) -> List[int]:
272
+ if truncation and max_length is not None:
273
+ return ids[:max_length]
274
+ return ids
275
+
276
+ def _format_chat_token_output(self,
277
+ encoded_inputs: List[List[int]],
278
+ *,
279
+ is_batched: bool,
280
+ padding=False,
281
+ truncation: bool = False,
282
+ max_length: Optional[int] = None,
283
+ return_tensors=None,
284
+ return_dict: bool = False):
285
+ encoded_inputs = [
286
+ self._truncate(ids, truncation=truncation, max_length=max_length)
287
+ for ids in encoded_inputs
288
+ ]
289
+
290
+ needs_batch_encoding = (
291
+ is_batched or padding or return_tensors is not None or return_dict)
292
+ if not needs_batch_encoding:
293
+ return encoded_inputs[0]
294
+
295
+ features = [{
296
+ "input_ids": ids,
297
+ "attention_mask": [1] * len(ids)
298
+ } for ids in encoded_inputs]
299
+ batch = self.pad(features,
300
+ padding=padding,
301
+ max_length=max_length if padding else None,
302
+ return_attention_mask=True,
303
+ return_tensors=return_tensors)
304
+
305
+ if return_dict:
306
+ return batch
307
+ if is_batched:
308
+ return batch["input_ids"]
309
+ return batch["input_ids"][0] if return_tensors is None else batch[
310
+ "input_ids"]
311
+
312
+ """ ----- Below are the abstract methods required by PreTrainedTokenizer ----- """
313
+
314
+ @property
315
+ def vocab_size(self) -> int:
316
+ return self.n_words
317
+
318
+ def get_vocab(self) -> Dict[str, int]:
319
+ return self.encoder
320
+
321
+ def _tokenize(self, text: str, **kwargs) -> List[str]:
322
+ return [self.decoder[t] for t in self.encode(text)]
323
+
324
+ def _convert_token_to_id(self, token: str) -> int:
325
+ return self.encoder.get(token, self.unk_id)
326
+
327
+ def _convert_id_to_token(self, index: int) -> str:
328
+ return self.decoder.get(index)
329
+
330
+ @staticmethod
331
+ def clean_up_tokenization(out_string: str) -> str:
332
+ return out_string
333
+
334
+ def convert_tokens_to_string(self, tokens: List[str]) -> str:
335
+ text = ''.join(tokens)
336
+ text = bytearray([self.byte_decoder[c]
337
+ for c in text]).decode('utf-8', 'replace')
338
+ return text
339
+
340
+ def save_vocabulary(self,
341
+ save_directory: str,
342
+ filename_prefix: Optional[str] = None) -> Tuple[str]:
343
+ if not os.path.isdir(save_directory):
344
+ raise ValueError(
345
+ f"vocabulary path ({save_directory}) should be a directory")
346
+ out_vocab_file = os.path.join(
347
+ save_directory,
348
+ (filename_prefix + "-" if filename_prefix else "") +
349
+ VOCAB_FILES_NAMES["vocab_file"])
350
+
351
+ if os.path.abspath(self.vocab_file) != os.path.abspath(
352
+ out_vocab_file) and os.path.isfile(self.vocab_file):
353
+ copyfile(self.vocab_file, out_vocab_file)
354
+
355
+ return (out_vocab_file, )
356
+
357
+ def apply_chat_template(self,
358
+ conversation,
359
+ tools: Optional[list[dict]] = None,
360
+ tokenize: bool = False,
361
+ add_generation_prompt: bool = True,
362
+ thinking: bool = True,
363
+ padding=False,
364
+ truncation: bool = False,
365
+ max_length: Optional[int] = None,
366
+ return_tensors=None,
367
+ return_dict: bool = False,
368
+ **kwargs):
369
+ # Tokenizer-level rendering reorders tool result messages to match
370
+ # assistant tool_calls, normalizes per-call arguments and response
371
+ # schema, then encodes the resulting XTML structure segment-by-segment.
372
+ is_batched = is_batched_conversation(conversation)
373
+ conversations = conversation if is_batched else [conversation]
374
+ image_prompts = kwargs.pop("image_prompts", None)
375
+ if is_batched and image_prompts is not None:
376
+ raise ValueError("image_prompts is only supported for one chat.")
377
+
378
+ # by default set thinking effort to max
379
+ kwargs.setdefault("thinking_effort", "max")
380
+
381
+ segment_batches = [
382
+ build_chat_segments(
383
+ messages,
384
+ tools=tools,
385
+ add_generation_prompt=add_generation_prompt,
386
+ thinking=thinking,
387
+ image_prompts=image_prompts,
388
+ **kwargs,
389
+ ) for messages in conversations
390
+ ]
391
+
392
+ if not tokenize:
393
+ rendered = ["".join(segment.text for segment in segments)
394
+ for segments in segment_batches]
395
+ return rendered if is_batched else rendered[0]
396
+
397
+ encoded_inputs = [
398
+ self._encode_chat_segments(segments) for segments in segment_batches
399
+ ]
400
+ return self._format_chat_token_output(
401
+ encoded_inputs,
402
+ is_batched=is_batched,
403
+ padding=padding,
404
+ truncation=truncation,
405
+ max_length=max_length,
406
+ return_tensors=return_tensors,
407
+ return_dict=return_dict,
408
+ )
tokenizer_config.json ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "added_tokens_decoder": {
3
+ "163584": {
4
+ "content": "[BOS]",
5
+ "lstrip": false,
6
+ "normalized": false,
7
+ "rstrip": false,
8
+ "single_word": false,
9
+ "special": true
10
+ },
11
+ "163585": {
12
+ "content": "[EOS]",
13
+ "lstrip": false,
14
+ "normalized": false,
15
+ "rstrip": false,
16
+ "single_word": false,
17
+ "special": true
18
+ },
19
+ "163586": {
20
+ "content": "<|end_of_msg|>",
21
+ "lstrip": false,
22
+ "normalized": false,
23
+ "rstrip": false,
24
+ "single_word": false,
25
+ "special": true
26
+ },
27
+ "163587": {
28
+ "content": "<|open|>",
29
+ "lstrip": false,
30
+ "normalized": false,
31
+ "rstrip": false,
32
+ "single_word": false,
33
+ "special": false
34
+ },
35
+ "163588": {
36
+ "content": "<|close|>",
37
+ "lstrip": false,
38
+ "normalized": false,
39
+ "rstrip": false,
40
+ "single_word": false,
41
+ "special": false
42
+ },
43
+ "163589": {
44
+ "content": "<|sep|>",
45
+ "lstrip": false,
46
+ "normalized": false,
47
+ "rstrip": false,
48
+ "single_word": false,
49
+ "special": false
50
+ },
51
+ "163590": {
52
+ "content": "[start_header_id]",
53
+ "lstrip": false,
54
+ "normalized": false,
55
+ "rstrip": false,
56
+ "single_word": false,
57
+ "special": true
58
+ },
59
+ "163591": {
60
+ "content": "[end_header_id]",
61
+ "lstrip": false,
62
+ "normalized": false,
63
+ "rstrip": false,
64
+ "single_word": false,
65
+ "special": true
66
+ },
67
+ "163593": {
68
+ "content": "[EOT]",
69
+ "lstrip": false,
70
+ "normalized": false,
71
+ "rstrip": false,
72
+ "single_word": false,
73
+ "special": true
74
+ },
75
+ "163602": {
76
+ "content": "<|media_begin|>",
77
+ "lstrip": false,
78
+ "normalized": false,
79
+ "rstrip": false,
80
+ "single_word": false,
81
+ "special": true
82
+ },
83
+ "163603": {
84
+ "content": "<|media_content|>",
85
+ "lstrip": false,
86
+ "normalized": false,
87
+ "rstrip": false,
88
+ "single_word": false,
89
+ "special": true
90
+ },
91
+ "163604": {
92
+ "content": "<|media_end|>",
93
+ "lstrip": false,
94
+ "normalized": false,
95
+ "rstrip": false,
96
+ "single_word": false,
97
+ "special": true
98
+ },
99
+ "163605": {
100
+ "content": "<|media_pad|>",
101
+ "lstrip": false,
102
+ "normalized": false,
103
+ "rstrip": false,
104
+ "single_word": false,
105
+ "special": true
106
+ },
107
+ "163649": {
108
+ "content": "<osagent_mode>",
109
+ "lstrip": false,
110
+ "normalized": false,
111
+ "rstrip": false,
112
+ "single_word": false,
113
+ "special": true
114
+ },
115
+ "163838": {
116
+ "content": "[UNK]",
117
+ "lstrip": false,
118
+ "normalized": false,
119
+ "rstrip": false,
120
+ "single_word": false,
121
+ "special": true
122
+ },
123
+ "163839": {
124
+ "content": "[PAD]",
125
+ "lstrip": false,
126
+ "normalized": false,
127
+ "rstrip": false,
128
+ "single_word": false,
129
+ "special": true
130
+ }
131
+ },
132
+ "additional_special_tokens": [
133
+ "<|end_of_msg|>",
134
+ "[start_header_id]",
135
+ "[end_header_id]",
136
+ "[EOT]",
137
+ "<|media_begin|>",
138
+ "<|media_content|>",
139
+ "<|media_end|>",
140
+ "<|media_pad|>",
141
+ "<osagent_mode>"
142
+ ],
143
+ "bos_token": "[BOS]",
144
+ "clean_up_tokenization_spaces": false,
145
+ "eos_token": "[EOS]",
146
+ "extra_special_tokens": {},
147
+ "model_max_length": 1000000000000000019884624838656,
148
+ "pad_token": "[PAD]",
149
+ "tokenizer_class": "TikTokenTokenizer",
150
+ "unk_token": "[UNK]",
151
+ "auto_map": {
152
+ "AutoTokenizer": [
153
+ "tokenization_kimi.TikTokenTokenizer",
154
+ null
155
+ ]
156
+ }
157
+ }