tda45 commited on
Commit
c305fa5
·
verified ·
1 Parent(s): 76513ec

Upload folder using huggingface_hub (part 2)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +2 -0
  2. llama.cpp/conversion/llama.py +444 -0
  3. llama.cpp/conversion/llama4.py +38 -0
  4. llama.cpp/conversion/llava.py +129 -0
  5. llama.cpp/conversion/maincoder.py +14 -0
  6. llama.cpp/conversion/mamba.py +198 -0
  7. llama.cpp/conversion/mellum.py +61 -0
  8. llama.cpp/conversion/mimo.py +295 -0
  9. llama.cpp/conversion/minicpm.py +180 -0
  10. llama.cpp/conversion/minimax.py +54 -0
  11. llama.cpp/conversion/mistral.py +202 -0
  12. llama.cpp/conversion/mistral3.py +67 -0
  13. llama.cpp/conversion/mpt.py +49 -0
  14. llama.cpp/conversion/nemotron.py +385 -0
  15. llama.cpp/conversion/olmo.py +120 -0
  16. llama.cpp/conversion/openelm.py +83 -0
  17. llama.cpp/conversion/orion.py +37 -0
  18. llama.cpp/conversion/pangu.py +46 -0
  19. llama.cpp/conversion/phi.py +388 -0
  20. llama.cpp/conversion/pixtral.py +41 -0
  21. llama.cpp/conversion/plamo.py +195 -0
  22. llama.cpp/conversion/plm.py +23 -0
  23. llama.cpp/conversion/qwen.py +675 -0
  24. llama.cpp/conversion/qwen3vl.py +360 -0
  25. llama.cpp/conversion/qwenvl.py +200 -0
  26. llama.cpp/conversion/refact.py +68 -0
  27. llama.cpp/conversion/rwkv.py +302 -0
  28. llama.cpp/conversion/sarashina2.py +32 -0
  29. llama.cpp/conversion/smallthinker.py +82 -0
  30. llama.cpp/conversion/smolvlm.py +47 -0
  31. llama.cpp/conversion/stablelm.py +98 -0
  32. llama.cpp/conversion/starcoder.py +23 -0
  33. llama.cpp/conversion/step3.py +337 -0
  34. llama.cpp/conversion/t5.py +286 -0
  35. llama.cpp/conversion/talkie.py +53 -0
  36. llama.cpp/conversion/ultravox.py +203 -0
  37. llama.cpp/conversion/wavtokenizer.py +45 -0
  38. llama.cpp/conversion/xverse.py +90 -0
  39. llama.cpp/conversion/youtuvl.py +64 -0
  40. llama.cpp/convert_hf_to_gguf.py +298 -0
  41. llama.cpp/convert_hf_to_gguf_update.py +497 -0
  42. llama.cpp/convert_llama_ggml_to_gguf.py +450 -0
  43. llama.cpp/convert_lora_to_gguf.py +546 -0
  44. llama.cpp/docs/android.md +103 -0
  45. llama.cpp/docs/android/imported-into-android-studio.jpg +3 -0
  46. llama.cpp/docs/autoparser.md +534 -0
  47. llama.cpp/docs/backend/BLIS.md +60 -0
  48. llama.cpp/docs/backend/CANN.md +357 -0
  49. llama.cpp/docs/backend/CUDA-FEDORA.md +283 -0
  50. llama.cpp/docs/backend/OPENCL.md +294 -0
.gitattributes CHANGED
@@ -34,3 +34,5 @@ saved_model/**/* 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
  *.gguf 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
  *.gguf filter=lfs diff=lfs merge=lfs -text
37
+ llama.cpp/docs/android/imported-into-android-studio.jpg filter=lfs diff=lfs merge=lfs -text
38
+ llama.cpp/docs/development/llama-star/idea-arch.key filter=lfs diff=lfs merge=lfs -text
llama.cpp/conversion/llama.py ADDED
@@ -0,0 +1,444 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import math
5
+
6
+ from typing import Callable, Iterable, TYPE_CHECKING
7
+
8
+ import numpy as np
9
+ import torch
10
+
11
+ if TYPE_CHECKING:
12
+ from torch import Tensor
13
+
14
+ from .base import ModelBase, TextModel, gguf, logger
15
+
16
+
17
+ @ModelBase.register(
18
+ "LLaMAForCausalLM",
19
+ "LlamaForCausalLM",
20
+ "MistralForCausalLM",
21
+ "MixtralForCausalLM",
22
+ "VLlama3ForCausalLM",
23
+ "LlavaForConditionalGeneration",
24
+ "VoxtralForConditionalGeneration",
25
+ "LlamaForCausalLMEagle3",
26
+ "Eagle3LlamaForCausalLM",
27
+ "Eagle3Speculator",
28
+ "Eagle3DraftModel",
29
+ "IQuestCoderForCausalLM",
30
+ "LlamaModel")
31
+ class LlamaModel(TextModel):
32
+ model_arch = gguf.MODEL_ARCH.LLAMA
33
+ undo_permute = True
34
+
35
+ def __init__(self, *args, **kwargs):
36
+ super().__init__(*args, **kwargs)
37
+ # fix for SmolVLM2, missing `num_attention_heads` in config.json
38
+ if self.hf_arch == "VLlama3ForCausalLM":
39
+ self.hparams["num_attention_heads"] = self.hparams.get("num_attention_heads", 32)
40
+ # Mistral consolidated format has no config.json; origin_hf_arch is HF-only.
41
+ if self.is_mistral_format:
42
+ self.origin_hf_arch = None
43
+ else:
44
+ hparams = ModelBase.load_hparams(self.dir_model, is_mistral_format=False)
45
+ self.origin_hf_arch = hparams.get('architectures', [None])[0]
46
+
47
+ # Detect eagle3 draft checkpoint by hparams (some models don't use a distinct HF arch name)
48
+ if "draft_vocab_size" in self.hparams and self.hparams["num_hidden_layers"] == 1:
49
+ self.is_eagle3 = True
50
+ self.model_arch = gguf.MODEL_ARCH.EAGLE3
51
+ logger.info("Detected EAGLE-3 draft model, switching to EAGLE3 architecture")
52
+ # Re-initialize tensor_map with eagle3 architecture
53
+ self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count)
54
+ # Update gguf_writer architecture
55
+ self.gguf_writer.arch = gguf.MODEL_ARCH_NAMES[self.model_arch]
56
+ self.gguf_writer.add_architecture()
57
+ if self.target_model_dir is None:
58
+ raise ValueError(
59
+ "EAGLE-3 model requires --target-model-dir to be specified. "
60
+ "Please provide the path to the target model directory to read config.json"
61
+ )
62
+ # Read both eagle3 raw config and target model config
63
+ with open(self.dir_model / "config.json", 'r', encoding='utf-8') as f:
64
+ eagle3_raw_config = json.load(f)
65
+ with open(self.target_model_dir / "config.json", 'r', encoding='utf-8') as f:
66
+ target_config = json.load(f)
67
+
68
+ if "text_config" in target_config:
69
+ target_config = {**target_config, **target_config["text_config"]}
70
+ self.target_vocab_size = target_config["vocab_size"]
71
+
72
+ # target_layers: derived from target model layer count (low/mid/high)
73
+ target_num_layers = target_config["num_hidden_layers"]
74
+ target_layers = [2, target_num_layers // 2, target_num_layers - 3]
75
+ logger.info(f"EAGLE-3: target_layers = {target_layers} (target model has {target_num_layers} layers)")
76
+ self.gguf_writer.add_target_layers(target_layers)
77
+
78
+ # target_hidden_size: prefer eagle3 config, fallback to target config
79
+ if eagle3_raw_config.get("target_hidden_size") is not None:
80
+ target_hidden_size = eagle3_raw_config["target_hidden_size"]
81
+ src = "EAGLE-3 config"
82
+ else:
83
+ target_hidden_size = target_config["hidden_size"]
84
+ src = "target model config"
85
+ logger.info(f"EAGLE-3: target_hidden_size = {target_hidden_size} (from {src})")
86
+ self.gguf_writer.add_target_hidden_size(target_hidden_size)
87
+
88
+ # norm_before_residual (RedHat-style eagle3 specific)
89
+ norm_before_residual = eagle3_raw_config.get("norm_before_residual", False)
90
+ logger.info(f"EAGLE-3: norm_before_residual = {norm_before_residual}")
91
+ self.gguf_writer.add_norm_before_residual(norm_before_residual)
92
+
93
+ def set_vocab(self):
94
+ # eagle3: use tokenizer from target model if provided
95
+ original_dir_model = None
96
+ if getattr(self, 'is_eagle3', False):
97
+ assert self.target_model_dir is not None
98
+ logger.info(f"EAGLE-3: Using tokenizer from target model: {self.target_model_dir}")
99
+ original_dir_model = self.dir_model
100
+ self.dir_model = self.target_model_dir
101
+
102
+ if self.origin_hf_arch == "GlmasrModel":
103
+ return self._set_vocab_glmedge()
104
+
105
+ if self.is_mistral_format:
106
+ return self._set_vocab_mistral()
107
+
108
+ path_tekken_json = self.dir_model / "tekken.json"
109
+ path_tokenizer_json = self.dir_model / "tokenizer.json"
110
+ if path_tekken_json.is_file() and not path_tokenizer_json.is_file():
111
+ self._set_vocab_mistral()
112
+
113
+ tokenizer_config_file = self.dir_model / 'tokenizer_config.json'
114
+ if tokenizer_config_file.is_file():
115
+ with open(tokenizer_config_file, "r", encoding="utf-8") as f:
116
+ tokenizer_config_json = json.load(f)
117
+ if (add_prefix_space := tokenizer_config_json.get("add_prefix_space")) is not None:
118
+ self.gguf_writer.add_add_space_prefix(add_prefix_space)
119
+ if tokenizer_config_json.get("tokenizer_class") == "HybridDNATokenizer":
120
+ return self._set_vocab_hybriddna()
121
+
122
+ try:
123
+ self._set_vocab_sentencepiece()
124
+ except FileNotFoundError:
125
+ try:
126
+ self._set_vocab_llama_hf()
127
+ except (FileNotFoundError, TypeError):
128
+ # Llama 3
129
+ self._set_vocab_gpt2()
130
+
131
+ # Apply to CodeLlama only (and ignore for Llama 3 with a vocab size of 128256)
132
+ if self.hparams.get("vocab_size", 32000) == 32016:
133
+ special_vocab = gguf.SpecialVocab(
134
+ self.dir_model, load_merges=False,
135
+ special_token_types = ['prefix', 'suffix', 'middle', 'eot']
136
+ )
137
+ special_vocab._set_special_token("prefix", 32007)
138
+ special_vocab._set_special_token("suffix", 32008)
139
+ special_vocab._set_special_token("middle", 32009)
140
+ special_vocab._set_special_token("eot", 32010)
141
+ special_vocab.add_to_gguf(self.gguf_writer)
142
+
143
+ # Apply to granite small models only
144
+ if self.hparams.get("vocab_size", 32000) == 49152:
145
+ self.gguf_writer.add_add_bos_token(False)
146
+
147
+ # eagle3: Restore original dir_model
148
+ if original_dir_model is not None:
149
+ self.dir_model = original_dir_model
150
+
151
+ def set_gguf_parameters(self):
152
+ super().set_gguf_parameters()
153
+ hparams = self.hparams
154
+
155
+ if not self.is_mistral_format:
156
+ self.gguf_writer.add_vocab_size(hparams["vocab_size"])
157
+
158
+ if (rope_dim := hparams.get("head_dim")) is None:
159
+ rope_dim = hparams["hidden_size"] // hparams["num_attention_heads"]
160
+ self.gguf_writer.add_rope_dimension_count(rope_dim)
161
+
162
+ @staticmethod
163
+ def permute(weights: Tensor, n_head: int, n_head_kv: int | None):
164
+ if n_head_kv is not None and n_head != n_head_kv:
165
+ n_head = n_head_kv
166
+ return (weights.reshape(n_head, 2, weights.shape[0] // n_head // 2, *weights.shape[1:])
167
+ .swapaxes(1, 2)
168
+ .reshape(weights.shape))
169
+
170
+ def _repack_nvfp4(self, name: str, weight: Tensor, scale: Tensor, scale2: Tensor, input_scale: Tensor):
171
+ # Mirror the BF16 Q/K RoPE permutation site in modify_tensors; the NVFP4 path bypasses it.
172
+ if self.undo_permute:
173
+ n_head = self.find_hparam(["n_heads", "num_attention_heads"], optional=True)
174
+ n_kv_head = self.find_hparam(["n_kv_heads", "num_key_value_heads"], optional=True)
175
+ if n_head is not None:
176
+ if name.endswith("q_proj.weight"):
177
+ weight = LlamaModel.permute(weight, n_head, n_head)
178
+ scale = LlamaModel.permute(scale, n_head, n_head)
179
+ elif name.endswith("k_proj.weight"):
180
+ weight = LlamaModel.permute(weight, n_head, n_kv_head)
181
+ scale = LlamaModel.permute(scale, n_head, n_kv_head)
182
+ super()._repack_nvfp4(name, weight, scale, scale2, input_scale)
183
+
184
+ _experts: list[dict[str, Tensor]] | None = None
185
+
186
+ @classmethod
187
+ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
188
+ name, gen = item
189
+
190
+ if "text_model." in name:
191
+ name = name.replace("text_model.", "") # for SmolVLM
192
+
193
+ return super().filter_tensors((name, gen))
194
+
195
+ def index_tensors(self, remote_hf_model_id: str | None = None) -> dict[str, Callable[[], Tensor]]:
196
+ tensors = super().index_tensors(remote_hf_model_id)
197
+
198
+ # Handle Eagle3Speculator nested config
199
+ if "transformer_layer_config" in self.hparams:
200
+ self.hparams = {**self.hparams, **self.hparams["transformer_layer_config"]}
201
+
202
+ # eagle3 detection
203
+ if "draft_vocab_size" in self.hparams and self.hparams["num_hidden_layers"] == 1:
204
+ logger.info("EAGLE-3: renaming midlayer.* / layers.0.* to model.layers.0.*")
205
+ new_tensors = {}
206
+ for name, gen in tensors.items():
207
+ if name.startswith("midlayer."):
208
+ new_name = "model.layers.0." + name[len("midlayer."):]
209
+ new_tensors[new_name] = gen
210
+ elif name.startswith("layers.0."): # Eagle3Speculator format
211
+ new_name = "model." + name
212
+ new_tensors[new_name] = gen
213
+ else:
214
+ new_tensors[name] = gen
215
+ return new_tensors
216
+
217
+ return tensors
218
+
219
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
220
+ # eagle3: special tensors that bypass standard llama mapping
221
+ if getattr(self, 'is_eagle3', False):
222
+ if name == "fc.weight":
223
+ yield (name, data_torch)
224
+ return
225
+ if name == "d2t":
226
+ # store for manual int64 handling in prepare_tensors (avoid F32 conversion)
227
+ if not hasattr(self, '_eagle3_int_tensors'):
228
+ self._eagle3_int_tensors = {}
229
+ self._eagle3_int_tensors[name] = data_torch
230
+ return
231
+ if name == "t2d":
232
+ # not used at runtime, skip
233
+ return
234
+ if name.endswith(".hidden_norm.weight"):
235
+ yield (self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_NORM_2, bid), data_torch)
236
+ return
237
+
238
+ n_head = self.find_hparam(["n_heads", "num_attention_heads"])
239
+ n_kv_head = self.find_hparam(["n_kv_heads", "num_key_value_heads"])
240
+
241
+ if self.hf_arch == "LlamaModel":
242
+ name = "model." + name
243
+
244
+ if self.undo_permute:
245
+ if name.endswith(("q_proj.weight", "q_proj.bias")):
246
+ data_torch = LlamaModel.permute(data_torch, n_head, n_head)
247
+ if name.endswith(("k_proj.weight", "k_proj.bias")):
248
+ data_torch = LlamaModel.permute(data_torch, n_head, n_kv_head)
249
+
250
+ # process the experts separately
251
+ if name.find("block_sparse_moe.experts") != -1:
252
+ n_experts = self.hparams["num_local_experts"]
253
+
254
+ assert bid is not None
255
+
256
+ if self._experts is None:
257
+ self._experts = [{} for _ in range(self.block_count)]
258
+
259
+ self._experts[bid][name] = data_torch
260
+
261
+ if len(self._experts[bid]) >= n_experts * 3:
262
+ # merge the experts into a single 3d tensor
263
+ for wid in ["w1", "w2", "w3"]:
264
+ datas: list[Tensor] = []
265
+
266
+ for xid in range(n_experts):
267
+ ename = f"model.layers.{bid}.block_sparse_moe.experts.{xid}.{wid}.weight"
268
+ datas.append(self._experts[bid][ename])
269
+ del self._experts[bid][ename]
270
+
271
+ data_torch = torch.stack(datas, dim=0)
272
+
273
+ merged_name = f"layers.{bid}.feed_forward.experts.{wid}.weight"
274
+
275
+ yield from super().modify_tensors(data_torch, merged_name, bid)
276
+ return
277
+ else:
278
+ return
279
+
280
+ yield from super().modify_tensors(data_torch, name, bid)
281
+
282
+ def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]:
283
+ if rope_params := self.rope_parameters.get("full_attention", self.rope_parameters):
284
+ if rope_params.get("rope_type", '').lower() == "llama3":
285
+ base = rope_params.get("rope_theta", 10000.0)
286
+ if (dim := self.hparams.get("head_dim")) is None:
287
+ dim = self.hparams["hidden_size"] // self.hparams["num_attention_heads"]
288
+ freqs = 1.0 / (base ** (torch.arange(0, dim, 2, dtype=torch.float32) / dim))
289
+
290
+ factor = rope_params.get("factor", 8.0)
291
+ low_freq_factor = rope_params.get("low_freq_factor", 1.0)
292
+ high_freq_factor = rope_params.get("high_freq_factor", 4.0)
293
+ old_context_len = rope_params.get("original_max_position_embeddings", 8192)
294
+
295
+ low_freq_wavelen = old_context_len / low_freq_factor
296
+ high_freq_wavelen = old_context_len / high_freq_factor
297
+ # assert low_freq_wavelen != high_freq_wavelen # Errors for Llama4
298
+
299
+ rope_factors = []
300
+ for freq in freqs:
301
+ wavelen = 2 * math.pi / freq
302
+ if wavelen < high_freq_wavelen:
303
+ rope_factors.append(1)
304
+ elif wavelen > low_freq_wavelen:
305
+ rope_factors.append(factor)
306
+ else:
307
+ smooth = (old_context_len / wavelen - low_freq_factor) / (high_freq_factor - low_freq_factor)
308
+ rope_factors.append(1 / ((1 - smooth) / factor + smooth))
309
+
310
+ yield (self.format_tensor_name(gguf.MODEL_TENSOR.ROPE_FREQS), torch.tensor(rope_factors, dtype=torch.float32))
311
+
312
+ def prepare_tensors(self):
313
+ # eagle3: collect d2t original dtype before parent converts tensors to F32
314
+ eagle3_original_dtypes = {}
315
+ if getattr(self, 'is_eagle3', False):
316
+ for name, data_torch in self.get_tensors():
317
+ if name == "d2t":
318
+ eagle3_original_dtypes[name] = data_torch.dtype
319
+
320
+ super().prepare_tensors()
321
+
322
+ # eagle3: write d2t as absolute target token ids
323
+ if getattr(self, 'is_eagle3', False) and hasattr(self, '_eagle3_int_tensors'):
324
+ for name, data_torch in self._eagle3_int_tensors.items():
325
+ old_dtype = eagle3_original_dtypes.get(name, data_torch.dtype)
326
+ data = data_torch.to(torch.int64).cpu().numpy()
327
+ if name == "d2t":
328
+ data = data.reshape(-1)
329
+ data = data + np.arange(data.size, dtype=np.int64)
330
+ if np.any((data < 0) | (data >= self.target_vocab_size)):
331
+ raise ValueError(f"EAGLE-3 d2t target ids out of range for target vocab size {self.target_vocab_size}")
332
+ if np.unique(data).size != data.size:
333
+ raise ValueError("EAGLE-3 d2t contains duplicate target ids")
334
+ data_qtype = gguf.GGMLQuantizationType.I64
335
+
336
+ shape_str = f"{{{', '.join(str(n) for n in reversed(data.shape))}}}"
337
+ logger.info(f"{name + ',':<30} {old_dtype} --> {data_qtype.name}, shape = {shape_str}")
338
+ self.gguf_writer.add_tensor(name, data, raw_dtype=data_qtype)
339
+
340
+ if self._experts is not None:
341
+ # flatten `list[dict[str, Tensor]]` into `list[str]`
342
+ experts = [k for d in self._experts for k in d.keys()]
343
+ if len(experts) > 0:
344
+ raise ValueError(f"Unprocessed experts: {experts}")
345
+
346
+
347
+ @ModelBase.register("ArceeForCausalLM")
348
+ class ArceeModel(LlamaModel):
349
+ model_arch = gguf.MODEL_ARCH.ARCEE
350
+
351
+ def set_gguf_parameters(self):
352
+ super().set_gguf_parameters()
353
+ self._try_set_pooling_type()
354
+
355
+
356
+ @ModelBase.register(
357
+ "Llama4ForConditionalGeneration",
358
+ "Llama4ForCausalLM",
359
+ )
360
+ class Llama4Model(LlamaModel):
361
+ model_arch = gguf.MODEL_ARCH.LLAMA4
362
+ undo_permute = False
363
+
364
+ def __init__(self, *args, **kwargs):
365
+ super().__init__(*args, **kwargs)
366
+ # IMPORTANT: the normal "intermediate_size" is renamed to "intermediate_size_mlp", we need to undo this
367
+ self.hparams["intermediate_size_moe"] = self.hparams["intermediate_size"]
368
+ self.hparams["intermediate_size"] = self.hparams["intermediate_size_mlp"]
369
+
370
+ def set_vocab(self):
371
+ self._set_vocab_gpt2()
372
+
373
+ def set_gguf_parameters(self):
374
+ super().set_gguf_parameters()
375
+ self.gguf_writer.add_interleave_moe_layer_step(self.hparams["interleave_moe_layer_step"])
376
+ self.gguf_writer.add_expert_feed_forward_length(self.hparams["intermediate_size_moe"])
377
+ if "layer_types" in self.hparams:
378
+ if all(lt == "full_attention" for lt in self.hparams["layer_types"]):
379
+ # all layers are full attention (for MobileLLM), disable swa
380
+ self.gguf_writer.add_sliding_window(0)
381
+
382
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None):
383
+ # split the gate_up into gate and up
384
+ if "gate_up_proj" in name:
385
+ name_up = name.replace("gate_up_proj", "up_proj.weight")
386
+ name_gate = name.replace("gate_up_proj", "gate_proj.weight")
387
+ dim_half = data_torch.shape[-1] // 2
388
+ gate_proj_weight, up_proj_weight = data_torch.transpose(-1, -2).split(dim_half, dim=-2)
389
+ yield from super().modify_tensors(gate_proj_weight, name_gate, bid)
390
+ yield from super().modify_tensors(up_proj_weight, name_up, bid)
391
+ return
392
+
393
+ if name.endswith("down_proj"):
394
+ name += ".weight"
395
+ data_torch = data_torch.transpose(-1, -2)
396
+
397
+ yield from super().modify_tensors(data_torch, name, bid)
398
+
399
+
400
+ @ModelBase.register("LlamaBidirectionalModel")
401
+ class LlamaEmbedNemotronModel(LlamaModel):
402
+ model_arch = gguf.MODEL_ARCH.LLAMA_EMBED
403
+
404
+
405
+ @ModelBase.register("SmolLM3ForCausalLM")
406
+ class SmolLM3Model(LlamaModel):
407
+ model_arch = gguf.MODEL_ARCH.SMOLLM3
408
+
409
+
410
+ @ModelBase.register("ApertusForCausalLM")
411
+ class ApertusModel(LlamaModel):
412
+ model_arch = gguf.MODEL_ARCH.APERTUS
413
+ undo_permute = False
414
+
415
+ _alpha_n = {}
416
+ _alpha_p = {}
417
+ _beta = {}
418
+ _eps = {}
419
+
420
+ def modify_tensors(self, data_torch, name, bid):
421
+ # Handle xIELU activation parameters
422
+ n_layers = self.hparams["num_hidden_layers"]
423
+ if name.endswith(".act_fn.alpha_n"):
424
+ self._alpha_n[bid] = data_torch.to("cpu").float().item()
425
+ if (len(self._alpha_n) == n_layers):
426
+ self.gguf_writer.add_xielu_alpha_n([self._alpha_n[k] for k in sorted(self._alpha_n)])
427
+ return
428
+ if name.endswith(".act_fn.alpha_p"):
429
+ self._alpha_p[bid] = data_torch.to("cpu").float().item()
430
+ if (len(self._alpha_p) == n_layers):
431
+ self.gguf_writer.add_xielu_alpha_p([self._alpha_p[k] for k in sorted(self._alpha_p)])
432
+ return
433
+ if name.endswith(".act_fn.beta"):
434
+ self._beta[bid] = data_torch.to("cpu").float().item()
435
+ if (len(self._beta) == n_layers):
436
+ self.gguf_writer.add_xielu_beta([self._beta[k] for k in sorted(self._beta)])
437
+ return
438
+ if name.endswith(".act_fn.eps"):
439
+ self._eps[bid] = data_torch.to("cpu").float().item()
440
+ if (len(self._eps) == n_layers):
441
+ self.gguf_writer.add_xielu_eps([self._eps[k] for k in sorted(self._eps)])
442
+ return
443
+
444
+ yield from super().modify_tensors(data_torch, name, bid)
llama.cpp/conversion/llama4.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable, Iterable, TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from torch import Tensor
7
+
8
+ from .base import MmprojModel, ModelBase, gguf
9
+
10
+
11
+ @ModelBase.register("Llama4ForConditionalGeneration")
12
+ class Llama4VisionModel(MmprojModel):
13
+ def set_gguf_parameters(self):
14
+ super().set_gguf_parameters()
15
+ self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.LLAMA4)
16
+ self.gguf_writer.add_vision_attention_layernorm_eps(self.hparams["norm_eps"])
17
+ self.gguf_writer.add_vision_projector_scale_factor(int(1.0 / self.hparams["pixel_shuffle_ratio"]))
18
+ assert self.hparams["hidden_act"] == "gelu"
19
+ self.gguf_writer.add_vision_use_gelu(True)
20
+
21
+ @classmethod
22
+ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
23
+ name, gen = item
24
+
25
+ if "multi_modal_projector" not in name and "vision_model" not in name:
26
+ return None
27
+
28
+ if "positional_embedding_vlm" in name and ".weight" not in name:
29
+ name += ".weight"
30
+
31
+ return super().filter_tensors((name, gen))
32
+
33
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
34
+ if "multi_modal_projector.linear_1" in name:
35
+ # despite the name with number postfix, this is a single fully connected layer
36
+ yield (gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.V_MMPROJ_FC] + '.weight', data_torch)
37
+ else:
38
+ yield from super().modify_tensors(data_torch, name, bid)
llama.cpp/conversion/llava.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ from typing import Iterable, TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from torch import Tensor
9
+
10
+ from .base import MmprojModel, ModelBase, gguf, logger
11
+
12
+ from .llama import LlamaModel
13
+
14
+
15
+ @ModelBase.register(
16
+ "LlavaForConditionalGeneration", # pixtral
17
+ "Mistral3ForConditionalGeneration", # mistral small 3.1
18
+ )
19
+ class LlavaVisionModel(MmprojModel):
20
+ img_break_tok_id = -1
21
+ use_break_tok = True
22
+
23
+ def __init__(self, *args, **kwargs):
24
+ super().__init__(*args, **kwargs)
25
+ if self.hparams.get("model_type") == "pixtral":
26
+ # layer_norm_eps is not in config.json, it is hard-coded in modeling_pixtral.py
27
+ self.hparams["layer_norm_eps"] = self.hparams.get("layer_norm_eps", 1e-5)
28
+ if self.use_break_tok:
29
+ self.img_break_tok_id = self.get_token_id("[IMG_BREAK]")
30
+ elif self.is_mistral_format:
31
+ # hparams is already vision config here so norm_eps is only defined in global_config.
32
+ self.hparams["norm_eps"] = self.global_config.get("norm_eps", None)
33
+ assert self.hparams["norm_eps"] is not None, "norm_eps not found in params.json"
34
+ if self.use_break_tok:
35
+ self.img_break_tok_id = self.find_vparam(["image_break_token_id"])
36
+
37
+ # params.json may ship -1 placeholders (Mistral Medium 3.5)
38
+ # resolve the real id from the bundled tokenizer in that case
39
+ if self.img_break_tok_id < 0:
40
+ self.img_break_tok_id = self.get_mistral_token_id("[IMG_BREAK]")
41
+ else:
42
+ raise ValueError(f"Unsupported model type: {self.hparams['model_type']}")
43
+ logger.info(f"Image break token id: {self.img_break_tok_id}")
44
+
45
+ def get_token_id(self, token: str) -> int:
46
+ tokenizer_config_file = self.dir_model / 'tokenizer_config.json'
47
+ with open(tokenizer_config_file, "r", encoding="utf-8") as f:
48
+ added_tokens_decoder = json.load(f).get('added_tokens_decoder') or {}
49
+ for id_, token_data in added_tokens_decoder.items():
50
+ if token_data.get("content") == token:
51
+ return int(id_)
52
+ # fallthrough to tokenizer.json
53
+ with open(self.dir_model / "tokenizer.json", "r", encoding="utf-8") as f:
54
+ tokenizer_json = json.load(f)
55
+ for token_data in tokenizer_json["added_tokens"]:
56
+ if token_data["content"] == token:
57
+ return int(token_data["id"])
58
+ raise ValueError(f"Token '{token}' not found in tokenizer config.")
59
+
60
+ def get_mistral_token_id(self, token: str) -> int:
61
+ # mistral native format ships tekken.json or a versioned spm tokenizer
62
+ tekken_file = self.dir_model / "tekken.json"
63
+ if tekken_file.is_file():
64
+ with open(tekken_file, "r", encoding="utf-8") as f:
65
+ data = json.load(f)
66
+ for entry in data.get("special_tokens", []):
67
+ if entry.get("token_str") == token:
68
+ return int(entry["rank"])
69
+ tokenizer_json_file = self.dir_model / "tokenizer.json"
70
+ if tokenizer_json_file.is_file():
71
+ with open(tokenizer_json_file, "r", encoding="utf-8") as f:
72
+ data = json.load(f)
73
+ for entry in data.get("added_tokens", []):
74
+ if entry.get("content") == token:
75
+ return int(entry["id"])
76
+ raise ValueError(f"Token '{token}' not found in mistral tokenizer files.")
77
+
78
+ def set_gguf_parameters(self):
79
+ super().set_gguf_parameters()
80
+ hparams = self.hparams
81
+ if hparams.get("model_type") == "pixtral":
82
+ self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.PIXTRAL)
83
+ self.gguf_writer.add_vision_attention_layernorm_eps(hparams["layer_norm_eps"])
84
+
85
+ # hidden_act
86
+ if hparams["hidden_act"] == "silu":
87
+ self.gguf_writer.add_vision_use_silu(True)
88
+ elif hparams["hidden_act"] == "gelu":
89
+ self.gguf_writer.add_vision_use_gelu(True)
90
+ else:
91
+ raise ValueError(f"Unsupported hidden_act: {hparams['hidden_act']}")
92
+
93
+ # spatial_merge_size
94
+ if "spatial_merge_size" in self.global_config:
95
+ self.gguf_writer.add_vision_spatial_merge_size(self.global_config["spatial_merge_size"])
96
+
97
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
98
+ n_head = (
99
+ self.hparams["num_attention_heads"] if not self.is_mistral_format else self.find_vparam(["num_attention_heads"])
100
+ )
101
+ n_kv_head = n_head
102
+
103
+ valid_prefixes = (
104
+ "multi_modal_projector.",
105
+ "vision_tower.",
106
+ "vision_encoder.",
107
+ "vision_language_adapter.",
108
+ "patch_merger.",
109
+ "pre_mm_projector_norm",
110
+ )
111
+
112
+ if any(name.startswith(prefix) for prefix in valid_prefixes):
113
+ # process vision tensors
114
+ if name.endswith(("q_proj.weight", "q_proj.bias")) and not self.is_mistral_format:
115
+ data_torch = LlamaModel.permute(data_torch, n_head, n_head)
116
+ if name.endswith(("k_proj.weight", "k_proj.bias")) and not self.is_mistral_format:
117
+ data_torch = LlamaModel.permute(data_torch, n_head, n_kv_head)
118
+ yield from super().modify_tensors(data_torch, name, bid)
119
+ return
120
+
121
+ embed_key = "embed_tokens.weight" if not self.is_mistral_format else "tok_embeddings.weight"
122
+ if self.img_break_tok_id > 0 and embed_key in name:
123
+ logger.info(f"Extracting [IMG_BREAK] token embedding from {name}")
124
+ # for pixtral model, we need to extract the [IMG_BREAK] token embedding
125
+ img_break_embd = data_torch[self.img_break_tok_id]
126
+ name = gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.V_TOK_EMBD_IMG_BREAK]
127
+ yield from super().modify_tensors(img_break_embd, name, bid)
128
+
129
+ return # skip other tensors
llama.cpp/conversion/maincoder.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from .base import ModelBase, TextModel, gguf
4
+
5
+
6
+ @ModelBase.register("MaincoderForCausalLM")
7
+ class MaincoderModel(TextModel):
8
+ model_arch = gguf.MODEL_ARCH.MAINCODER
9
+
10
+ def set_gguf_parameters(self):
11
+ super().set_gguf_parameters()
12
+
13
+ if (head_dim := self.hparams.get("head_dim")) is not None:
14
+ self.gguf_writer.add_rope_dimension_count(head_dim)
llama.cpp/conversion/mamba.py ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ from pathlib import Path
6
+ from typing import Callable, Iterable, TYPE_CHECKING
7
+
8
+ import torch
9
+
10
+ if TYPE_CHECKING:
11
+ from torch import Tensor
12
+
13
+ from .base import ModelBase, TextModel, gguf, logger
14
+
15
+
16
+ @ModelBase.register("MambaForCausalLM", "MambaLMHeadModel", "FalconMambaForCausalLM")
17
+ class MambaModel(TextModel):
18
+ model_arch = gguf.MODEL_ARCH.MAMBA
19
+
20
+ def __init__(self, dir_model: Path, *args, **kwargs):
21
+ # Avoid using AutoConfig for hparams
22
+ hparams = kwargs.pop("hparams", None)
23
+ if hparams is None:
24
+ with open(dir_model / "config.json", "r", encoding="utf-8") as f:
25
+ hparams = json.load(f)
26
+ super().__init__(dir_model, *args, hparams=hparams, **kwargs)
27
+
28
+ def set_vocab(self):
29
+ vocab_size = self.hparams["vocab_size"]
30
+ # Round vocab size to next multiple of 8
31
+ pad_vocab = self.hparams.get("pad_vocab_size_multiple", 8)
32
+ # pad using ceiling division
33
+ # ref: https://stackoverflow.com/a/17511341/22827863
34
+ vocab_size = -(vocab_size // -pad_vocab) * pad_vocab
35
+ self.hparams["vocab_size"] = vocab_size
36
+
37
+ if (self.dir_model / "tokenizer.json").is_file():
38
+ self._set_vocab_gpt2()
39
+ elif (self.dir_model / "tokenizer.model").is_file():
40
+ self._set_vocab_sentencepiece()
41
+ else:
42
+ # Use the GPT-NeoX tokenizer when no tokenizer files are present
43
+ self._set_vocab_builtin("gpt-neox", vocab_size)
44
+
45
+ def set_gguf_parameters(self):
46
+ d_model = self.find_hparam(["hidden_size", "d_model"])
47
+ d_conv = self.find_hparam(["conv_kernel", "d_conv"], optional=True) or 4
48
+ d_inner = self.find_hparam(["intermediate_size", "d_inner"], optional=True) or 2 * d_model
49
+ d_state = self.find_hparam(["state_size", "d_state"], optional=True) or 16
50
+ # ceiling division
51
+ # ref: https://stackoverflow.com/a/17511341/22827863
52
+ # ref: https://github.com/state-spaces/mamba/blob/ce59daea3a090d011d6476c6e5b97f6d58ddad8b/mamba_ssm/modules/mamba_simple.py#L58
53
+ dt_rank = self.find_hparam(["time_step_rank", "dt_rank"], optional=True) or -(d_model // -16)
54
+ rms_norm_eps = self.find_hparam(["layer_norm_epsilon", "rms_norm_eps"], optional=True) or 1e-5
55
+ use_dt_b_c_norm = False
56
+ # For falconmamba we do apply RMS norm on B / DT and C layers
57
+ if self.find_hparam(["model_type"], optional=True) in ("falcon_mamba",):
58
+ use_dt_b_c_norm = True
59
+ # Fail early for models which don't have a block expansion factor of 2
60
+ assert d_inner == 2 * d_model
61
+
62
+ self.gguf_writer.add_context_length(2**20) # arbitrary value; for those who use the default
63
+ self.gguf_writer.add_embedding_length(d_model)
64
+ self.gguf_writer.add_feed_forward_length(0) # unused, but seemingly required when loading
65
+ self.gguf_writer.add_head_count(0) # unused, but seemingly required when loading
66
+ self.gguf_writer.add_block_count(self.block_count)
67
+ self.gguf_writer.add_ssm_conv_kernel(d_conv)
68
+ self.gguf_writer.add_ssm_inner_size(d_inner)
69
+ self.gguf_writer.add_ssm_state_size(d_state)
70
+ self.gguf_writer.add_ssm_time_step_rank(dt_rank)
71
+ self.gguf_writer.add_layer_norm_rms_eps(rms_norm_eps)
72
+ self.gguf_writer.add_ssm_dt_b_c_rms(use_dt_b_c_norm) # For classic Mamba we don't apply rms norm on B / DT layers
73
+ self.gguf_writer.add_file_type(self.ftype)
74
+
75
+ _tok_embd = None
76
+
77
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
78
+ output_name = self.format_tensor_name(gguf.MODEL_TENSOR.OUTPUT)
79
+ tok_embd_name = self.format_tensor_name(gguf.MODEL_TENSOR.TOKEN_EMBD)
80
+
81
+ new_name = self.map_tensor_name(name)
82
+
83
+ if name.endswith(".A_log"):
84
+ logger.debug("A_log --> A ==> " + new_name)
85
+ data_torch = -torch.exp(data_torch)
86
+
87
+ # [4 1 8192 1] -> [4 8192 1 1]
88
+ if self.match_model_tensor_name(new_name, gguf.MODEL_TENSOR.SSM_CONV1D, bid):
89
+ data_torch = data_torch.squeeze()
90
+
91
+ # assuming token_embd.weight is seen before output.weight
92
+ if self._tok_embd is not None and new_name == output_name:
93
+ if torch.equal(self._tok_embd, data_torch):
94
+ logger.debug(f"{output_name} is equivalent to {tok_embd_name}, omitting")
95
+ return
96
+ elif new_name == tok_embd_name:
97
+ self._tok_embd = data_torch
98
+
99
+ yield from super().modify_tensors(data_torch, new_name, bid)
100
+
101
+
102
+ @ModelBase.register("Mamba2ForCausalLM")
103
+ class Mamba2Model(TextModel):
104
+ model_arch = gguf.MODEL_ARCH.MAMBA2
105
+
106
+ def __init__(self, dir_model: Path, *args, **kwargs):
107
+ # Avoid using AutoConfig for hparams
108
+ # It wrongly assumes all Mamba2 models are Mamba-Codestral-7B-v0.1
109
+ hparams = kwargs.pop("hparams", None)
110
+ if hparams is None:
111
+ with open(dir_model / "config.json", "r", encoding="utf-8") as f:
112
+ hparams = json.load(f)
113
+ if "llm_config" in hparams:
114
+ hparams["text_config"] = hparams["llm_config"]
115
+ super().__init__(dir_model, *args, hparams=hparams, **kwargs)
116
+ self.d_model = self.find_hparam(["hidden_size", "d_model", "dim"])
117
+ self.expand = self.find_hparam(["mamba_expand", "expand"], optional=True) or 2
118
+ self.d_inner = self.find_hparam(["mamba_d_ssm", "intermediate_size", "d_inner"], optional=True) or self.expand * self.d_model
119
+ self.n_group = self.find_hparam(["n_groups"], optional=True) or 1
120
+
121
+ def set_vocab(self):
122
+ vocab_size = self.hparams["vocab_size"]
123
+ # Round vocab size to next multiple of 16
124
+ pad_vocab = self.hparams.get("pad_vocab_size_multiple", 16)
125
+ # pad using ceiling division
126
+ # ref: https://stackoverflow.com/a/17511341/22827863
127
+ vocab_size = -(vocab_size // -pad_vocab) * pad_vocab
128
+ self.hparams["vocab_size"] = vocab_size
129
+
130
+ if (self.dir_model / "tokenizer.model").is_file():
131
+ self._set_vocab_sentencepiece()
132
+ elif (self.dir_model / "tokenizer.model.v3").is_file():
133
+ # mamba-codestral
134
+ raise NotImplementedError(f"Please rename {self.dir_model / 'tokenizer.model.v3'} to {self.dir_model / 'tokenizer.model'}")
135
+ elif (self.dir_model / "tokenizer.json").is_file():
136
+ self._set_vocab_gpt2()
137
+ else:
138
+ # Use the GPT-NeoX tokenizer when no tokenizer files are present
139
+ self._set_vocab_builtin("gpt-neox", vocab_size)
140
+
141
+ def set_gguf_parameters(self):
142
+ d_conv = self.find_hparam(["conv_kernel", "d_conv"], optional=True) or 4
143
+ d_state = self.find_hparam(["state_size", "d_state"], optional=True) or 128
144
+ head_dim = self.find_hparam(["mamba_d_head", "head_dim"], optional=True) or 64
145
+
146
+ rms_norm_eps = self.find_hparam(["layer_norm_epsilon", "rms_norm_eps"], optional=True) or 1e-5
147
+
148
+ # skip the assertion for FalconH1 Model
149
+ if self.model_arch != gguf.MODEL_ARCH.FALCON_H1:
150
+ assert self.d_inner == self.expand * self.d_model
151
+ assert self.d_inner % head_dim == 0
152
+
153
+ self.gguf_writer.add_context_length(2**20) # arbitrary value; for those who use the default
154
+ self.gguf_writer.add_embedding_length(self.d_model)
155
+ self.gguf_writer.add_feed_forward_length(0) # unused, but seemingly required when loading
156
+ self.gguf_writer.add_head_count(0) # unused, but seemingly required when loading
157
+ self.gguf_writer.add_block_count(self.block_count)
158
+ self.gguf_writer.add_ssm_conv_kernel(d_conv)
159
+ self.gguf_writer.add_ssm_inner_size(self.d_inner)
160
+ self.gguf_writer.add_ssm_state_size(d_state)
161
+ self.gguf_writer.add_ssm_time_step_rank(self.d_inner // head_dim)
162
+ self.gguf_writer.add_ssm_group_count(self.n_group)
163
+ self.gguf_writer.add_layer_norm_rms_eps(rms_norm_eps)
164
+ self.gguf_writer.add_file_type(self.ftype)
165
+
166
+ @classmethod
167
+ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
168
+ name, gen = item
169
+
170
+ if name.startswith(("model.backbone", "model.lm_head")):
171
+ # map Mamba-Codestral-7B-v0.1 tensor names to the names used by Mamba-2
172
+ name = name.removeprefix("model.")
173
+
174
+ if name.endswith(".dt_bias"):
175
+ name = name.rpartition(".dt_bias")[0] + ".dt_proj.bias"
176
+
177
+ return super().filter_tensors((name, gen))
178
+
179
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
180
+ new_name = self.map_tensor_name(name)
181
+
182
+ if self.match_model_tensor_name(new_name, gguf.MODEL_TENSOR.SSM_CONV1D, bid):
183
+ data_torch = data_torch.squeeze()
184
+ elif any(self.match_model_tensor_name(new_name, t, bid, suffix="") for t in [
185
+ gguf.MODEL_TENSOR.SSM_A,
186
+ gguf.MODEL_TENSOR.SSM_D,
187
+ ]):
188
+ # unsqueeze A to use similar shape semantics as Mamba-1
189
+ # (D is also unsqueezed, but for more straightforward broadcast internally)
190
+ data_torch = data_torch.reshape((*data_torch.shape, 1))
191
+ elif self.match_model_tensor_name(new_name, gguf.MODEL_TENSOR.SSM_NORM, bid):
192
+ data_torch = data_torch.reshape((self.n_group, self.d_inner // self.n_group))
193
+
194
+ if name.endswith(".A_log"):
195
+ logger.debug("A_log --> A ==> " + new_name)
196
+ data_torch = -torch.exp(data_torch)
197
+
198
+ yield (new_name, data_torch)
llama.cpp/conversion/mellum.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Iterable, TYPE_CHECKING
4
+
5
+ import torch
6
+
7
+ if TYPE_CHECKING:
8
+ from torch import Tensor
9
+
10
+ from .base import ModelBase, TextModel, gguf, logger
11
+
12
+
13
+ @ModelBase.register("MellumForCausalLM")
14
+ class MellumModel(TextModel):
15
+ model_arch = gguf.MODEL_ARCH.MELLUM
16
+
17
+ def set_gguf_parameters(self):
18
+ super().set_gguf_parameters()
19
+ if (moe_intermediate_size := self.hparams.get("moe_intermediate_size")) is not None:
20
+ self.gguf_writer.add_expert_feed_forward_length(moe_intermediate_size)
21
+ logger.info(f"gguf: expert feed forward length = {moe_intermediate_size}")
22
+
23
+ use_sliding_window = self.hparams.get("use_sliding_window")
24
+ sliding_window = self.hparams.get("sliding_window")
25
+ if (use_sliding_window is True or use_sliding_window is None) and sliding_window is not None:
26
+ self.gguf_writer.add_sliding_window(sliding_window)
27
+ logger.info(f"gguf: sliding window = {sliding_window}")
28
+ self.gguf_writer.add_sliding_window_pattern([t == "sliding_attention" for t in self.hparams["layer_types"]])
29
+ logger.info(f"gguf: sliding window pattern length = {len(self.hparams['layer_types'])}")
30
+
31
+ _experts: list[dict[str, Tensor]] | None = None
32
+
33
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
34
+ if name.find("experts") != -1:
35
+ n_experts = self.find_hparam(["num_local_experts", "num_experts"])
36
+ assert bid is not None
37
+
38
+ if self._experts is None:
39
+ self._experts = [{} for _ in range(self.block_count)]
40
+
41
+ self._experts[bid][name] = data_torch
42
+
43
+ if len(self._experts[bid]) >= n_experts * 3:
44
+ for w_name in ["down_proj", "gate_proj", "up_proj"]:
45
+ datas: list[Tensor] = []
46
+
47
+ for xid in range(n_experts):
48
+ ename = f"model.layers.{bid}.mlp.experts.{xid}.{w_name}.weight"
49
+ datas.append(self._experts[bid][ename])
50
+ del self._experts[bid][ename]
51
+
52
+ data_torch = torch.stack(datas, dim=0)
53
+
54
+ merged_name = f"model.layers.{bid}.mlp.experts.{w_name}.weight"
55
+
56
+ yield from super().modify_tensors(data_torch, merged_name, bid)
57
+ return
58
+ else:
59
+ return
60
+
61
+ yield from super().modify_tensors(data_torch, name, bid)
llama.cpp/conversion/mimo.py ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from typing import Callable, TYPE_CHECKING
6
+
7
+ import torch
8
+
9
+ if TYPE_CHECKING:
10
+ from torch import Tensor
11
+
12
+ from .base import MmprojModel, ModelBase, TextModel, gguf
13
+
14
+
15
+ @ModelBase.register("MiMoV2FlashForCausalLM", "MiMoV2ForCausalLM")
16
+ class MimoV2Model(TextModel):
17
+ model_arch = gguf.MODEL_ARCH.MIMO2
18
+
19
+ # MiMo V2-Flash, V2.5 and V2.5-Pro all ship 3 trained MTP layers under model.mtp.layers.{0,1,2}.
20
+ # The HF config does not expose the count, so it's hardcoded to match the count found in the safetensors.
21
+ _n_nextn = 3
22
+
23
+ def __init__(self, *args, **kwargs):
24
+ super().__init__(*args, **kwargs)
25
+
26
+ self.block_count = self.hparams["num_hidden_layers"] + self._n_nextn
27
+ self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count)
28
+
29
+ @staticmethod
30
+ def _tp_aware_qkv_dequant(weight: Tensor, scale_inv: Tensor,
31
+ n_q: int, n_kv: int, hd: int, vhd: int,
32
+ bs: int = 128) -> Tensor:
33
+ # MiMo-V2.5 (TP=4) and V2.5-Pro (TP=8) ship qkv_proj sharded across TP
34
+ # ranks; per rank, rows are stacked as [Q_per | K_per | V_per].
35
+ # weight_scale_inv has ceil(rows_per_rank/bs) block-rows per rank (last
36
+ # may extend past rows_per_rank with phantom rows not in the weight).
37
+ # Naive repeat_interleave aligns rank 0 only and mis-applies scales to
38
+ # later ranks once rows_per_rank isn't a multiple of bs.
39
+ # Re-group the per-rank [Q_per|K_per|V_per] rows into a single fused
40
+ # [Q | K | V] tensor matching the un-sharded original layout.
41
+ q_size = n_q * hd
42
+ k_size = n_kv * hd
43
+ v_size = n_kv * vhd
44
+ total_rows = q_size + k_size + v_size
45
+ if weight.shape[0] != total_rows:
46
+ raise ValueError(f"qkv_proj weight rows {weight.shape[0]} != q+k+v {total_rows}")
47
+
48
+ # detect TP from scale_inv block count, descending order so larger matches first
49
+ tp = None
50
+ for cand in (8, 4):
51
+ if total_rows % cand != 0:
52
+ continue
53
+ rpr = total_rows // cand
54
+ bpr = (rpr + bs - 1) // bs
55
+ if scale_inv.shape[0] == cand * bpr:
56
+ tp = cand
57
+ break
58
+ if tp is None:
59
+ raise ValueError(
60
+ f"qkv_proj: cannot detect TP - scale_inv rows {scale_inv.shape[0]}, "
61
+ f"q+k+v {total_rows}")
62
+
63
+ q_per = q_size // tp
64
+ k_per = k_size // tp
65
+ v_per = v_size // tp
66
+ rows_per_rank = q_per + k_per + v_per
67
+ blocks_per_rank = (rows_per_rank + bs - 1) // bs
68
+
69
+ scale_inv = scale_inv.float()
70
+ # per-row scale-row index: rank * blocks_per_rank + (rr_in_rank // bs)
71
+ row_idx = torch.arange(total_rows)
72
+ rr = row_idx % rows_per_rank
73
+ rank = row_idx // rows_per_rank
74
+ scale_row_idx = rank * blocks_per_rank + (rr // bs)
75
+ # gather: (total_rows, n_col_blocks)
76
+ scale_per_row_block = scale_inv[scale_row_idx]
77
+ # expand col-blocks -> cols: each block-col covers `bs` weight cols
78
+ scale_full = scale_per_row_block.repeat_interleave(bs, dim=1)
79
+ # crop to weight col count (in case last col-block isn't full)
80
+ scale_full = scale_full[:, : weight.shape[1]]
81
+ dequant = weight.float() * scale_full
82
+
83
+ if tp == 1:
84
+ return dequant
85
+
86
+ # Re-group per-rank [Q_per|K_per|V_per] rows into unified [Q | K | V]
87
+ qs, ks, vs = [], [], []
88
+ for r in range(tp):
89
+ base = r * rows_per_rank
90
+ qs.append(dequant[base : base + q_per])
91
+ ks.append(dequant[base + q_per : base + q_per + k_per])
92
+ vs.append(dequant[base + q_per + k_per : base + rows_per_rank])
93
+ return torch.cat(qs + ks + vs, dim=0)
94
+
95
+ def dequant_model(self):
96
+ # Capture raw FP8 (weight, scale_inv) lambdas for qkv_proj BEFORE super
97
+ # rewrites them with the existing dequant. Replace super's lambda after
98
+ # it runs so scale_inv removal still happens via the standard path.
99
+ qkv_overrides: dict[str, tuple[Callable, Callable, int]] = {}
100
+ qc = self.hparams.get("quantization_config")
101
+ if isinstance(qc, dict) and qc.get("quant_method") == "fp8":
102
+ pat = re.compile(r"^model\.layers\.(\d+)\.self_attn\.qkv_proj\.weight_scale_inv$")
103
+ for name in list(self.model_tensors.keys()):
104
+ m = pat.match(name)
105
+ if not m:
106
+ continue
107
+ weight_name = name.removesuffix("_scale_inv")
108
+ if weight_name not in self.model_tensors:
109
+ continue
110
+ qkv_overrides[weight_name] = (
111
+ self.model_tensors[weight_name],
112
+ self.model_tensors[name],
113
+ int(m.group(1)),
114
+ )
115
+
116
+ super().dequant_model()
117
+
118
+ if not qkv_overrides:
119
+ return
120
+
121
+ n_q = self.hparams["num_attention_heads"]
122
+ hd = self.hparams["head_dim"]
123
+ vhd = self.hparams["v_head_dim"]
124
+ hybrid = self.hparams["hybrid_layer_pattern"]
125
+ n_layer_text = self.hparams["num_hidden_layers"]
126
+ for weight_name, (w_fn, s_fn, bid) in qkv_overrides.items():
127
+ # MTP layers (bid >= n_layer_text) use SWA-style attention dims
128
+ is_swa = True if bid >= n_layer_text else hybrid[bid] == 1
129
+ n_kv = self.hparams["swa_num_key_value_heads" if is_swa else "num_key_value_heads"]
130
+ self.model_tensors[weight_name] = (
131
+ lambda w_fn=w_fn, s_fn=s_fn, n_q=n_q, n_kv=n_kv, hd=hd, vhd=vhd:
132
+ MimoV2Model._tp_aware_qkv_dequant(w_fn(), s_fn(), n_q, n_kv, hd, vhd)
133
+ )
134
+
135
+ def set_gguf_parameters(self):
136
+ super().set_gguf_parameters()
137
+
138
+ assert self.hparams["swa_head_dim"] == self.hparams["head_dim"]
139
+ assert self.hparams["swa_num_attention_heads"] == self.hparams["num_attention_heads"]
140
+ assert self.hparams["swa_v_head_dim"] == self.hparams["v_head_dim"]
141
+ assert self.hparams["topk_method"] == "noaux_tc"
142
+
143
+ n_head_kv = self.hparams["num_key_value_heads"]
144
+ n_head_kv_swa = self.hparams["swa_num_key_value_heads"]
145
+ # Extend the per-layer pattern with SWA entries for the MTP blocks so the
146
+ # runtime arrays (sized to extended block_count) are fully populated.
147
+ hybrid = list(self.hparams["hybrid_layer_pattern"]) + [1] * self._n_nextn
148
+ n_head_kv_arr = [n_head_kv_swa if use_swa == 1 else n_head_kv for use_swa in hybrid]
149
+ self.gguf_writer.add_head_count_kv(n_head_kv_arr)
150
+
151
+ self.gguf_writer.add_sliding_window(self.hparams["sliding_window"])
152
+ self.gguf_writer.add_sliding_window_pattern(hybrid)
153
+ self.gguf_writer.add_value_length(self.hparams["v_head_dim"])
154
+ self.gguf_writer.add_expert_count(self.hparams["n_routed_experts"])
155
+ self.gguf_writer.add_expert_feed_forward_length(self.hparams["moe_intermediate_size"])
156
+
157
+ rope_dim = int(self.hparams["head_dim"] * self.rope_parameters["partial_rotary_factor"])
158
+ self.gguf_writer.add_rope_dimension_count(rope_dim)
159
+
160
+ self.gguf_writer.add_layer_norm_rms_eps(self.hparams.get("layernorm_epsilon", 1e-5))
161
+
162
+ v_scale = self.hparams.get("attention_value_scale")
163
+ if v_scale is not None:
164
+ self.gguf_writer.add_attn_value_scale(float(v_scale))
165
+
166
+ self.gguf_writer.add_nextn_predict_layers(self._n_nextn)
167
+
168
+ _experts: list[dict[str, Tensor]] | None = None
169
+
170
+ @classmethod
171
+ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
172
+ name, gen = item
173
+
174
+ if "attention_sink" in name and not name.endswith(".weight"):
175
+ name += ".weight"
176
+
177
+ return super().filter_tensors((name, gen))
178
+
179
+ def modify_tensors(self, data_torch, name, bid):
180
+ # Remap MTP/NextN tensors to additional layer slots so the standard tensor map handles them.
181
+ # HF: model.mtp.layers.{i}.foo -> model.layers.{n_layer_text + i}.foo
182
+ m = re.match(r"^model\.mtp\.layers\.(\d+)\.(.*)$", name)
183
+ if m is not None:
184
+ mtp_idx = int(m.group(1))
185
+ assert mtp_idx < self._n_nextn, f"MTP layer index {mtp_idx} >= _n_nextn ({self._n_nextn})"
186
+ rest = m.group(2)
187
+ n_layer_text = self.hparams["num_hidden_layers"]
188
+ new_bid = n_layer_text + mtp_idx
189
+ name = f"model.layers.{new_bid}.{rest}"
190
+ bid = new_bid
191
+
192
+ # process the experts separately
193
+ if name.find("mlp.experts") != -1:
194
+ n_experts = self.hparams["n_routed_experts"]
195
+ assert bid is not None
196
+
197
+ if self._experts is None:
198
+ self._experts = [{} for _ in range(self.block_count)]
199
+
200
+ self._experts[bid][name] = data_torch
201
+
202
+ if len(self._experts[bid]) >= n_experts * 3:
203
+ # merge the experts into a single 3d tensor
204
+ for w_name in ["gate_proj", "up_proj", "down_proj"]:
205
+ datas: list[Tensor] = []
206
+
207
+ for xid in range(n_experts):
208
+ ename_to_retrieve = f"model.layers.{bid}.mlp.experts.{xid}.{w_name}.weight"
209
+ datas.append(self._experts[bid][ename_to_retrieve])
210
+ del self._experts[bid][ename_to_retrieve]
211
+
212
+ data_torch = torch.stack(datas, dim=0)
213
+ merged_name = f"model.layers.{bid}.mlp.experts.{w_name}.weight"
214
+
215
+ yield from super().modify_tensors(data_torch, merged_name, bid)
216
+ return
217
+ else:
218
+ return
219
+ yield from super().modify_tensors(data_torch, name, bid)
220
+
221
+ def prepare_tensors(self):
222
+ super().prepare_tensors()
223
+
224
+ if self._experts is not None:
225
+ # flatten `list[dict[str, Tensor]]` into `list[str]`
226
+ experts = [k for d in self._experts for k in d.keys()]
227
+ if len(experts) > 0:
228
+ raise ValueError(f"Unprocessed experts: {experts}")
229
+
230
+
231
+ @ModelBase.register("MiMoV2ForCausalLM")
232
+ class MiMoV2VisionModel(MmprojModel):
233
+ def __init__(self, *args, **kwargs):
234
+ super().__init__(*args, **kwargs)
235
+ assert self.hparams_vision is not None
236
+ hp = self.hparams_vision
237
+
238
+ hp["image_size"] = hp.get("image_size", 560)
239
+ hp["num_attention_heads"] = hp.get("num_heads", 32)
240
+ hp["num_hidden_layers"] = hp.get("depth", 28)
241
+
242
+ self.n_q_heads = int(hp["num_heads"])
243
+ self.num_kv_heads = int(hp.get("num_key_value_heads", 8))
244
+ self.head_dim = int(hp.get("qk_channels", 64))
245
+ self.spatial_merge_size = int(hp["spatial_merge_size"])
246
+ # MiMoV2 vision RMSNorm: HF uses getattr(config, "rms_norm_eps", 1e-6) and the
247
+ # field is absent from MiMo-V2.5's vision_config
248
+ self.rms_norm_eps = float(hp.get("rms_norm_eps", 1e-6))
249
+
250
+ # fullatt_block_indexes are also reflected in vit_window_attn_types as -1
251
+ self.fullatt_block_indexes = list(hp.get("fullatt_block_indexes") or [])
252
+ self.vit_window_attn_types = list(hp.get("vit_window_attn_types") or [])
253
+ self.visual_token_window_size = int(hp.get("visual_token_window_size", -1))
254
+ self.use_sink = bool(hp.get("use_sink", False))
255
+
256
+ def set_gguf_parameters(self):
257
+ super().set_gguf_parameters()
258
+
259
+ self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.MIMOVL)
260
+ self.gguf_writer.add_vision_use_silu(True)
261
+ self.gguf_writer.add_vision_head_count_kv(self.num_kv_heads)
262
+ self.gguf_writer.add_vision_spatial_merge_size(self.spatial_merge_size)
263
+ self.gguf_writer.add_uint32(gguf.Keys.ClipVision.WINDOW_SIZE, self.visual_token_window_size)
264
+ self.gguf_writer.add_vision_wa_pattern_mode(self.vit_window_attn_types)
265
+ self.gguf_writer.add_vision_attention_layernorm_eps(self.rms_norm_eps)
266
+ self.gguf_writer.add_vision_min_pixels(int(self.preprocessor_config["min_pixels"]))
267
+ self.gguf_writer.add_vision_max_pixels(int(self.preprocessor_config["max_pixels"]))
268
+
269
+ def tensor_force_quant(self, name, new_name, bid, n_dims):
270
+ # Sinks must be F32: any sink-style softmax/mask add in ggml requires
271
+ # F32, and we fold sinks into a host-built F32 mask at encode time.
272
+ if new_name.endswith(".attn_sinks"):
273
+ return gguf.GGMLQuantizationType.F32
274
+ return super().tensor_force_quant(name, new_name, bid, n_dims)
275
+
276
+ @classmethod
277
+ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
278
+ name, _ = item
279
+ if not name.startswith("visual."):
280
+ return None
281
+ return super().filter_tensors(item)
282
+
283
+ def modify_tensors(self, data_torch, name, bid):
284
+ # Conv3D patch embed: split along the temporal axis (kt=2) into two Conv2D
285
+ # weights that the existing qwen2vl-style two-Conv2D path consumes.
286
+ if name == "visual.patch_embed.proj.weight":
287
+ _, _, kt, _, _ = data_torch.shape
288
+ if kt != 2:
289
+ raise ValueError(f"unexpected temporal_patch_size: {kt}")
290
+ embd_name = gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.V_ENC_EMBD_PATCH]
291
+ yield (embd_name + ".weight", data_torch[:, :, 0, ...])
292
+ yield (embd_name + ".weight.1", data_torch[:, :, 1, ...])
293
+ return
294
+
295
+ yield from super().modify_tensors(data_torch, name, bid)
llama.cpp/conversion/minicpm.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable, Iterable, TYPE_CHECKING
4
+
5
+ import torch
6
+
7
+ if TYPE_CHECKING:
8
+ from torch import Tensor
9
+
10
+ from .base import MmprojModel, ModelBase, TextModel, gguf, logger
11
+
12
+ from .llama import LlamaModel
13
+ from .qwen import Qwen3_5TextModel
14
+
15
+
16
+ @ModelBase.register("MiniCPMForCausalLM")
17
+ class MiniCPMModel(TextModel):
18
+ model_arch = gguf.MODEL_ARCH.MINICPM
19
+
20
+ def set_gguf_parameters(self):
21
+ super().set_gguf_parameters()
22
+ embedding_scale = float(self.hparams["scale_emb"])
23
+ self.gguf_writer.add_embedding_scale(embedding_scale)
24
+ logger.info(f"gguf: (minicpm) embedding_scale = {embedding_scale}")
25
+ residual_scale = self.hparams["scale_depth"] / self.hparams["num_hidden_layers"] ** 0.5
26
+ self.gguf_writer.add_residual_scale(residual_scale)
27
+ logger.info(f"gguf: (minicpm) residual_scale = {residual_scale}")
28
+ logit_scale = self.hparams["hidden_size"] / self.hparams["dim_model_base"]
29
+ self.gguf_writer.add_logit_scale(logit_scale)
30
+ logger.info(f"gguf: (minicpm) logit_scale = {logit_scale}")
31
+
32
+ def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]:
33
+ rope_dims = self.hparams["hidden_size"] // self.hparams["num_attention_heads"]
34
+
35
+ long_factors = self.rope_parameters.get('long_factor')
36
+ short_factors = self.rope_parameters.get('short_factor')
37
+ if long_factors or short_factors:
38
+ if long_factors is None or short_factors is None:
39
+ raise KeyError('Missing the required key rope_scaling.long_factor or rope_scaling_short_factor')
40
+
41
+ if len(long_factors) != len(short_factors) or len(long_factors) != rope_dims / 2:
42
+ raise ValueError(f'The length of rope long and short factors must be {rope_dims / 2}')
43
+
44
+ yield (self.format_tensor_name(gguf.MODEL_TENSOR.ROPE_FACTORS_LONG), torch.tensor(long_factors, dtype=torch.float32))
45
+ yield (self.format_tensor_name(gguf.MODEL_TENSOR.ROPE_FACTORS_SHORT), torch.tensor(short_factors, dtype=torch.float32))
46
+
47
+ def set_vocab(self):
48
+ self._set_vocab_sentencepiece()
49
+
50
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
51
+ n_head = self.hparams["num_attention_heads"]
52
+ n_kv_head = self.hparams.get("num_key_value_heads")
53
+
54
+ # HF models permute some of the tensors, so we need to undo that
55
+ if name.endswith(("q_proj.weight")):
56
+ data_torch = LlamaModel.permute(data_torch, n_head, n_head)
57
+ if name.endswith(("k_proj.weight")):
58
+ data_torch = LlamaModel.permute(data_torch, n_head, n_kv_head)
59
+
60
+ yield from super().modify_tensors(data_torch, name, bid)
61
+
62
+
63
+ @ModelBase.register("MiniCPM3ForCausalLM")
64
+ class MiniCPM3Model(TextModel):
65
+ model_arch = gguf.MODEL_ARCH.MINICPM3
66
+
67
+ def set_gguf_parameters(self):
68
+ hparams = self.hparams
69
+
70
+ self.gguf_writer.add_file_type(self.ftype)
71
+ self.gguf_writer.add_context_length(hparams["max_position_embeddings"])
72
+ self.gguf_writer.add_embedding_length(hparams["hidden_size"])
73
+ self.gguf_writer.add_block_count(self.block_count)
74
+ self.gguf_writer.add_feed_forward_length(hparams["intermediate_size"])
75
+ self.gguf_writer.add_head_count(hparams["num_attention_heads"])
76
+ self.gguf_writer.add_head_count_kv(hparams["num_key_value_heads"])
77
+ self.gguf_writer.add_layer_norm_rms_eps(hparams["rms_norm_eps"])
78
+ self.gguf_writer.add_vocab_size(hparams["vocab_size"])
79
+ if "q_lora_rank" in hparams and hparams["q_lora_rank"] is not None:
80
+ self.gguf_writer.add_q_lora_rank(hparams["q_lora_rank"])
81
+ self.gguf_writer.add_kv_lora_rank(hparams["kv_lora_rank"])
82
+ self.gguf_writer.add_key_length(hparams["qk_nope_head_dim"] + hparams["qk_rope_head_dim"])
83
+ self.gguf_writer.add_rope_dimension_count(hparams["qk_rope_head_dim"])
84
+
85
+ def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]:
86
+ long_factors = self.rope_parameters.get('long_factor')
87
+ short_factors = self.rope_parameters.get('short_factor')
88
+ if long_factors or short_factors:
89
+ rope_dims = self.hparams["qk_rope_head_dim"]
90
+
91
+ if long_factors is None or short_factors is None:
92
+ raise KeyError('Missing the required key rope_scaling.long_factor or rope_scaling_short_factor')
93
+
94
+ if len(long_factors) != len(short_factors) or len(long_factors) != rope_dims / 2:
95
+ raise ValueError(f'The length of rope long and short factors must be {rope_dims / 2}')
96
+
97
+ yield (self.format_tensor_name(gguf.MODEL_TENSOR.ROPE_FACTORS_LONG), torch.tensor(long_factors, dtype=torch.float32))
98
+ yield (self.format_tensor_name(gguf.MODEL_TENSOR.ROPE_FACTORS_SHORT), torch.tensor(short_factors, dtype=torch.float32))
99
+
100
+ def set_vocab(self):
101
+ self._set_vocab_sentencepiece()
102
+
103
+ def _reverse_hf_permute(self, weights: Tensor, n_head: int, n_kv_head: int | None = None) -> Tensor:
104
+ if n_kv_head is not None and n_head != n_kv_head:
105
+ n_head //= n_kv_head
106
+
107
+ return (
108
+ weights.reshape(n_head, 2, weights.shape[0] // n_head // 2, *weights.shape[1:])
109
+ .swapaxes(1, 2)
110
+ .reshape(weights.shape)
111
+ )
112
+
113
+
114
+ # MiniCPM-V 4.6: text tower is Qwen3.5 (linear+full hybrid attention) wrapped under
115
+ # `model.language_model.*`; vision tower is SigLIP + a window-attention ViT merger
116
+ # + a final DownsampleMLP merger. The same HF arch is registered twice below: once as
117
+ # the LM (text mode) and once as the mmproj (vision mode), mirroring the Qwen3-VL setup.
118
+
119
+ @ModelBase.register("MiniCPMV4_6ForConditionalGeneration")
120
+ class MiniCPMV4_6TextModel(Qwen3_5TextModel):
121
+ model_arch = gguf.MODEL_ARCH.QWEN35
122
+
123
+ @classmethod
124
+ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
125
+ name, gen = item
126
+
127
+ if name.startswith("model.merger."):
128
+ return None
129
+ # MTP tensors are not used at inference yet; align with Qwen3Next behaviour
130
+ if name.startswith("mtp"):
131
+ return None
132
+
133
+ return super().filter_tensors(item)
134
+
135
+
136
+ @ModelBase.register("MiniCPMV4_6ForConditionalGeneration")
137
+ class MiniCPMV4_6VisionModel(MmprojModel):
138
+ def __init__(self, *args, **kwargs):
139
+ super().__init__(*args, **kwargs)
140
+ if self.hparams_vision is not None:
141
+ # In MiniCPM-V 4.6 `vision_config.image_size` (980) describes the SigLIP
142
+ # positional embedding bucket grid (70 x 70), while the per-slice processing
143
+ # resolution is the preprocessor's `scale_resolution` (typically 448).
144
+ # The CLIP loader in tools/mtmd/clip.cpp consumes `clip.vision.image_size`
145
+ # as the slice size and warmup resolution, so report `scale_resolution` there
146
+ # to match the upstream MiniCPMV4_6ImageProcessorPil slicing rules.
147
+ scale_resolution = self.preprocessor_config.get("scale_resolution")
148
+ if scale_resolution is not None:
149
+ self.hparams_vision["image_size"] = int(scale_resolution)
150
+
151
+ def set_gguf_parameters(self):
152
+ super().set_gguf_parameters()
153
+ assert self.hparams_vision is not None
154
+
155
+ # projector type string is consumed by clip_projector_type_from_string() in clip.cpp
156
+ # (mapped to PROJECTOR_TYPE_MINICPMV4_6).
157
+ self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.MINICPMV4_6)
158
+
159
+ # ViT merger 2x2 + final merger 2x2 = 4x spatial merge per dimension; used for slice alignment
160
+ self.gguf_writer.add_vision_projector_scale_factor(4)
161
+
162
+ # borrow wa_layer_indexes for vit_merger insertion point
163
+ insert_layer_id = int(self.global_config.get(
164
+ "insert_layer_id", self.hparams_vision.get("insert_layer_id", 6)))
165
+ self.gguf_writer.add_vision_wa_layer_indexes([insert_layer_id])
166
+
167
+ # SigLIP vision body uses gelu_pytorch_tanh, which matches ggml_gelu (tanh approx).
168
+ self.gguf_writer.add_vision_use_gelu(True)
169
+ self.gguf_writer.add_vision_attention_layernorm_eps(
170
+ self.hparams_vision.get("layer_norm_eps", 1e-6))
171
+
172
+ @classmethod
173
+ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
174
+ name, gen = item
175
+
176
+ # lm_head / MTP -> belong to the LM file
177
+ if name.startswith(("lm_head.", "mtp")):
178
+ return None
179
+
180
+ return super().filter_tensors(item)
llama.cpp/conversion/minimax.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ import torch
6
+
7
+ if TYPE_CHECKING:
8
+ from torch import Tensor
9
+
10
+ from .base import ModelBase, TextModel, gguf
11
+
12
+
13
+ @ModelBase.register("MiniMaxM2ForCausalLM")
14
+ class MiniMaxM2Model(TextModel):
15
+ model_arch = gguf.MODEL_ARCH.MINIMAXM2
16
+ _experts_cache: dict[int, dict[str, Tensor]] = {}
17
+
18
+ def set_gguf_parameters(self):
19
+ super().set_gguf_parameters()
20
+
21
+ self.gguf_writer.add_expert_feed_forward_length(self.find_hparam(["intermediate_size"]))
22
+ self.gguf_writer.add_rope_dimension_count(self.find_hparam(["rotary_dim"]))
23
+
24
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None):
25
+ # merge expert weights
26
+ if 'experts' in name:
27
+ n_experts = self.find_hparam(["num_local_experts", "num_experts"])
28
+ assert bid is not None
29
+
30
+ expert_cache = self._experts_cache.setdefault(bid, {})
31
+ expert_cache[name] = data_torch
32
+ expert_weights = ["w1", "w2", "w3"]
33
+
34
+ # not enough expert weights to merge
35
+ if len(expert_cache) < n_experts * len(expert_weights):
36
+ return
37
+
38
+ for w_name in expert_weights:
39
+ datas: list[Tensor] = []
40
+
41
+ for xid in range(n_experts):
42
+ ename = f"model.layers.{bid}.block_sparse_moe.experts.{xid}.{w_name}.weight"
43
+ datas.append(expert_cache[ename])
44
+ del expert_cache[ename]
45
+
46
+ data_torch = torch.stack(datas, dim=0)
47
+ merged_name = f"model.layers.{bid}.block_sparse_moe.experts.{w_name}.weight"
48
+ new_name = self.map_tensor_name(merged_name)
49
+ yield from super().modify_tensors(data_torch, new_name, bid)
50
+
51
+ del self._experts_cache[bid]
52
+ return
53
+
54
+ yield from super().modify_tensors(data_torch, name, bid)
llama.cpp/conversion/mistral.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Callable, TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from torch import Tensor
8
+
9
+ from .base import MistralTokenizerType, MistralVocab, _mistral_common_installed, _mistral_import_error_msg, gguf, logger
10
+
11
+ from .deepseek import DeepseekV2Model
12
+ from .llama import LlamaModel
13
+
14
+ if _mistral_common_installed:
15
+ from mistral_common.tokens.tokenizers.base import TokenizerVersion # type: ignore[import-not-found, ty:unresolved-import]
16
+ from mistral_common.tokens.tokenizers.tekken import Tekkenizer # type: ignore[import-not-found, ty:unresolved-import]
17
+ from mistral_common.tokens.tokenizers.sentencepiece import SentencePieceTokenizer # type: ignore[import-not-found, ty:unresolved-import]
18
+ else:
19
+ TokenizerVersion = None # type: ignore[assignment]
20
+ Tekkenizer = None # type: ignore[assignment]
21
+ SentencePieceTokenizer = None # type: ignore[assignment]
22
+
23
+
24
+ class MistralModel(LlamaModel):
25
+ model_arch = gguf.MODEL_ARCH.MISTRAL3
26
+ model_name = "Mistral"
27
+ hf_arch = ""
28
+ is_mistral_format = True
29
+ undo_permute = False
30
+
31
+ def __init__(self, *args, **kwargs):
32
+ super().__init__(*args, **kwargs)
33
+ # for compatibility, we use LLAMA arch for older models
34
+ # TODO: remove this once everyone migrates to newer version of llama.cpp
35
+ if "llama_4_scaling" not in self.hparams:
36
+ self.model_arch = gguf.MODEL_ARCH.LLAMA
37
+ self.gguf_writer.arch = gguf.MODEL_ARCH_NAMES[self.model_arch]
38
+ self.gguf_writer.add_architecture()
39
+ self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count)
40
+
41
+ def dequant_model(self):
42
+ # transform quantization config into HF format
43
+ quant_config = self.hparams.get("quantization")
44
+ if quant_config is not None:
45
+ assert quant_config["qformat_weight"] == "fp8_e4m3"
46
+ self.hparams["quantization_config"] = {
47
+ "activation_scheme": "static",
48
+ "quant_method": "fp8",
49
+ "weight_block_size": None,
50
+ }
51
+ return super().dequant_model()
52
+
53
+ @staticmethod
54
+ def get_community_chat_template(vocab: MistralVocab, templates_dir: Path, is_mistral_format: bool):
55
+ assert TokenizerVersion is not None and Tekkenizer is not None and SentencePieceTokenizer is not None, _mistral_import_error_msg
56
+ assert isinstance(vocab.tokenizer, (Tekkenizer, SentencePieceTokenizer)), (
57
+ f"Expected Tekkenizer or SentencePieceTokenizer, got {type(vocab.tokenizer)}"
58
+ )
59
+
60
+ if vocab.tokenizer.version == TokenizerVersion.v1:
61
+ return "mistral-v1"
62
+ elif vocab.tokenizer.version == TokenizerVersion.v3 and vocab.tokenizer_type == MistralTokenizerType.spm:
63
+ return "mistral-v3"
64
+ elif vocab.tokenizer.version == TokenizerVersion.v3 and vocab.tokenizer_type == MistralTokenizerType.tekken:
65
+ return "mistral-v3-tekken"
66
+ elif vocab.tokenizer.version == TokenizerVersion.v7 and vocab.tokenizer_type == MistralTokenizerType.spm:
67
+ return "mistral-v7"
68
+ elif vocab.tokenizer.version == TokenizerVersion.v7 and vocab.tokenizer_type == MistralTokenizerType.tekken:
69
+ return "mistral-v7-tekken"
70
+ elif vocab.tokenizer.version == TokenizerVersion.v11:
71
+ template_file = "Mistral-Small-3.2-24B-Instruct-2506.jinja"
72
+ elif vocab.tokenizer.version == TokenizerVersion.v13:
73
+ template_file = "unsloth-mistral-Devstral-Small-2507.jinja"
74
+ else:
75
+ err_message = f"Unknown tokenizer type: {vocab.tokenizer_type} and version {vocab.tokenizer.version}"
76
+ if is_mistral_format:
77
+ err_message += (
78
+ " . Please pass --disable-mistral-community-chat-template argument to the CLI "
79
+ "if you want to skip this error and use the Mistral official `mistral-common` pre-processing library."
80
+ )
81
+ raise ValueError(err_message)
82
+
83
+ template_path = templates_dir / template_file
84
+ if not template_path.exists():
85
+ raise FileNotFoundError(f"Template file not found: {template_path}")
86
+
87
+ with open(template_path, "r", encoding="utf-8") as f:
88
+ template = f.read()
89
+
90
+ return template
91
+
92
+ def set_gguf_parameters(self):
93
+ super().set_gguf_parameters()
94
+ MistralModel.set_mistral_config(self.gguf_writer, self.hparams)
95
+
96
+ @staticmethod
97
+ def set_mistral_config(gguf_writer: gguf.GGUFWriter, hparams: dict):
98
+ if "yarn" in hparams:
99
+ yarn_params = hparams["yarn"]
100
+ mscale_all_dim = 1.0 if not yarn_params["apply_scale"] else 0.0
101
+ gguf_writer.add_rope_scaling_type(gguf.RopeScalingType.YARN)
102
+ gguf_writer.add_rope_scaling_factor(yarn_params["factor"])
103
+ gguf_writer.add_rope_scaling_yarn_beta_fast(yarn_params["beta"])
104
+ gguf_writer.add_rope_scaling_yarn_beta_slow(yarn_params["alpha"])
105
+ gguf_writer.add_rope_scaling_yarn_log_mul(mscale_all_dim)
106
+ gguf_writer.add_rope_scaling_orig_ctx_len(yarn_params["original_max_position_embeddings"])
107
+
108
+ llama_4_scaling = hparams.get("llama_4_scaling")
109
+ if llama_4_scaling is not None:
110
+ gguf_writer.add_attn_temperature_scale(llama_4_scaling["beta"])
111
+
112
+
113
+ class MistralMoeModel(DeepseekV2Model):
114
+ model_arch = gguf.MODEL_ARCH.DEEPSEEK2
115
+ model_name = "Mistral"
116
+ hf_arch = ""
117
+ is_mistral_format = True
118
+
119
+ def __init__(self, *args, **kwargs):
120
+ super().__init__(*args, **kwargs)
121
+ logger.info("Using MistralMoeModel")
122
+ # remap hparams from Mistral MoE format to DeepseekV2 format
123
+ # we do this way to be able to reuse DeepseekV2Model set_gguf_parameters logic
124
+ # ref: https://github.com/vllm-project/vllm/blob/b294e28db2c5dee61bc25157664edcada8b90b31/vllm/transformers_utils/configs/mistral.py
125
+ config = self.hparams
126
+ # Mistral key -> HF key
127
+ config_mapping = {
128
+ "dim": "hidden_size",
129
+ "norm_eps": "rms_norm_eps",
130
+ "n_kv_heads": "num_key_value_heads",
131
+ "n_layers": "num_hidden_layers",
132
+ "n_heads": "num_attention_heads",
133
+ "hidden_dim": "intermediate_size",
134
+ }
135
+ # HF key -> (Mistral key, default value)
136
+ top_level_mapping_with_default = {
137
+ "model_type": ("model_type", "transformer"),
138
+ "hidden_act": ("activation", "silu"),
139
+ "tie_word_embeddings": ("tied_embeddings", False),
140
+ "max_seq_len": ("max_seq_len", config.get("max_position_embeddings", 128_000)),
141
+ "max_position_embeddings": ("max_position_embeddings", 128_000),
142
+ }
143
+ # mapping top-level keys
144
+ for key, new_key in config_mapping.items():
145
+ if key in config:
146
+ config[new_key] = config[key]
147
+ for new_key, (key, default_value) in top_level_mapping_with_default.items():
148
+ config[new_key] = config.get(key, default_value)
149
+ # mapping MoE-specific keys
150
+ moe_config_map = {
151
+ "route_every_n": "moe_layer_freq",
152
+ "first_k_dense_replace": "first_k_dense_replace",
153
+ "num_experts_per_tok": "num_experts_per_tok",
154
+ "num_experts": "n_routed_experts",
155
+ "expert_hidden_dim": "moe_intermediate_size",
156
+ "routed_scale": "routed_scaling_factor",
157
+ "num_shared_experts": "n_shared_experts",
158
+ "num_expert_groups": "n_group",
159
+ "num_expert_groups_per_tok": "topk_group",
160
+ }
161
+ moe = config["moe"]
162
+ for key, new_key in moe_config_map.items():
163
+ if key in moe:
164
+ config[new_key] = moe[key]
165
+ # provide missing values
166
+ config["topk_method"] = None
167
+ config["norm_topk_prob"] = True
168
+ config["scoring_func"] = "softmax"
169
+
170
+ def set_vocab(self):
171
+ self._set_vocab_mistral()
172
+
173
+ def set_gguf_parameters(self):
174
+ super().set_gguf_parameters()
175
+ MistralModel.set_mistral_config(self.gguf_writer, self.hparams)
176
+ yarn_params = self.hparams["yarn"]
177
+ self.gguf_writer.add_attn_temperature_length(yarn_params["original_max_position_embeddings"])
178
+
179
+ # [TAG_DEEPSEEK2_YARN_LOG_MUL_FIX]
180
+ # note: for legacy reasons, this is not consistent with the other usages of self.gguf_writer.add_rope_scaling_yarn_log_mul
181
+ # ref https://github.com/ggml-org/llama.cpp/pull/17945
182
+ self.gguf_writer.add_rope_scaling_yarn_log_mul(0.1) # mscale_all_dim * 0.1
183
+
184
+ @classmethod
185
+ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
186
+ name, gen = item
187
+
188
+ # rename certain tensors so that we can reuse DeepseekV2Model modify_tensors logic
189
+ if name.endswith(".qscale_act"):
190
+ name = name.replace(".qscale_act", ".input_scale")
191
+ if name.endswith(".qscale_weight"):
192
+ name = name.replace(".qscale_weight", ".weight_scale")
193
+ if ".wkv_b." in name:
194
+ name = name.replace(".wkv_b.", ".kv_b_proj.")
195
+ if ".experts." in name:
196
+ name = name.replace(".experts.", ".mlp.experts.")
197
+ name = name.replace(".w1.", ".gate_proj.")
198
+ name = name.replace(".w2.", ".down_proj.")
199
+ name = name.replace(".w3.", ".up_proj.")
200
+ name = "model." + name
201
+
202
+ return super().filter_tensors((name, gen))
llama.cpp/conversion/mistral3.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from torch import Tensor
7
+
8
+ from .base import ModelBase, TextModel, gguf
9
+
10
+ from .deepseek import DeepseekV2Model
11
+ from .llama import LlamaModel
12
+
13
+
14
+ @ModelBase.register(
15
+ "Mistral3ForConditionalGeneration",
16
+ "Ministral3ForCausalLM",
17
+ )
18
+ class Mistral3Model(TextModel):
19
+ class Ministral3Model(LlamaModel):
20
+ model_arch = gguf.MODEL_ARCH.MISTRAL3
21
+
22
+ def set_gguf_parameters(self):
23
+ super().set_gguf_parameters()
24
+ rope_params = self.rope_parameters
25
+ if self.hparams.get("model_type") == "ministral3":
26
+ assert rope_params, "ministral3 must have 'rope_parameters' config"
27
+ assert rope_params["rope_type"] == "yarn", "ministral3 rope_type must be 'yarn'"
28
+ self.gguf_writer.add_rope_scaling_yarn_log_mul(rope_params["mscale_all_dim"])
29
+ self.gguf_writer.add_attn_temperature_scale(rope_params["llama_4_scaling_beta"])
30
+
31
+ class Mistral4Model(DeepseekV2Model):
32
+ model_arch = gguf.MODEL_ARCH.MISTRAL4
33
+ skip_mtp = False # model contains no MTP layers, so no need to skip
34
+ merge_expert = False # experts are already stacked as 3D
35
+
36
+ def modify_tensors(self, data_torch, name, bid):
37
+ if name.endswith(".down_proj") or name.endswith(".gate_up_proj"):
38
+ name = name + ".weight"
39
+ yield from super().modify_tensors(data_torch, name, bid)
40
+
41
+ model_arch = gguf.MODEL_ARCH.MISTRAL3 # unused
42
+ impl: TextModel
43
+
44
+ def __init__(self, *args, **kwargs):
45
+ super().__init__(*args, **kwargs)
46
+ if self.hparams.get("model_type") == "mistral4":
47
+ self.impl = Mistral3Model.Mistral4Model(*args, **kwargs)
48
+ else:
49
+ self.impl = Mistral3Model.Ministral3Model(*args, **kwargs)
50
+
51
+ def set_vocab(self):
52
+ self.impl.set_vocab()
53
+
54
+ def set_gguf_parameters(self):
55
+ self.impl.set_gguf_parameters()
56
+
57
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None):
58
+ yield from self.impl.modify_tensors(data_torch, name, bid)
59
+
60
+ def prepare_tensors(self):
61
+ self.impl.prepare_tensors()
62
+
63
+ def write_vocab(self):
64
+ self.impl.write_vocab()
65
+
66
+ def write(self):
67
+ self.impl.write()
llama.cpp/conversion/mpt.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Iterable, TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from torch import Tensor
7
+
8
+ from .base import ModelBase, TextModel, gguf
9
+
10
+
11
+ @ModelBase.register("MPTForCausalLM")
12
+ class MPTModel(TextModel):
13
+ model_arch = gguf.MODEL_ARCH.MPT
14
+
15
+ def set_vocab(self):
16
+ try:
17
+ self._set_vocab_gpt2()
18
+ except Exception:
19
+ # Fallback for SEA-LION model
20
+ self._set_vocab_sentencepiece()
21
+ self.gguf_writer.add_add_bos_token(False)
22
+ self.gguf_writer.add_pad_token_id(3)
23
+ self.gguf_writer.add_eos_token_id(1)
24
+ self.gguf_writer.add_unk_token_id(0)
25
+
26
+ def set_gguf_parameters(self):
27
+ self.gguf_writer.add_context_length(self.hparams["max_seq_len"])
28
+ self.gguf_writer.add_embedding_length(self.hparams["d_model"])
29
+ self.gguf_writer.add_block_count(self.block_count)
30
+ self.gguf_writer.add_feed_forward_length(4 * self.hparams["d_model"])
31
+ self.gguf_writer.add_head_count(self.hparams["n_heads"])
32
+ if kv_n_heads := self.hparams["attn_config"].get("kv_n_heads"):
33
+ self.gguf_writer.add_head_count_kv(kv_n_heads)
34
+ self.gguf_writer.add_layer_norm_eps(1e-5)
35
+ if self.hparams["attn_config"]["clip_qkv"] is not None:
36
+ self.gguf_writer.add_clamp_kqv(self.hparams["attn_config"]["clip_qkv"])
37
+ if self.hparams["attn_config"]["alibi"]:
38
+ self.gguf_writer.add_max_alibi_bias(self.hparams["attn_config"]["alibi_bias_max"])
39
+ else:
40
+ self.gguf_writer.add_max_alibi_bias(0.0)
41
+
42
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
43
+ if "scales" in name:
44
+ new_name = self.map_tensor_name(name, try_suffixes=(".weight", ".bias", ".scales"))
45
+ new_name = new_name.replace("scales", "act.scales")
46
+ else:
47
+ new_name = self.map_tensor_name(name, try_suffixes=(".weight", ".bias"))
48
+
49
+ yield from super().modify_tensors(data_torch, new_name, bid)
llama.cpp/conversion/nemotron.py ADDED
@@ -0,0 +1,385 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Iterable, TYPE_CHECKING
4
+
5
+ import torch
6
+
7
+ if TYPE_CHECKING:
8
+ from torch import Tensor
9
+
10
+ from .base import MmprojModel, ModelBase, TextModel, gguf, logger
11
+
12
+ from .granite import GraniteHybridModel
13
+
14
+
15
+ @ModelBase.register(
16
+ "NemotronH_Nano_VL_V2",
17
+ "RADIOModel",
18
+ )
19
+ class NemotronNanoV2VLModel(MmprojModel):
20
+ # ViT-Huge architecture parameters for RADIO v2.5-h
21
+ _vit_hidden_size = 1280
22
+ _vit_intermediate_size = 5120
23
+ _vit_num_layers = 32
24
+ _vit_num_heads = 16
25
+
26
+ def get_vision_config(self) -> dict[str, Any] | None:
27
+ # RADIO config doesn't have standard ViT parameters, so they need to be constructed manually
28
+ vision_config = self.global_config.get("vision_config")
29
+ if vision_config is None:
30
+ return None
31
+ # Add ViT-H parameters
32
+ vision_config = {
33
+ **vision_config,
34
+ "hidden_size": self._vit_hidden_size,
35
+ "intermediate_size": self._vit_intermediate_size,
36
+ "num_hidden_layers": self._vit_num_layers,
37
+ "num_attention_heads": self._vit_num_heads,
38
+ "image_size": self.global_config.get("force_image_size", 512),
39
+ }
40
+ return vision_config
41
+
42
+ def set_gguf_parameters(self):
43
+ if "image_mean" not in self.preprocessor_config:
44
+ self.preprocessor_config["image_mean"] = [0.485, 0.456, 0.406]
45
+ if "image_std" not in self.preprocessor_config:
46
+ self.preprocessor_config["image_std"] = [0.229, 0.224, 0.225]
47
+
48
+ super().set_gguf_parameters()
49
+ hparams = self.global_config
50
+ self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.NEMOTRON_V2_VL)
51
+ self.gguf_writer.add_vision_attention_layernorm_eps(1e-6)
52
+ self.gguf_writer.add_vision_use_gelu(True)
53
+ downsample_ratio = hparams.get("downsample_ratio", 0.5)
54
+ self.gguf_writer.add_vision_projector_scale_factor(int(1.0 / downsample_ratio))
55
+
56
+ def tensor_force_quant(self, name, new_name, bid, n_dims):
57
+ if ".position_embd." in new_name or "pos_embed" in new_name:
58
+ return gguf.GGMLQuantizationType.F32
59
+ return super().tensor_force_quant(name, new_name, bid, n_dims)
60
+
61
+ @classmethod
62
+ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
63
+ name, gen = item
64
+
65
+ if "input_conditioner" in name:
66
+ return None
67
+
68
+ # mtmd does not support video yet so skip tensors related to video.
69
+ if "radio_model.model.patch_generator.video_embedder" in name:
70
+ return None
71
+
72
+ if not name.startswith("vision_model.radio_model.model.") and not name.startswith("mlp1."):
73
+ return None
74
+
75
+ if "patch_generator.pos_embed" in name:
76
+ if not name.endswith(".weight"):
77
+ name += ".weight"
78
+
79
+ return super().filter_tensors((name, gen))
80
+
81
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
82
+ # RADIO's pos_embed doesn't have .weight suffix, but clip.cpp expects it
83
+ if "patch_generator.pos_embed" in name:
84
+ # Downsample position embeddings for fixed 512x512 image size
85
+ import torch.nn.functional as F
86
+ n_embd = self.hparams["hidden_size"]
87
+ image_size = self.global_config.get("force_image_size", 512)
88
+ patch_size = self.hparams["patch_size"]
89
+ target_patches_per_side = image_size // patch_size # 32
90
+ max_patches_per_side = int((data_torch.shape[1]) ** 0.5) # 128
91
+ if target_patches_per_side != max_patches_per_side:
92
+ # Reshape to grid, interpolate, flatten back
93
+ data_torch = data_torch.reshape(1, max_patches_per_side, max_patches_per_side, n_embd)
94
+ data_torch = data_torch.permute(0, 3, 1, 2).float() # [1, n_embd, 128, 128]
95
+ data_torch = F.interpolate(data_torch, size=(target_patches_per_side, target_patches_per_side),
96
+ mode='bilinear', align_corners=True)
97
+ data_torch = data_torch.permute(0, 2, 3, 1) # [1, 32, 32, n_embd]
98
+ data_torch = data_torch.reshape(1, target_patches_per_side * target_patches_per_side, n_embd)
99
+
100
+ # Reshape linear patch embedding to conv2d format for ggml_conv_2d
101
+ # From [n_embd, patch_size*patch_size*3] to [n_embd, 3, patch_size, patch_size]
102
+ if "patch_generator.embedder" in name:
103
+ patch_size = self.hparams["patch_size"]
104
+ n_embd = self.hparams["hidden_size"]
105
+ data_torch = data_torch.reshape(n_embd, 3, patch_size, patch_size)
106
+
107
+ yield from super().modify_tensors(data_torch, name, bid)
108
+
109
+
110
+ @ModelBase.register("NemotronForCausalLM")
111
+ class NemotronModel(TextModel):
112
+ model_arch = gguf.MODEL_ARCH.NEMOTRON
113
+
114
+ def set_vocab(self):
115
+ self._set_vocab_sentencepiece()
116
+ self.gguf_writer.add_pad_token_id(0)
117
+ self.gguf_writer.add_unk_token_id(1)
118
+
119
+ def set_gguf_parameters(self):
120
+ super().set_gguf_parameters()
121
+ hparams = self.hparams
122
+ self.gguf_writer.add_vocab_size(hparams["vocab_size"])
123
+
124
+ f_norm_eps = self.find_hparam(["layer_norm_eps", "layer_norm_epsilon", "norm_epsilon", "norm_eps"])
125
+ self.gguf_writer.add_layer_norm_eps(f_norm_eps)
126
+
127
+ # * Partial RoPE
128
+ rot_pct = self.rope_parameters["partial_rotary_factor"]
129
+ n_embd = self.find_hparam(["hidden_size", "n_embd"])
130
+ n_head = self.find_hparam(["num_attention_heads", "n_head"])
131
+ self.gguf_writer.add_rope_dimension_count(int(rot_pct * n_embd) // n_head)
132
+
133
+ # * RopeScaling for Nemotron
134
+ factor = self.hparams.get("factor") or self.rope_parameters.get("factor")
135
+ if factor is None:
136
+ self.gguf_writer.add_rope_scaling_type(gguf.RopeScalingType.NONE)
137
+ else:
138
+ self.gguf_writer.add_rope_scaling_type(gguf.RopeScalingType.LINEAR)
139
+ self.gguf_writer.add_rope_scaling_factor(factor)
140
+
141
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
142
+ # * Adding +1 to LayerNorm's weights here to implement layernorm1p w/o changing anything on the GGML engine side
143
+ # model.layers.{l}.input_layernorm.weight
144
+ # model.layers.{l}.post_attention_layernorm.weight
145
+ # model.norm.weight
146
+ if name.endswith("norm.weight"):
147
+ data_torch = data_torch + 1
148
+
149
+ yield from super().modify_tensors(data_torch, name, bid)
150
+
151
+
152
+ @ModelBase.register("NemotronHForCausalLM")
153
+ class NemotronHModel(GraniteHybridModel):
154
+ """Hybrid mamba2/attention model from NVIDIA"""
155
+ model_arch = gguf.MODEL_ARCH.NEMOTRON_H
156
+ is_moe: bool = False
157
+
158
+ def __init__(self, *args, **kwargs):
159
+ # We have to determine the correct model architecture (MoE vs non-MoE) before
160
+ # calling the parent __init__. This is because the parent constructor
161
+ # uses self.model_arch to build the tensor name map, and all MoE-specific
162
+ # mappings would be missed if it were called with the default non-MoE arch.
163
+ hparams = ModelBase.load_hparams(args[0], self.is_mistral_format)
164
+ has_moe_params = (
165
+ "num_experts_per_tok" in hparams
166
+ or (isinstance(hparams.get("llm_config"), dict) and "num_experts_per_tok" in hparams["llm_config"])
167
+ )
168
+ if has_moe_params:
169
+ self.model_arch = gguf.MODEL_ARCH.NEMOTRON_H_MOE
170
+ self.is_moe = True
171
+
172
+ super().__init__(*args, **kwargs)
173
+
174
+ # Save the top-level head_dim for later
175
+ self.head_dim = self.hparams.get("head_dim", self.hparams.get("attention_head_dim"))
176
+ assert self.head_dim is not None, "Could not find the attention head dim in config"
177
+
178
+ # Don't use expand to calculate d_inner
179
+ self.d_inner = self.find_hparam(["num_heads"]) * self.d_model
180
+
181
+ # Update the ssm / attn / mlp layers
182
+ # M: Mamba2, *: Attention, -: MLP
183
+ # MoE:
184
+ # M: Mamba2, *: Attention, E: Expert
185
+ pattern = self.hparams.get("hybrid_override_pattern") or self.hparams.get("layers_block_type")
186
+ if pattern is None:
187
+ self._ssm_layers = []
188
+ self._mlp_layers = []
189
+ elif isinstance(pattern, str):
190
+ self._ssm_layers = [i for i, val in enumerate(pattern) if val == "M"]
191
+ self._mlp_layers = [i for i, val in enumerate(pattern) if val == ("E" if self.is_moe else "-")]
192
+ else:
193
+ self._ssm_layers = [i for i, val in enumerate(pattern) if val == "mamba"]
194
+ self._mlp_layers = [i for i, val in enumerate(pattern) if val == "moe"]
195
+
196
+ def get_attn_layers(self):
197
+ pattern = self.hparams.get("hybrid_override_pattern") or self.hparams.get("layers_block_type")
198
+ if pattern is None:
199
+ return []
200
+ assert len(pattern) == self.block_count, f"Mismatch between pattern ({len(pattern)}) and block_count ({self.block_count})!"
201
+ if isinstance(pattern, str):
202
+ return [i for i, val in enumerate(pattern) if val == "*"]
203
+
204
+ return [i for i, val in enumerate(pattern) if val == "attention"]
205
+
206
+ def set_gguf_parameters(self):
207
+ super().set_gguf_parameters()
208
+
209
+ head_dim = self.head_dim
210
+ if head_dim is None:
211
+ raise ValueError("Could not find the attention head dim in config")
212
+ self.gguf_writer.add_key_length(head_dim)
213
+ self.gguf_writer.add_value_length(head_dim)
214
+
215
+ # Set feed_forward_length
216
+ # NOTE: This will trigger an override warning. This is preferable to
217
+ # duplicating all the parent logic
218
+ if not self.is_moe:
219
+ n_ff = self.find_hparam(["intermediate_size", "n_inner", "hidden_dim"])
220
+ self.gguf_writer.add_feed_forward_length([
221
+ n_ff if i in self._mlp_layers else 0 for i in range(self.block_count)
222
+ ])
223
+ else:
224
+ moe_intermediate_size = self.hparams["moe_intermediate_size"]
225
+ self.gguf_writer.add_feed_forward_length([
226
+ moe_intermediate_size if i in self._mlp_layers else 0 for i in range(self.block_count)
227
+ ])
228
+ self.gguf_writer.add_expert_used_count(self.hparams["num_experts_per_tok"])
229
+ self.gguf_writer.add_expert_feed_forward_length(self.hparams["moe_intermediate_size"])
230
+ self.gguf_writer.add_expert_shared_feed_forward_length(self.hparams["moe_shared_expert_intermediate_size"])
231
+ self.gguf_writer.add_expert_count(self.hparams["n_routed_experts"])
232
+ self.gguf_writer.add_expert_shared_count(self.hparams["n_shared_experts"])
233
+ self.gguf_writer.add_expert_weights_norm(self.hparams["norm_topk_prob"])
234
+ self.gguf_writer.add_expert_weights_scale(self.hparams["routed_scaling_factor"])
235
+ self.gguf_writer.add_expert_group_count(self.hparams["n_group"])
236
+
237
+ # number of experts used per token (top-k)
238
+ if (n_experts_used := self.hparams.get("num_experts_per_tok")) is not None:
239
+ self.gguf_writer.add_expert_used_count(n_experts_used)
240
+
241
+ if (latent_size := self.hparams.get("moe_latent_size")) is not None:
242
+ self.gguf_writer.add_moe_latent_size(latent_size)
243
+
244
+ def set_vocab(self):
245
+ # The NemotronH config uses pattern characters (e.g. '-') that may not
246
+ # be supported by the installed transformers version. AutoTokenizer
247
+ # internally calls AutoConfig which triggers this parsing failure.
248
+ # Using trust_remote_code=True to load the model's own config class.
249
+ tokens: list[str] = []
250
+ toktypes: list[int] = []
251
+
252
+ from transformers import AutoTokenizer
253
+ tokenizer = AutoTokenizer.from_pretrained(self.dir_model, trust_remote_code=True)
254
+
255
+ # Pad vocab size (from Mamba2Model/GraniteHybridModel)
256
+ self.hparams["pad_vocab_size_multiple"] = 8 # Setting this here since GraniteHybridModel.set_vocab() isn't being invoked now.
257
+ # From Mamba2Model.set_vocab():
258
+ vocab_size = self.hparams["vocab_size"]
259
+ pad_vocab = self.hparams.get("pad_vocab_size_multiple", 16)
260
+ # ref: https://stackoverflow.com/a/17511341/22827863
261
+ vocab_size = -(vocab_size // -pad_vocab) * pad_vocab
262
+ self.hparams["vocab_size"] = vocab_size
263
+
264
+ assert max(tokenizer.vocab.values()) < vocab_size # ty: ignore[unresolved-attribute]
265
+
266
+ tokpre = self.get_vocab_base_pre(tokenizer)
267
+
268
+ reverse_vocab = {id_: encoded_tok for encoded_tok, id_ in tokenizer.vocab.items()} # ty: ignore[unresolved-attribute]
269
+ added_vocab = tokenizer.get_added_vocab() # ty: ignore[unresolved-attribute]
270
+
271
+ added_tokens_decoder = tokenizer.added_tokens_decoder # ty: ignore[unresolved-attribute]
272
+
273
+ for i in range(vocab_size):
274
+ if i not in reverse_vocab:
275
+ tokens.append(f"[PAD{i}]")
276
+ toktypes.append(gguf.TokenType.UNUSED)
277
+ else:
278
+ token: str = reverse_vocab[i]
279
+ if token in added_vocab:
280
+ if not added_tokens_decoder[i].normalized:
281
+ previous_token = token
282
+ token = tokenizer.decode(tokenizer.encode(token, add_special_tokens=False)) # ty: ignore[unresolved-attribute, invalid-assignment]
283
+ if previous_token != token:
284
+ logger.info(f"{repr(previous_token)} is encoded and decoded back to {repr(token)} using AutoTokenizer")
285
+
286
+ if added_tokens_decoder[i].special or self.does_token_look_special(token):
287
+ toktypes.append(gguf.TokenType.CONTROL)
288
+ else:
289
+ token = token.replace(b"\xe2\x96\x81".decode("utf-8"), " ") # pre-normalize user-defined spaces
290
+ toktypes.append(gguf.TokenType.USER_DEFINED)
291
+ else:
292
+ toktypes.append(gguf.TokenType.NORMAL)
293
+ tokens.append(token)
294
+
295
+ # From TextModel.set_vocab_gpt2():
296
+ self.gguf_writer.add_tokenizer_model("gpt2")
297
+ self.gguf_writer.add_tokenizer_pre(tokpre)
298
+ self.gguf_writer.add_token_list(tokens)
299
+ self.gguf_writer.add_token_types(toktypes)
300
+
301
+ special_vocab = gguf.SpecialVocab(self.dir_model, load_merges=True)
302
+ special_vocab.add_to_gguf(self.gguf_writer)
303
+
304
+ # The tokenizer _does_ add a BOS token (via post_processor type
305
+ # TemplateProcessing) but does not set add_bos_token to true in the
306
+ # config, so we need to explicitly override it here.
307
+ if not self.is_moe:
308
+ self.gguf_writer.add_add_bos_token(True)
309
+
310
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
311
+ if self.is_moe and bid is not None:
312
+ # Skip Multi-Token Prediction (MTP) tensors. These are used for
313
+ # for speculative decoding but we don't include them in this model
314
+ # conversion. See https://github.com/ggml-org/llama.cpp/pull/18886
315
+ if name.startswith("mtp."):
316
+ logger.info(f"gguf: Skipping MTP (Speculative) layer: {name}")
317
+ return
318
+
319
+ if name.endswith("mixer.gate.e_score_correction.bias"):
320
+ yield from ModelBase.modify_tensors(self, data_torch, name, bid)
321
+ return
322
+
323
+ if name.endswith("mixer.dt_bias"):
324
+ new_name = name.replace("dt_bias", "dt.bias")
325
+ yield from ModelBase.modify_tensors(self, data_torch, new_name, bid)
326
+ return
327
+
328
+ if name.endswith("mixer.conv1d.weight"):
329
+ squeezed_data = data_torch.squeeze()
330
+ yield from ModelBase.modify_tensors(self, squeezed_data, name, bid)
331
+ return
332
+
333
+ if name.endswith("mixer.A_log"):
334
+ transformed_data = -torch.exp(data_torch)
335
+ reshaped_data = transformed_data.squeeze().reshape(-1, 1)
336
+ yield from ModelBase.modify_tensors(self, reshaped_data, name, bid)
337
+ return
338
+
339
+ if name.endswith("mixer.D"):
340
+ reshaped_data = data_torch.squeeze().reshape(-1, 1)
341
+ yield from ModelBase.modify_tensors(self, reshaped_data, name, bid)
342
+ return
343
+
344
+ if name.endswith("mixer.norm.weight"):
345
+ reshaped_data = data_torch.reshape(self.n_group, -1)
346
+ yield from ModelBase.modify_tensors(self, reshaped_data, name, bid)
347
+ return
348
+
349
+ if name.find("mixer.experts") != -1:
350
+ n_experts = self.hparams["n_routed_experts"]
351
+ assert bid is not None
352
+
353
+ if self._experts is None:
354
+ self._experts = [{} for _ in range(self.block_count)]
355
+
356
+ self._experts[bid][name] = data_torch
357
+
358
+ if len(self._experts[bid]) >= n_experts * 2:
359
+ # merge the experts into a single tensor
360
+ for w_name in ["down_proj", "up_proj"]:
361
+ datas: list[Tensor] = []
362
+
363
+ for xid in range(n_experts):
364
+ ename = f"backbone.layers.{bid}.mixer.experts.{xid}.{w_name}.weight"
365
+ datas.append(self._experts[bid][ename])
366
+ del self._experts[bid][ename]
367
+
368
+ data_torch = torch.stack(datas, dim=0)
369
+ merged_name = f"model.layers.{bid}.mlp.experts.{w_name}.weight"
370
+
371
+ yield from ModelBase.modify_tensors(self, data_torch, merged_name, bid)
372
+ return
373
+ else:
374
+ return
375
+
376
+ yield from super().modify_tensors(data_torch, name, bid)
377
+
378
+ def prepare_tensors(self):
379
+ super().prepare_tensors()
380
+
381
+ if self._experts is not None:
382
+ # flatten `list[dict[str, Tensor]]` into `list[str]`
383
+ experts = [k for d in self._experts for k in d.keys()]
384
+ if len(experts) > 0:
385
+ raise ValueError(f"Unprocessed experts: {experts}")
llama.cpp/conversion/olmo.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Iterable, TYPE_CHECKING
4
+
5
+ import torch
6
+
7
+ if TYPE_CHECKING:
8
+ from torch import Tensor
9
+
10
+ from .base import ModelBase, TextModel, gguf
11
+
12
+ from .llama import LlamaModel
13
+
14
+
15
+ @ModelBase.register("OlmoForCausalLM")
16
+ @ModelBase.register("OLMoForCausalLM")
17
+ class OlmoModel(TextModel):
18
+ model_arch = gguf.MODEL_ARCH.OLMO
19
+
20
+ def set_gguf_parameters(self):
21
+ super().set_gguf_parameters()
22
+ self.gguf_writer.add_layer_norm_eps(1e-5)
23
+ clip_qkv = self.hparams.get("clip_qkv")
24
+ if clip_qkv is not None:
25
+ self.gguf_writer.add_clamp_kqv(clip_qkv)
26
+
27
+ # Same as super class, but permuting q_proj, k_proj
28
+ # Copied from: LlamaModel
29
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
30
+ n_head = self.hparams["num_attention_heads"]
31
+ n_kv_head = self.hparams.get("num_key_value_heads")
32
+
33
+ if name.endswith("q_proj.weight"):
34
+ data_torch = LlamaModel.permute(data_torch, n_head, n_head)
35
+ if name.endswith("k_proj.weight"):
36
+ data_torch = LlamaModel.permute(data_torch, n_head, n_kv_head)
37
+
38
+ yield from super().modify_tensors(data_torch, name, bid)
39
+
40
+
41
+ @ModelBase.register("SeedOssForCausalLM")
42
+ class SeedOssModel(TextModel):
43
+ model_arch = gguf.MODEL_ARCH.SEED_OSS
44
+
45
+
46
+ @ModelBase.register("Olmo2ForCausalLM")
47
+ @ModelBase.register("Olmo3ForCausalLM")
48
+ class Olmo2Model(TextModel):
49
+ model_arch = gguf.MODEL_ARCH.OLMO2
50
+
51
+ def set_gguf_parameters(self):
52
+ super().set_gguf_parameters()
53
+
54
+ if "sliding_window" in self.hparams:
55
+ self.gguf_writer.add_sliding_window(self.hparams["sliding_window"])
56
+
57
+ sliding_window_pattern = []
58
+ if "layer_types" in self.hparams:
59
+ sliding_window_pattern = [t == "sliding_attention" for t in self.hparams["layer_types"]]
60
+ else:
61
+ # Olmo2 does not use sliding window attention.
62
+ # Olmo3 defaults to using sliding window for all layers except every 4th.
63
+ for i in range(self.hparams["num_hidden_layers"]):
64
+ sliding_window_pattern.append((i + 1) % 4 != 0)
65
+
66
+ self.gguf_writer.add_sliding_window_pattern(sliding_window_pattern)
67
+
68
+
69
+ @ModelBase.register("OlmoeForCausalLM")
70
+ class OlmoeModel(TextModel):
71
+ model_arch = gguf.MODEL_ARCH.OLMOE
72
+
73
+ def set_gguf_parameters(self):
74
+ super().set_gguf_parameters()
75
+ self.gguf_writer.add_layer_norm_rms_eps(1e-5)
76
+
77
+ _experts: list[dict[str, Tensor]] | None = None
78
+
79
+ # Copied from: Qwen2MoeModel
80
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
81
+ # process the experts separately
82
+ if name.find("experts") != -1:
83
+ n_experts = self.find_hparam(["num_local_experts", "num_experts"])
84
+ assert bid is not None
85
+
86
+ if self._experts is None:
87
+ self._experts = [{} for _ in range(self.block_count)]
88
+
89
+ self._experts[bid][name] = data_torch
90
+
91
+ if len(self._experts[bid]) >= n_experts * 3:
92
+ # merge the experts into a single 3d tensor
93
+ for w_name in ["down_proj", "gate_proj", "up_proj"]:
94
+ datas: list[Tensor] = []
95
+
96
+ for xid in range(n_experts):
97
+ ename = f"model.layers.{bid}.mlp.experts.{xid}.{w_name}.weight"
98
+ datas.append(self._experts[bid][ename])
99
+ del self._experts[bid][ename]
100
+
101
+ data_torch = torch.stack(datas, dim=0)
102
+
103
+ merged_name = f"model.layers.{bid}.mlp.experts.{w_name}.weight"
104
+
105
+ yield from super().modify_tensors(data_torch, merged_name, bid)
106
+ return
107
+ else:
108
+ return
109
+
110
+ yield from super().modify_tensors(data_torch, name, bid)
111
+
112
+ # Copied from: Qwen2MoeModel
113
+ def prepare_tensors(self):
114
+ super().prepare_tensors()
115
+
116
+ if self._experts is not None:
117
+ # flatten `list[dict[str, Tensor]]` into `list[str]`
118
+ experts = [k for d in self._experts for k in d.keys()]
119
+ if len(experts) > 0:
120
+ raise ValueError(f"Unprocessed experts: {experts}")
llama.cpp/conversion/openelm.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Iterable, TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from torch import Tensor
7
+
8
+ from .base import ModelBase, TextModel, gguf
9
+
10
+
11
+ @ModelBase.register("OpenELMForCausalLM")
12
+ class OpenELMModel(TextModel):
13
+ model_arch = gguf.MODEL_ARCH.OPENELM
14
+
15
+ @staticmethod
16
+ def _make_divisible(v: float | int, divisor: int) -> int:
17
+ # ref: https://huggingface.co/apple/OpenELM-270M-Instruct/blob/eb111ff2e6724348e5b905984063d4064d4bc579/configuration_openelm.py#L34-L38
18
+ new_v = max(divisor, int(v + divisor / 2) // divisor * divisor)
19
+ # Make sure that round down does not go down by more than 10%.
20
+ if new_v < 0.9 * v:
21
+ new_v += divisor
22
+ return new_v
23
+
24
+ def __init__(self, *args, **kwargs):
25
+ super().__init__(*args, **kwargs)
26
+
27
+ ffn_multipliers: list[float] = self.hparams["ffn_multipliers"]
28
+ ffn_dim_divisor: int = self.hparams["ffn_dim_divisor"]
29
+ self._n_embd: int = self.hparams["model_dim"]
30
+ self._num_kv_heads: list[int] = self.hparams["num_kv_heads"]
31
+ self._num_query_heads: list[int] = self.hparams["num_query_heads"]
32
+ self._ffn_dims: list[int] = [
33
+ OpenELMModel._make_divisible(multiplier * self._n_embd, ffn_dim_divisor)
34
+ for multiplier in ffn_multipliers
35
+ ]
36
+ assert isinstance(self._num_kv_heads, list) and isinstance(self._num_kv_heads[0], int)
37
+ assert isinstance(self._num_query_heads, list) and isinstance(self._num_query_heads[0], int)
38
+
39
+ # Uses the tokenizer from meta-llama/Llama-2-7b-hf
40
+ def set_vocab(self):
41
+ try:
42
+ self._set_vocab_sentencepiece()
43
+ except FileNotFoundError:
44
+ self._set_vocab_builtin("llama-spm", self.hparams["vocab_size"])
45
+
46
+ def set_gguf_parameters(self):
47
+ n_embd = self._n_embd
48
+ head_dim = self.hparams["head_dim"]
49
+ rot_pct = 1.0
50
+ assert self.block_count == len(self._num_kv_heads)
51
+ assert self.block_count == len(self._num_query_heads)
52
+ assert self.block_count == len(self._ffn_dims)
53
+
54
+ self.gguf_writer.add_block_count(self.block_count)
55
+ self.gguf_writer.add_context_length(self.hparams["max_context_length"])
56
+ self.gguf_writer.add_embedding_length(n_embd)
57
+ self.gguf_writer.add_feed_forward_length(self._ffn_dims)
58
+ self.gguf_writer.add_head_count(self._num_query_heads)
59
+ self.gguf_writer.add_head_count_kv(self._num_kv_heads)
60
+ self.gguf_writer.add_rope_freq_base(self.hparams["rope_freq_constant"])
61
+ # https://huggingface.co/apple/OpenELM-270M-Instruct/blob/c401df2/modeling_openelm.py#L30
62
+ self.gguf_writer.add_layer_norm_rms_eps(1e-6)
63
+ self.gguf_writer.add_rope_dimension_count(int(rot_pct * head_dim))
64
+ self.gguf_writer.add_key_length(head_dim)
65
+ self.gguf_writer.add_value_length(head_dim)
66
+ self.gguf_writer.add_file_type(self.ftype)
67
+
68
+ def find_hparam(self, keys: Iterable[str], optional: bool = False) -> Any:
69
+ if "n_layers" in keys:
70
+ return self.hparams["num_transformer_layers"]
71
+
72
+ return super().find_hparam(keys, optional)
73
+
74
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
75
+
76
+ # split ff
77
+ if bid is not None and name == f"transformer.layers.{bid}.ffn.proj_1.weight":
78
+ ff_dim = self._ffn_dims[bid]
79
+ yield (self.format_tensor_name(gguf.MODEL_TENSOR.FFN_GATE, bid), data_torch[:ff_dim])
80
+ yield (self.format_tensor_name(gguf.MODEL_TENSOR.FFN_UP, bid), data_torch[ff_dim:])
81
+ return
82
+
83
+ yield (self.map_tensor_name(name), data_torch)
llama.cpp/conversion/orion.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from .base import ModelBase, TextModel, gguf
4
+
5
+
6
+ @ModelBase.register("OrionForCausalLM")
7
+ class OrionModel(TextModel):
8
+ model_arch = gguf.MODEL_ARCH.ORION
9
+
10
+ def set_vocab(self):
11
+ self._set_vocab_sentencepiece()
12
+
13
+ def set_gguf_parameters(self):
14
+ head_count = self.hparams["num_attention_heads"]
15
+ head_count_kv = self.hparams.get("num_key_value_heads", head_count)
16
+
17
+ ctx_length = 0
18
+ if "max_sequence_length" in self.hparams:
19
+ ctx_length = self.hparams["max_sequence_length"]
20
+ elif "max_position_embeddings" in self.hparams:
21
+ ctx_length = self.hparams["max_position_embeddings"]
22
+ elif "model_max_length" in self.hparams:
23
+ ctx_length = self.hparams["model_max_length"]
24
+ else:
25
+ raise ValueError("gguf: can not find ctx length parameter.")
26
+
27
+ self.gguf_writer.add_file_type(self.ftype)
28
+ self.gguf_writer.add_tensor_data_layout("Meta AI original pth")
29
+ self.gguf_writer.add_context_length(ctx_length)
30
+ self.gguf_writer.add_embedding_length(self.hparams["hidden_size"])
31
+ self.gguf_writer.add_block_count(self.block_count)
32
+ self.gguf_writer.add_feed_forward_length(self.hparams["intermediate_size"])
33
+ self.gguf_writer.add_head_count(head_count)
34
+ self.gguf_writer.add_head_count_kv(head_count_kv)
35
+ # note: config provides rms norm but it is actually layer norm
36
+ # ref: https://huggingface.co/OrionStarAI/Orion-14B-Chat/blob/276a17221ce42beb45f66fac657a41540e71f4f5/modeling_orion.py#L570-L571
37
+ self.gguf_writer.add_layer_norm_eps(self.hparams["rms_norm_eps"])
llama.cpp/conversion/pangu.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ from typing import Iterable, TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from torch import Tensor
9
+
10
+ from .base import ModelBase, TextModel, gguf, logger
11
+
12
+
13
+ @ModelBase.register("PanguEmbeddedForCausalLM")
14
+ class PanguEmbeddedModel(TextModel):
15
+ model_arch = gguf.MODEL_ARCH.PANGU_EMBED
16
+
17
+ def set_vocab(self):
18
+ self._set_vocab_sentencepiece()
19
+
20
+ tokenizer_config_file = self.dir_model / 'tokenizer_config.json'
21
+ if tokenizer_config_file.is_file():
22
+ with open(tokenizer_config_file, "r", encoding="utf-8") as f:
23
+ tokenizer_config_json = json.load(f)
24
+ if "add_prefix_space" in tokenizer_config_json:
25
+ self.gguf_writer.add_add_space_prefix(tokenizer_config_json["add_prefix_space"])
26
+
27
+ def set_gguf_parameters(self):
28
+ super().set_gguf_parameters()
29
+ hparams = self.hparams
30
+ self.gguf_writer.add_vocab_size(hparams["vocab_size"])
31
+
32
+ # PanguEmbedded's hparam loaded from config.json without head_dim
33
+ if (rope_dim := hparams.get("head_dim")) is None:
34
+ rope_dim = hparams["hidden_size"] // hparams["num_attention_heads"]
35
+ self.gguf_writer.add_rope_dimension_count(rope_dim)
36
+
37
+ if hparams.get("head_dim") is None:
38
+ self.gguf_writer.add_key_length(rope_dim)
39
+ self.gguf_writer.add_value_length(rope_dim)
40
+
41
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
42
+ if name == "lm_head.weight":
43
+ if self.hparams.get("tie_word_embeddings", False):
44
+ logger.info("Skipping tied output layer 'lm_head.weight'")
45
+ return
46
+ yield from super().modify_tensors(data_torch, name, bid)
llama.cpp/conversion/phi.py ADDED
@@ -0,0 +1,388 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import math
5
+
6
+ from typing import Callable, Iterable, TYPE_CHECKING
7
+
8
+ import torch
9
+
10
+ if TYPE_CHECKING:
11
+ from torch import Tensor
12
+
13
+ from .base import MmprojModel, ModelBase, SentencePieceTokenTypes, TextModel, gguf, logger
14
+
15
+
16
+ @ModelBase.register("PhiForCausalLM")
17
+ class Phi2Model(TextModel):
18
+ model_arch = gguf.MODEL_ARCH.PHI2
19
+
20
+ def set_gguf_parameters(self):
21
+ rot_pct = self.rope_parameters["partial_rotary_factor"]
22
+ n_embd = self.find_hparam(["hidden_size", "n_embd"])
23
+ n_head = self.find_hparam(["num_attention_heads", "n_head"])
24
+
25
+ self.gguf_writer.add_context_length(self.find_hparam(["n_positions", "max_position_embeddings"]))
26
+
27
+ self.gguf_writer.add_embedding_length(n_embd)
28
+ self.gguf_writer.add_feed_forward_length(4 * n_embd)
29
+ self.gguf_writer.add_block_count(self.block_count)
30
+ self.gguf_writer.add_head_count(n_head)
31
+ self.gguf_writer.add_head_count_kv(n_head)
32
+ self.gguf_writer.add_layer_norm_eps(self.find_hparam(["layer_norm_epsilon", "layer_norm_eps"]))
33
+ self.gguf_writer.add_rope_dimension_count(int(rot_pct * n_embd) // n_head)
34
+ self.gguf_writer.add_file_type(self.ftype)
35
+ self.gguf_writer.add_add_bos_token(False)
36
+
37
+
38
+ @ModelBase.register("Phi3ForCausalLM", "Phi4ForCausalLMV")
39
+ class Phi3MiniModel(TextModel):
40
+ model_arch = gguf.MODEL_ARCH.PHI3
41
+
42
+ def set_vocab(self):
43
+ # Phi-4 model uses GPT2Tokenizer
44
+ tokenizer_config_file = self.dir_model / 'tokenizer_config.json'
45
+ if tokenizer_config_file.is_file():
46
+ with open(tokenizer_config_file, "r", encoding="utf-8") as f:
47
+ tokenizer_config_json = json.load(f)
48
+ tokenizer_class = tokenizer_config_json['tokenizer_class']
49
+ if tokenizer_class == 'GPT2Tokenizer':
50
+ return self._set_vocab_gpt2()
51
+
52
+ from sentencepiece import SentencePieceProcessor
53
+
54
+ tokenizer_path = self.dir_model / 'tokenizer.model'
55
+
56
+ if not tokenizer_path.is_file():
57
+ raise ValueError(f'Error: Missing {tokenizer_path}')
58
+
59
+ tokenizer = SentencePieceProcessor()
60
+ tokenizer.LoadFromFile(str(tokenizer_path))
61
+
62
+ vocab_size = self.hparams.get('vocab_size', tokenizer.vocab_size())
63
+
64
+ tokens: list[bytes] = [f"[PAD{i}]".encode("utf-8") for i in range(vocab_size)]
65
+ scores: list[float] = [-10000.0] * vocab_size
66
+ toktypes: list[int] = [SentencePieceTokenTypes.UNUSED] * vocab_size
67
+
68
+ for token_id in range(tokenizer.vocab_size()):
69
+
70
+ piece = tokenizer.IdToPiece(token_id)
71
+ text = piece.encode("utf-8")
72
+ score = tokenizer.GetScore(token_id)
73
+
74
+ toktype = SentencePieceTokenTypes.NORMAL
75
+ if tokenizer.IsUnknown(token_id):
76
+ toktype = SentencePieceTokenTypes.UNKNOWN
77
+ elif tokenizer.IsControl(token_id):
78
+ toktype = SentencePieceTokenTypes.CONTROL
79
+ elif tokenizer.IsUnused(token_id):
80
+ toktype = SentencePieceTokenTypes.UNUSED
81
+ elif tokenizer.IsByte(token_id):
82
+ toktype = SentencePieceTokenTypes.BYTE
83
+
84
+ tokens[token_id] = text
85
+ scores[token_id] = score
86
+ toktypes[token_id] = toktype
87
+
88
+ added_tokens_file = self.dir_model / 'added_tokens.json'
89
+ if added_tokens_file.is_file():
90
+ with open(added_tokens_file, "r", encoding="utf-8") as f:
91
+ added_tokens_json = json.load(f)
92
+
93
+ for key in added_tokens_json:
94
+ token_id = added_tokens_json[key]
95
+ if token_id >= vocab_size:
96
+ logger.debug(f'ignore token {token_id}: id is out of range, max={vocab_size - 1}')
97
+ continue
98
+
99
+ tokens[token_id] = key.encode("utf-8")
100
+ scores[token_id] = -1000.0
101
+ toktypes[token_id] = SentencePieceTokenTypes.USER_DEFINED
102
+
103
+ tokenizer_config_file = self.dir_model / 'tokenizer_config.json'
104
+ if tokenizer_config_file.is_file():
105
+ with open(tokenizer_config_file, "r", encoding="utf-8") as f:
106
+ tokenizer_config_json = json.load(f)
107
+ added_tokens_decoder = tokenizer_config_json.get("added_tokens_decoder", {})
108
+ for token_id, foken_data in added_tokens_decoder.items():
109
+ token_id = int(token_id)
110
+ token = foken_data["content"].encode("utf-8")
111
+ if toktypes[token_id] != SentencePieceTokenTypes.UNUSED:
112
+ if tokens[token_id] != token:
113
+ logger.warning(f'replacing token {token_id}: {tokens[token_id].decode("utf-8")!r} -> {token.decode("utf-8")!r}')
114
+ tokens[token_id] = token
115
+ scores[token_id] = -1000.0
116
+ toktypes[token_id] = SentencePieceTokenTypes.USER_DEFINED
117
+ if foken_data.get("special"):
118
+ toktypes[token_id] = SentencePieceTokenTypes.CONTROL
119
+
120
+ tokenizer_file = self.dir_model / 'tokenizer.json'
121
+ if tokenizer_file.is_file():
122
+ with open(tokenizer_file, "r", encoding="utf-8") as f:
123
+ tokenizer_json = json.load(f)
124
+ added_tokens = tokenizer_json.get("added_tokens", [])
125
+ for foken_data in added_tokens:
126
+ token_id = int(foken_data["id"])
127
+ token = foken_data["content"].encode("utf-8")
128
+ if toktypes[token_id] != SentencePieceTokenTypes.UNUSED:
129
+ if tokens[token_id] != token:
130
+ logger.warning(f'replacing token {token_id}: {tokens[token_id].decode("utf-8")!r} -> {token.decode("utf-8")!r}')
131
+ tokens[token_id] = token
132
+ scores[token_id] = -1000.0
133
+ toktypes[token_id] = SentencePieceTokenTypes.USER_DEFINED
134
+ if foken_data.get("special"):
135
+ toktypes[token_id] = SentencePieceTokenTypes.CONTROL
136
+
137
+ self.gguf_writer.add_tokenizer_model("llama")
138
+ self.gguf_writer.add_tokenizer_pre("default")
139
+ self.gguf_writer.add_token_list(tokens)
140
+ self.gguf_writer.add_token_scores(scores)
141
+ self.gguf_writer.add_token_types(toktypes)
142
+
143
+ special_vocab = gguf.SpecialVocab(self.dir_model, n_vocab=len(tokens))
144
+ special_vocab.add_to_gguf(self.gguf_writer)
145
+
146
+ def set_gguf_parameters(self):
147
+ n_embd = self.find_hparam(["hidden_size", "n_embd"])
148
+ n_head = self.find_hparam(["num_attention_heads", "n_head"])
149
+ n_head_kv = self.find_hparam(["num_key_value_heads", "n_head_kv"])
150
+ rms_eps = self.find_hparam(["rms_norm_eps"])
151
+ max_pos_embds = self.find_hparam(["n_positions", "max_position_embeddings"])
152
+ orig_max_pos_embds = self.rope_parameters["original_max_position_embeddings"]
153
+ rot_pct = self.rope_parameters.get("partial_rotary_factor", 1.0)
154
+ rope_dims = int(rot_pct * n_embd) // n_head
155
+
156
+ self.gguf_writer.add_context_length(max_pos_embds)
157
+ self.gguf_writer.add_rope_scaling_orig_ctx_len(orig_max_pos_embds)
158
+ self.gguf_writer.add_embedding_length(n_embd)
159
+ self.gguf_writer.add_feed_forward_length(self.find_hparam(["intermediate_size"]))
160
+ self.gguf_writer.add_block_count(self.block_count)
161
+ self.gguf_writer.add_head_count(n_head)
162
+ self.gguf_writer.add_head_count_kv(n_head_kv)
163
+ self.gguf_writer.add_layer_norm_rms_eps(rms_eps)
164
+ self.gguf_writer.add_rope_dimension_count(rope_dims)
165
+ self.gguf_writer.add_rope_freq_base(self.rope_parameters.get("full_attention", self.rope_parameters)["rope_theta"])
166
+ self.gguf_writer.add_file_type(self.ftype)
167
+ sliding_window = self.hparams.get("sliding_window")
168
+ # use zero value of sliding_window to distinguish Phi-4 from other PHI3 models
169
+ if sliding_window is None:
170
+ sliding_window = 0
171
+ self.gguf_writer.add_sliding_window(sliding_window)
172
+
173
+ def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]:
174
+ n_embd = self.find_hparam(["hidden_size", "n_embd"])
175
+ n_head = self.find_hparam(["num_attention_heads", "n_head"])
176
+ max_pos_embds = self.find_hparam(["n_positions", "max_position_embeddings"])
177
+ orig_max_pos_embds = self.rope_parameters["original_max_position_embeddings"]
178
+ rot_pct = self.rope_parameters.get("partial_rotary_factor", 1.0)
179
+ rope_dims = int(rot_pct * n_embd) // n_head
180
+
181
+ # write rope scaling for long context (128k) model
182
+ long_factors = self.rope_parameters.get('long_factor')
183
+ short_factors = self.rope_parameters.get('short_factor')
184
+ if not long_factors:
185
+ return
186
+
187
+ scale = max_pos_embds / orig_max_pos_embds
188
+
189
+ rope_scaling_type = self.rope_parameters.get('rope_type', '').lower()
190
+ if len(rope_scaling_type) == 0:
191
+ raise KeyError('Missing the required key rope_scaling.type')
192
+
193
+ if rope_scaling_type == 'su' or rope_scaling_type == 'longrope':
194
+ attn_factor = math.sqrt(1 + math.log(scale) / math.log(orig_max_pos_embds)) if scale > 1.0 else 1.0
195
+ elif rope_scaling_type == 'yarn':
196
+ attn_factor = 0.1 * math.log(scale) + 1.0 if scale > 1.0 else 1.0
197
+ else:
198
+ raise NotImplementedError(f'The rope scaling type {rope_scaling_type} is not supported yet')
199
+
200
+ self.gguf_writer.add_rope_scaling_attn_factors(attn_factor)
201
+
202
+ if long_factors is None or short_factors is None:
203
+ raise KeyError('Missing the required key rope_scaling.long_factor or rope_scaling_short_factor')
204
+
205
+ if len(long_factors) != len(short_factors) or len(long_factors) != rope_dims / 2:
206
+ raise ValueError(f'The length of rope long and short factors must be {rope_dims / 2}. long_factors = {len(long_factors)}, short_factors = {len(short_factors)}.')
207
+
208
+ yield (self.format_tensor_name(gguf.MODEL_TENSOR.ROPE_FACTORS_LONG), torch.tensor(long_factors, dtype=torch.float32))
209
+ yield (self.format_tensor_name(gguf.MODEL_TENSOR.ROPE_FACTORS_SHORT), torch.tensor(short_factors, dtype=torch.float32))
210
+
211
+
212
+ @ModelBase.register("Phi4ForCausalLMV")
213
+ class Phi4VisionMmprojModel(MmprojModel):
214
+ def __init__(self, *args, **kwargs):
215
+ super().__init__(*args, **kwargs)
216
+ assert self.hparams_vision is not None
217
+
218
+ self.vision_total_layers = int(self.find_vparam(self.n_block_keys))
219
+ if self.vision_total_layers < 2:
220
+ raise ValueError(
221
+ f"Phi-4 vision mmproj conversion requires at least 2 vision layers, got {self.vision_total_layers}"
222
+ )
223
+
224
+ # Phi-4 uses SigLIP2 hidden_states[-2], so export one fewer encoder block and
225
+ # drop post-layernorm/head weights. This makes the GGUF runtime output match
226
+ # the feature map consumed by the patched siglip.cpp Phi-4 projector path.
227
+ self.vision_export_layers = self.vision_total_layers - 1
228
+ self.vision_last_layer_idx = self.vision_total_layers - 1
229
+
230
+ for key in self.n_block_keys:
231
+ if key in self.hparams_vision:
232
+ self.hparams_vision[key] = self.vision_export_layers
233
+ break
234
+
235
+ self.block_count = self.vision_export_layers
236
+ self.tensor_map = gguf.get_tensor_name_map(gguf.MODEL_ARCH.MMPROJ, self.block_count)
237
+
238
+ patch_size = self.preprocessor_config.get("patch_size")
239
+ if patch_size is None:
240
+ raise KeyError("Phi-4 vision mmproj conversion requires patch_size in preprocessor_config.json")
241
+
242
+ self.hparams_vision["patch_size"] = patch_size
243
+
244
+ pos_emb_name = next(
245
+ (
246
+ name for name in self.model_tensors
247
+ if name.endswith("vision_model.embeddings.position_embedding.weight")
248
+ ),
249
+ None,
250
+ )
251
+ if pos_emb_name is None:
252
+ raise KeyError("Phi-4 vision mmproj conversion could not find position_embedding.weight")
253
+
254
+ pos_emb_shape = self.model_tensors[pos_emb_name]().shape
255
+ base_grid_tokens = int(pos_emb_shape[0])
256
+ grid_side = math.isqrt(base_grid_tokens)
257
+ if grid_side * grid_side != base_grid_tokens:
258
+ raise ValueError(f"Unexpected Phi-4 position embedding shape: {tuple(pos_emb_shape)}")
259
+
260
+ self.hparams_vision["image_size"] = grid_side * patch_size
261
+
262
+ min_num_patches = self.preprocessor_config.get("min_num_patches", self.global_config.get("min_num_patches"))
263
+ max_num_patches = self.preprocessor_config.get("max_num_patches", self.global_config.get("max_num_patches"))
264
+ if min_num_patches is None or max_num_patches is None:
265
+ raise KeyError("Phi-4 vision mmproj conversion requires min_num_patches and max_num_patches")
266
+
267
+ self.min_pixels = int(min_num_patches) * patch_size * patch_size
268
+ self.max_pixels = int(max_num_patches) * patch_size * patch_size
269
+
270
+ def set_gguf_parameters(self):
271
+ super().set_gguf_parameters()
272
+ assert self.hparams_vision is not None
273
+
274
+ self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.PHI4)
275
+ self.gguf_writer.add_vision_min_pixels(self.min_pixels)
276
+ self.gguf_writer.add_vision_max_pixels(self.max_pixels)
277
+ self.gguf_writer.add_vision_use_gelu(True)
278
+ self.gguf_writer.add_vision_attention_layernorm_eps(self.hparams_vision.get("layer_norm_eps", 1e-6))
279
+
280
+ @classmethod
281
+ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
282
+ name, gen = item
283
+
284
+ name = name.replace("model.vision_tower.vision_tower.", "vision_tower.")
285
+
286
+ if not name.startswith(("vision_tower.", "model.mm_projector.", "mm_projector.")):
287
+ return None
288
+
289
+ if ".vision_model.head." in name:
290
+ return None
291
+
292
+ if ".vision_model.post_layernorm." in name:
293
+ return None
294
+
295
+ return super().filter_tensors((name, gen))
296
+
297
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
298
+ if name.startswith("vision_tower."):
299
+ if bid is not None and bid == self.vision_last_layer_idx:
300
+ return
301
+
302
+ if name.endswith("vision_model.embeddings.patch_embedding.weight"):
303
+ assert self.hparams_vision is not None
304
+ if data_torch.ndim != 2:
305
+ raise ValueError(f"Unexpected Phi-4 patch embedding shape: {tuple(data_torch.shape)}")
306
+
307
+ patch_area = self.hparams_vision["patch_size"] ** 2
308
+ in_features = data_torch.shape[1]
309
+ if in_features % patch_area != 0:
310
+ raise ValueError(
311
+ f"Phi-4 patch embedding input dim {in_features} is not divisible by patch area {patch_area}"
312
+ )
313
+
314
+ num_channels = in_features // patch_area
315
+ patch_size = self.hparams_vision["patch_size"]
316
+ data_torch = data_torch.view(data_torch.shape[0], patch_size, patch_size, num_channels)
317
+ data_torch = data_torch.permute(0, 3, 1, 2)
318
+
319
+ yield from super().modify_tensors(data_torch, name, bid)
320
+ return
321
+
322
+ if name.startswith(("model.mm_projector.", "mm_projector.")):
323
+ local_name = name
324
+ local_name = local_name.replace("model.mm_projector.", "")
325
+ local_name = local_name.replace("mm_projector.", "")
326
+
327
+ if not (local_name.startswith("0.") or local_name.startswith("2.")):
328
+ return
329
+
330
+ suffix = ".bias" if local_name.endswith(".bias") else ".weight"
331
+ mm_idx = int(local_name.split(".", maxsplit=1)[0])
332
+ yield (self.format_tensor_name(gguf.MODEL_TENSOR.V_MMPROJ, mm_idx, suffix=suffix), data_torch)
333
+ return
334
+
335
+ return
336
+
337
+
338
+ @ModelBase.register("PhiMoEForCausalLM")
339
+ class PhiMoeModel(Phi3MiniModel):
340
+ model_arch = gguf.MODEL_ARCH.PHIMOE
341
+
342
+ _experts: list[dict[str, Tensor]] | None = None
343
+
344
+ def set_gguf_parameters(self):
345
+ super().set_gguf_parameters()
346
+ self.gguf_writer.add_expert_used_count(self.find_hparam(["num_experts_per_tok", "num_experts_per_token"]))
347
+ self.gguf_writer.add_expert_count(self.find_hparam(["num_local_experts", "num_experts"]))
348
+
349
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
350
+ # process the experts separately
351
+ if name.find("block_sparse_moe.experts") != -1:
352
+ n_experts = self.find_hparam(["num_local_experts", "num_experts"])
353
+ assert bid is not None
354
+
355
+ if self._experts is None:
356
+ self._experts = [{} for _ in range(self.block_count)]
357
+
358
+ self._experts[bid][name] = data_torch
359
+
360
+ if len(self._experts[bid]) >= n_experts * 3:
361
+ # merge the experts into a single 3d tensor
362
+ for w_name in ["w1", "w2", "w3"]:
363
+ datas: list[Tensor] = []
364
+
365
+ for xid in range(n_experts):
366
+ ename = f"model.layers.{bid}.block_sparse_moe.experts.{xid}.{w_name}.weight"
367
+ datas.append(self._experts[bid][ename])
368
+ del self._experts[bid][ename]
369
+
370
+ data_torch = torch.stack(datas, dim=0)
371
+
372
+ merged_name = f"model.layers.{bid}.block_sparse_moe.experts.{w_name}.weight"
373
+
374
+ yield from super().modify_tensors(data_torch, merged_name, bid)
375
+ return
376
+ else:
377
+ return
378
+
379
+ yield from super().modify_tensors(data_torch, name, bid)
380
+
381
+ def prepare_tensors(self):
382
+ super().prepare_tensors()
383
+
384
+ if self._experts is not None:
385
+ # flatten `list[dict[str, Tensor]]` into `list[str]`
386
+ experts = [k for d in self._experts for k in d.keys()]
387
+ if len(experts) > 0:
388
+ raise ValueError(f"Unprocessed experts: {experts}")
llama.cpp/conversion/pixtral.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Sequence
4
+
5
+ from .base import gguf
6
+
7
+ from .llava import LlavaVisionModel
8
+
9
+
10
+ class PixtralModel(LlavaVisionModel):
11
+ model_name = "Pixtral"
12
+ hf_arch = ""
13
+ is_mistral_format = True
14
+
15
+ def set_gguf_parameters(self):
16
+ super().set_gguf_parameters()
17
+ self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.PIXTRAL)
18
+
19
+ self.gguf_writer.add_vision_attention_layernorm_eps(
20
+ self.find_hparam(["norm_eps"])
21
+ )
22
+ self.gguf_writer.add_rope_freq_base(self.find_vparam(["rope_theta"]))
23
+
24
+ self.gguf_writer.add_vision_use_silu(True)
25
+
26
+ # spatial_merge_size
27
+ if self.find_vparam(["mm_projector_id"], optional=True) == "patch_merge":
28
+ self.gguf_writer.add_vision_spatial_merge_size(
29
+ self.find_vparam(["spatial_merge_size"])
30
+ )
31
+
32
+ def map_tensor_name(self, name: str, try_suffixes: Sequence[str] = (".weight", ".bias")) -> str:
33
+ if name == "vision_language_adapter.w_in.weight":
34
+ return "mm.1.weight"
35
+ elif name == "vision_language_adapter.w_in.bias":
36
+ return "mm.1.bias"
37
+ elif name == "vision_language_adapter.w_out.weight":
38
+ return "mm.2.weight"
39
+ elif name == "vision_language_adapter.w_out.bias":
40
+ return "mm.2.bias"
41
+ return super().map_tensor_name(name, try_suffixes)
llama.cpp/conversion/plamo.py ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ from typing import Iterable, TYPE_CHECKING
6
+
7
+ import torch
8
+
9
+ if TYPE_CHECKING:
10
+ from torch import Tensor
11
+
12
+ from .base import ModelBase, TextModel, gguf
13
+
14
+
15
+ @ModelBase.register("PlamoForCausalLM")
16
+ class PlamoModel(TextModel):
17
+ model_arch = gguf.MODEL_ARCH.PLAMO
18
+
19
+ def set_vocab(self):
20
+ self._set_vocab_sentencepiece()
21
+
22
+ def set_gguf_parameters(self):
23
+ hparams = self.hparams
24
+
25
+ self.gguf_writer.add_context_length(4096) # not in config.json
26
+ self.gguf_writer.add_embedding_length(hparams["hidden_size"])
27
+ self.gguf_writer.add_feed_forward_length(hparams["intermediate_size"])
28
+ self.gguf_writer.add_block_count(self.block_count)
29
+ self.gguf_writer.add_head_count(hparams["num_attention_heads"])
30
+ self.gguf_writer.add_head_count_kv(5) # hparams["num_key_value_heads"]) is wrong
31
+ self.gguf_writer.add_layer_norm_rms_eps(hparams["rms_norm_eps"])
32
+ self.gguf_writer.add_file_type(self.ftype)
33
+
34
+ def shuffle_attn_q_weight(self, data_torch):
35
+ assert data_torch.size() == (5120, 5120)
36
+ data_torch = data_torch.reshape(8, 5, 128, 5120)
37
+ data_torch = torch.permute(data_torch, (1, 0, 2, 3))
38
+ data_torch = torch.reshape(data_torch, (5120, 5120))
39
+ return data_torch
40
+
41
+ def shuffle_attn_output_weight(self, data_torch):
42
+ assert data_torch.size() == (5120, 5120)
43
+ data_torch = data_torch.reshape(5120, 8, 5, 128)
44
+ data_torch = torch.permute(data_torch, (0, 2, 1, 3))
45
+ data_torch = torch.reshape(data_torch, (5120, 5120))
46
+ return data_torch
47
+
48
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
49
+ new_name = self.map_tensor_name(name)
50
+
51
+ # shuffle for broadcasting of gqa in ggml_mul_mat
52
+ if new_name.endswith("attn_q.weight"):
53
+ data_torch = self.shuffle_attn_q_weight(data_torch)
54
+ elif new_name.endswith("attn_output.weight"):
55
+ data_torch = self.shuffle_attn_output_weight(data_torch)
56
+
57
+ yield from super().modify_tensors(data_torch, name, bid)
58
+
59
+
60
+ @ModelBase.register("Plamo2ForCausalLM", "PLaMo2ForCausalLM")
61
+ class Plamo2Model(TextModel):
62
+ model_arch = gguf.MODEL_ARCH.PLAMO2
63
+
64
+ def set_vocab(self):
65
+ self._set_vocab_plamo()
66
+
67
+ def set_gguf_parameters(self):
68
+ hparams = self.hparams
69
+ self.gguf_writer.add_vocab_size(self.hparams["vocab_size"])
70
+
71
+ # Which layers are Mamba layers
72
+ # PLaMo 2 uses mamba_step to indicate the pattern (e.g., 2 means every other layer)
73
+ # This logic matches modeling_plamo.py's is_mamba function
74
+ mamba_step = hparams.get("mamba_step", 2)
75
+ mamba_enabled = hparams.get("mamba_enabled", True)
76
+ num_key_value_heads = []
77
+ num_attention_heads = []
78
+
79
+ if mamba_enabled:
80
+ for i in range(self.block_count):
81
+ if self.block_count <= (mamba_step // 2):
82
+ # use attention in last layer
83
+ is_mamba = (i != self.block_count - 1)
84
+ else:
85
+ is_mamba = (i % mamba_step) != (mamba_step // 2)
86
+ if is_mamba:
87
+ num_key_value_heads.append(0)
88
+ num_attention_heads.append(0)
89
+ else:
90
+ num_key_value_heads.append(hparams.get("num_key_value_heads", 4))
91
+ num_attention_heads.append(hparams.get("num_attention_heads", 32))
92
+
93
+ if num_key_value_heads and num_attention_heads:
94
+ self.gguf_writer.add_head_count_kv(num_key_value_heads)
95
+ self.gguf_writer.add_head_count(num_attention_heads)
96
+
97
+ self.gguf_writer.add_context_length(hparams.get("max_position_embeddings", 2048))
98
+ self.gguf_writer.add_embedding_length(hparams.get("hidden_size", 4096))
99
+ self.gguf_writer.add_key_length(hparams.get("hidden_size_per_head", 128))
100
+ self.gguf_writer.add_value_length(hparams.get("hidden_size_per_head", 128))
101
+ self.gguf_writer.add_block_count(self.block_count)
102
+ self.gguf_writer.add_layer_norm_rms_eps(hparams.get("rms_norm_eps", 1e-06))
103
+ self.gguf_writer.add_rope_freq_base(self.rope_parameters.get("rope_theta", 10000))
104
+
105
+ # Mamba parameters
106
+ self.gguf_writer.add_ssm_state_size(hparams.get("mamba_d_state", 64))
107
+ self.gguf_writer.add_ssm_conv_kernel(hparams.get("mamba_d_conv", 4))
108
+ self.gguf_writer.add_ssm_time_step_rank(hparams.get("mamba_num_heads", 64))
109
+ intermediate_size = hparams.get("mamba_num_heads", 64) * hparams.get("hidden_size_per_head", 128)
110
+ self.gguf_writer.add_ssm_inner_size(intermediate_size)
111
+ self.gguf_writer.add_ssm_group_count(0)
112
+
113
+ # MLP feed forward parameters (for attention layers)
114
+ self.gguf_writer.add_feed_forward_length(hparams.get("intermediate_size", 13312))
115
+ self.gguf_writer.add_file_type(self.ftype)
116
+
117
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
118
+ if name.endswith(".A_log"):
119
+ data_torch = -torch.exp(data_torch)
120
+ elif name.endswith(".dt_bias"):
121
+ name = name.rpartition(".dt_bias")[0] + ".dt_proj.bias"
122
+ elif name.endswith(".dt_norm_weight"):
123
+ name = name.rpartition(".dt_norm_weight")[0] + ".dt_norm.weight"
124
+ elif name.endswith(".B_norm_weight"):
125
+ name = name.rpartition(".B_norm_weight")[0] + ".B_norm.weight"
126
+ elif name.endswith(".C_norm_weight"):
127
+ name = name.rpartition(".C_norm_weight")[0] + ".C_norm.weight"
128
+ elif name.endswith(".k_weight"):
129
+ name = name.rpartition(".k_weight")[0] + ".k.weight"
130
+ elif name.endswith(".q_weight"):
131
+ name = name.rpartition(".q_weight")[0] + ".q.weight"
132
+ elif name.endswith(".conv1d.weight"):
133
+ data_torch = torch.squeeze(data_torch) # remove (, 1, )
134
+ assert data_torch.ndim == 2
135
+ elif name.endswith(".pre_mixer_norm.weight"):
136
+ data_torch += 1.0
137
+ elif name.endswith(".post_mixer_norm.weight"):
138
+ data_torch += 1.0 / 5
139
+ elif name.endswith(".pre_mlp_norm.weight"):
140
+ data_torch += 1.0
141
+ elif name.endswith(".post_mlp_norm.weight"):
142
+ data_torch += 1.0 / (5**1.5)
143
+ elif name.endswith(".norm.weight"):
144
+ data_torch += 1.0
145
+
146
+ yield from super().modify_tensors(data_torch, name, bid)
147
+
148
+
149
+ @ModelBase.register("Plamo3ForCausalLM", "PLaMo3ForCausalLM")
150
+ class Plamo3Model(TextModel):
151
+ model_arch = gguf.MODEL_ARCH.PLAMO3
152
+
153
+ def set_vocab(self):
154
+ self._set_vocab_plamo()
155
+
156
+ tokenizer_config_path = self.dir_model / "tokenizer_config.json"
157
+ tokenizer_config = {}
158
+
159
+ if tokenizer_config_path.is_file():
160
+ with open(tokenizer_config_path, encoding="utf-8") as f:
161
+ tokenizer_config = json.load(f)
162
+
163
+ chat_template = tokenizer_config.get("chat_template")
164
+ chat_template_jinja = self.dir_model / "chat_template.jinja"
165
+
166
+ if chat_template_jinja.is_file():
167
+ with open(chat_template_jinja, encoding="utf-8") as f:
168
+ chat_template = f.read()
169
+
170
+ if chat_template:
171
+ self.gguf_writer.add_chat_template(chat_template)
172
+
173
+ def set_gguf_parameters(self):
174
+ super().set_gguf_parameters()
175
+ self.gguf_writer.add_vocab_size(self.hparams["vocab_size"])
176
+ if (sliding_window := self.find_hparam(["window_size", "sliding_window"], optional=True)) is not None:
177
+ self.gguf_writer.add_sliding_window(sliding_window)
178
+ self.gguf_writer.add_sliding_window_pattern(self.hparams["sliding_window_pattern"])
179
+
180
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
181
+
182
+ if name.endswith(".pre_mixer_norm.weight"):
183
+ data_torch = data_torch + 1.0
184
+ elif name.endswith(".post_mixer_norm.weight"):
185
+ data_torch = data_torch + 1.0 / 5
186
+ elif name.endswith(".pre_mlp_norm.weight"):
187
+ data_torch = data_torch + 1.0
188
+ elif name.endswith(".post_mlp_norm.weight"):
189
+ data_torch = data_torch + 1.0 / (5**1.5)
190
+ elif name.endswith((".mixer.q_norm.weight", ".mixer.k_norm.weight")):
191
+ data_torch = data_torch + 1.0
192
+ elif name.endswith(".norm.weight"):
193
+ data_torch = data_torch + 1.0
194
+
195
+ yield from super().modify_tensors(data_torch, name, bid)
llama.cpp/conversion/plm.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from .base import ModelBase, TextModel, gguf
4
+
5
+
6
+ @ModelBase.register("PLMForCausalLM")
7
+ class PLMModel(TextModel):
8
+ model_arch = gguf.MODEL_ARCH.PLM
9
+
10
+ def set_vocab(self):
11
+ self._set_vocab_gpt2()
12
+
13
+ def set_gguf_parameters(self):
14
+ super().set_gguf_parameters()
15
+ hparams = self.hparams
16
+ self.gguf_writer.add_vocab_size(hparams["vocab_size"])
17
+ self.gguf_writer.add_kv_lora_rank(hparams["kv_lora_rank"])
18
+ self.gguf_writer.add_key_length(hparams["qk_nope_head_dim"] + hparams["qk_rope_head_dim"])
19
+ self.gguf_writer.add_value_length(hparams["v_head_dim"])
20
+ self.gguf_writer.add_rope_dimension_count(hparams["qk_rope_head_dim"])
21
+
22
+ def prepare_tensors(self):
23
+ super().prepare_tensors()
llama.cpp/conversion/qwen.py ADDED
@@ -0,0 +1,675 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Iterable, TYPE_CHECKING
4
+
5
+ import torch
6
+
7
+ if TYPE_CHECKING:
8
+ from torch import Tensor
9
+
10
+ from .base import ModelBase, TextModel, gguf, logger
11
+
12
+
13
+ @ModelBase.register("QWenLMHeadModel")
14
+ class QwenModel(TextModel):
15
+ model_arch = gguf.MODEL_ARCH.QWEN
16
+
17
+ @staticmethod
18
+ def token_bytes_to_string(b):
19
+ from transformers.models.gpt2.tokenization_gpt2 import bytes_to_unicode # ty: ignore[unresolved-import]
20
+ byte_encoder = bytes_to_unicode()
21
+ return ''.join([byte_encoder[ord(char)] for char in b.decode('latin-1')])
22
+
23
+ @staticmethod
24
+ def bpe(mergeable_ranks: dict[bytes, int], token: bytes, max_rank: int | None = None) -> list[bytes]:
25
+ parts = [bytes([b]) for b in token]
26
+ while True:
27
+ min_idx = None
28
+ min_rank = None
29
+ for i, pair in enumerate(zip(parts[:-1], parts[1:])):
30
+ rank = mergeable_ranks.get(pair[0] + pair[1])
31
+ if rank is not None and (min_rank is None or rank < min_rank):
32
+ min_idx = i
33
+ min_rank = rank
34
+ if min_rank is None or (max_rank is not None and min_rank >= max_rank):
35
+ break
36
+ assert min_idx is not None
37
+ parts = parts[:min_idx] + [parts[min_idx] + parts[min_idx + 1]] + parts[min_idx + 2:]
38
+ return parts
39
+
40
+ def set_vocab(self):
41
+ self._set_vocab_qwen()
42
+
43
+
44
+ @ModelBase.register(
45
+ "Qwen2Model",
46
+ "Qwen2ForCausalLM",
47
+ "Qwen2AudioForConditionalGeneration",
48
+ "KORMoForCausalLM",
49
+ "AudioFlamingo3ForConditionalGeneration",
50
+ "DotsOCRForCausalLM",
51
+ )
52
+ class Qwen2Model(TextModel):
53
+ model_arch = gguf.MODEL_ARCH.QWEN2
54
+
55
+ def set_vocab(self):
56
+ try:
57
+ self._set_vocab_sentencepiece()
58
+ except FileNotFoundError:
59
+ self._set_vocab_gpt2()
60
+
61
+ def set_gguf_parameters(self):
62
+ super().set_gguf_parameters()
63
+ self._try_set_pooling_type()
64
+
65
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
66
+ if self.hf_arch == "Qwen2Model":
67
+ name = f"model.{name}" # map to Qwen2ForCausalLM tensors
68
+ yield from super().modify_tensors(data_torch, name, bid)
69
+
70
+
71
+ @ModelBase.register("Qwen2MoeForCausalLM")
72
+ class Qwen2MoeModel(TextModel):
73
+ model_arch = gguf.MODEL_ARCH.QWEN2MOE
74
+
75
+ def set_gguf_parameters(self):
76
+ super().set_gguf_parameters()
77
+ if (moe_intermediate_size := self.hparams.get("moe_intermediate_size")) is not None:
78
+ self.gguf_writer.add_expert_feed_forward_length(moe_intermediate_size)
79
+ logger.info(f"gguf: expert feed forward length = {moe_intermediate_size}")
80
+ if (shared_expert_intermediate_size := self.hparams.get('shared_expert_intermediate_size')) is not None:
81
+ self.gguf_writer.add_expert_shared_feed_forward_length(shared_expert_intermediate_size)
82
+ logger.info(f"gguf: expert shared feed forward length = {shared_expert_intermediate_size}")
83
+
84
+ _experts: list[dict[str, Tensor]] | None = None
85
+
86
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
87
+ # handle aggregated expert tensors
88
+ # GGUF stores dimensions reversed from PyTorch, so:
89
+ # PyTorch (A,B,C) -> GGUF writes [C,B,A] -> GGML reads ne={C,B,A}
90
+ # Input shapes from HF: (n_expert, n_ff_exp, n_embd) or (n_expert, n_embd, n_ff_exp)
91
+ # Expected GGML ne: {n_embd, n_ff_exp, n_expert} for gate/up, {n_ff_exp, n_embd, n_expert} for down
92
+ if name.endswith("mlp.experts.down_proj") or name.endswith("mlp.experts.down_proj.weight"):
93
+ mapped = f"{name}.weight" if not name.endswith(".weight") else name
94
+ # HF: [n_expert, n_embd, n_ff] -> GGML: {n_ff, n_embd, n_expert}
95
+ yield from super().modify_tensors(data_torch, mapped, bid)
96
+ return
97
+
98
+ if name.endswith("mlp.experts.gate_up_proj") or name.endswith("mlp.experts.gate_up_proj.weight"):
99
+ if data_torch.ndim < 3 or data_torch.shape[-2] % 2 != 0:
100
+ raise ValueError(f"Unexpected gate_up_proj shape for {name}: {tuple(data_torch.shape)}")
101
+ # HF: [n_expert, 2*n_ff, n_embd] -> split on dim=-2
102
+ n_ff = data_torch.shape[-2] // 2
103
+ gate = data_torch[..., :n_ff, :].contiguous()
104
+ up = data_torch[..., n_ff:, :].contiguous()
105
+ # gate/up: [n_expert, n_ff, n_embd] -> GGML: {n_embd, n_ff, n_expert}
106
+ base_name = name.removesuffix(".weight").removesuffix(".gate_up_proj")
107
+ mapped_gate = f"{base_name}.gate_proj.weight"
108
+ mapped_up = f"{base_name}.up_proj.weight"
109
+ yield from super().modify_tensors(gate, mapped_gate, bid)
110
+ yield from super().modify_tensors(up, mapped_up, bid)
111
+ return
112
+
113
+ if name.find("experts") != -1:
114
+ n_experts = self.find_hparam(["num_local_experts", "num_experts"])
115
+ assert bid is not None
116
+
117
+ if self._experts is None:
118
+ self._experts = [{} for _ in range(self.block_count)]
119
+
120
+ self._experts[bid][name] = data_torch
121
+
122
+ if len(self._experts[bid]) >= n_experts * 3:
123
+ # merge the experts into a single 3d tensor
124
+ for w_name in ["down_proj", "gate_proj", "up_proj"]:
125
+ datas: list[Tensor] = []
126
+
127
+ for xid in range(n_experts):
128
+ ename = f"model.layers.{bid}.mlp.experts.{xid}.{w_name}.weight"
129
+ datas.append(self._experts[bid][ename])
130
+ del self._experts[bid][ename]
131
+
132
+ data_torch = torch.stack(datas, dim=0)
133
+
134
+ merged_name = f"model.layers.{bid}.mlp.experts.{w_name}.weight"
135
+
136
+ yield from super().modify_tensors(data_torch, merged_name, bid)
137
+ return
138
+ else:
139
+ return
140
+
141
+ yield from super().modify_tensors(data_torch, name, bid)
142
+
143
+ def prepare_tensors(self):
144
+ super().prepare_tensors()
145
+
146
+ if self._experts is not None:
147
+ # flatten `list[dict[str, Tensor]]` into `list[str]`
148
+ experts = [k for d in self._experts for k in d.keys()]
149
+ if len(experts) > 0:
150
+ raise ValueError(f"Unprocessed experts: {experts}")
151
+
152
+
153
+ @ModelBase.register("Qwen3ForCausalLM", "Qwen3Model")
154
+ class Qwen3Model(Qwen2Model):
155
+ model_arch = gguf.MODEL_ARCH.QWEN3
156
+
157
+ # extra logic for rerank models
158
+ is_rerank: bool = False
159
+ is_tied_embeddings: bool = False
160
+ token_false_id: int | None = None
161
+ token_true_id: int | None = None
162
+
163
+ def __init__(self, *args, **kwargs):
164
+ super().__init__(*args, **kwargs)
165
+
166
+ # track for intern-s1-mini
167
+ hparams = ModelBase.load_hparams(self.dir_model, is_mistral_format=False)
168
+ self.origin_hf_arch = hparams.get('architectures', [None])[0]
169
+
170
+ if self._is_qwen3_reranker():
171
+ self._find_rerank_config()
172
+
173
+ def _is_qwen3_reranker(self) -> bool:
174
+ readme_path = self.dir_model / "README.md"
175
+ readme_text = ""
176
+ if readme_path.exists():
177
+ with readme_path.open("r", encoding="utf-8") as f:
178
+ readme_text = f.read()
179
+
180
+ name_hints = [
181
+ str(self.dir_model.name),
182
+ str(self.hparams.get("_name_or_path", "")),
183
+ str(self.hparams.get("model_type", "")),
184
+ str(self.origin_hf_arch or ""),
185
+ ]
186
+ name_hints = [hint.lower() for hint in name_hints if hint]
187
+
188
+ if "# qwen3-reranker" in readme_text.lower() or "# qwen3-vl-reranker" in readme_text.lower():
189
+ return True
190
+
191
+ if any("qwen3-reranker" in hint or "qwen3-vl-reranker" in hint for hint in name_hints):
192
+ return True
193
+
194
+ return "sequenceclassification" in (self.origin_hf_arch or "").lower()
195
+
196
+ def set_vocab(self):
197
+ # deal with intern-s1-mini
198
+ if self.origin_hf_arch == 'InternS1ForConditionalGeneration':
199
+ self._set_vocab_interns1()
200
+ return
201
+
202
+ super().set_vocab()
203
+
204
+ def _find_rerank_config(self):
205
+ from transformers import AutoTokenizer
206
+ tokenizer = AutoTokenizer.from_pretrained(self.dir_model)
207
+
208
+ self.is_rerank = True
209
+ self.is_tied_embeddings = self.hparams.get("tie_word_embeddings", False)
210
+ self.token_false_id = tokenizer.convert_tokens_to_ids("no") # ty: ignore[unresolved-attribute, invalid-assignment]
211
+ self.token_true_id = tokenizer.convert_tokens_to_ids("yes") # ty: ignore[unresolved-attribute, invalid-assignment]
212
+ self.sep_token_id = tokenizer.convert_tokens_to_ids("|") # ty: ignore[unresolved-attribute]
213
+
214
+ assert self.token_false_id is not None and self.token_true_id is not None
215
+
216
+ def set_gguf_parameters(self):
217
+ super().set_gguf_parameters()
218
+ if self.is_rerank:
219
+ self.gguf_writer.add_pooling_type(gguf.PoolingType.RANK)
220
+ self.gguf_writer.add_classifier_output_labels(["yes", "no"])
221
+ self.gguf_writer.add_chat_template([{
222
+ "name": "rerank",
223
+ "template": "<|im_start|>system\nJudge whether the Document meets the requirements based on the Query and the Instruct provided. Note that the answer can only be \"yes\" or \"no\".<|im_end|>\n"
224
+ "<|im_start|>user\n<Instruct>: Given a web search query, retrieve relevant passages that answer the query\n<Query>: {query}\n<Document>: {document}<|im_end|>\n"
225
+ "<|im_start|>assistant\n<think>\n\n</think>\n\n"
226
+ }])
227
+
228
+ def _get_cls_out_tensor(self, data_torch: Tensor) -> Tensor:
229
+ # extract "yes" and "no" tokens from the output lm_head tensor
230
+ false_row = data_torch[self.token_false_id]
231
+ true_row = data_torch[self.token_true_id]
232
+ return torch.stack([true_row, false_row], dim=0)
233
+
234
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
235
+ if self.is_rerank:
236
+ is_tied_head = self.is_tied_embeddings and "embed_tokens" in name
237
+ is_real_head = not self.is_tied_embeddings and "lm_head" in name
238
+ if is_tied_head or is_real_head:
239
+ cls_out_head = (
240
+ gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.CLS_OUT] + ".weight",
241
+ self._get_cls_out_tensor(data_torch),
242
+ )
243
+ yield cls_out_head
244
+ if is_tied_head:
245
+ yield from super().modify_tensors(data_torch, name, bid)
246
+ return
247
+
248
+ yield from super().modify_tensors(data_torch, name, bid)
249
+
250
+
251
+ @ModelBase.register("Qwen3MoeForCausalLM")
252
+ class Qwen3MoeModel(Qwen2MoeModel):
253
+ model_arch = gguf.MODEL_ARCH.QWEN3MOE
254
+
255
+ def __init__(self, *args, **kwargs):
256
+ super().__init__(*args, **kwargs)
257
+ hparams = ModelBase.load_hparams(self.dir_model, False)
258
+ self.origin_hf_arch = hparams.get('architectures', [None])[0]
259
+
260
+ def set_vocab(self):
261
+ # deal with intern-s1
262
+ if self.origin_hf_arch == 'InternS1ForConditionalGeneration':
263
+ self._set_vocab_interns1()
264
+ return
265
+
266
+ super().set_vocab()
267
+
268
+
269
+ @ModelBase.register("Qwen3NextForCausalLM")
270
+ class Qwen3NextModel(Qwen2MoeModel):
271
+ model_arch = gguf.MODEL_ARCH.QWEN3NEXT
272
+
273
+ def set_gguf_parameters(self):
274
+ super().set_gguf_parameters()
275
+ self.gguf_writer.add_ssm_conv_kernel(self.hparams["linear_conv_kernel_dim"])
276
+ self.gguf_writer.add_ssm_state_size(self.hparams["linear_key_head_dim"])
277
+ self.gguf_writer.add_ssm_group_count(self.hparams["linear_num_key_heads"])
278
+ self.gguf_writer.add_ssm_time_step_rank(self.hparams["linear_num_value_heads"])
279
+ self.gguf_writer.add_ssm_inner_size(self.hparams["linear_value_head_dim"] * self.hparams["linear_num_value_heads"])
280
+ self.gguf_writer.add_full_attention_interval(self.hparams.get("full_attention_interval", 4))
281
+ if (rope_dim := self.hparams.get("head_dim")) is None:
282
+ rope_dim = self.hparams["hidden_size"] // self.hparams["num_attention_heads"]
283
+ self.gguf_writer.add_rope_dimension_count(int(rope_dim * self.rope_parameters.get("partial_rotary_factor", 0.25)))
284
+
285
+ @classmethod
286
+ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
287
+ name, gen = item
288
+
289
+ if name.startswith("mtp"):
290
+ # ignore MTP layers for now
291
+ return None
292
+
293
+ return super().filter_tensors(item)
294
+
295
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
296
+ if name.endswith(".A_log"):
297
+ data_torch = -torch.exp(data_torch)
298
+ elif name.endswith(".dt_bias"):
299
+ name = name.rpartition(".dt_bias")[0] + ".dt_proj.bias"
300
+ elif "conv1d" in name:
301
+ data_torch = data_torch.squeeze()
302
+ elif name.endswith("norm.weight") and not name.endswith("linear_attn.norm.weight"):
303
+ data_torch = data_torch + 1
304
+
305
+ if "in_proj_qkvz.weight" in name:
306
+ # original order: [q, k, v, z] * head_count
307
+ # corrected order: [q * head_count, k * head_count, v * head_count, z * head_count]
308
+ head_k_dim = self.hparams["linear_key_head_dim"]
309
+ head_v_dim = self.hparams["linear_value_head_dim"]
310
+ num_v_heads = self.hparams["linear_num_value_heads"]
311
+ num_k_heads = self.hparams["linear_num_key_heads"]
312
+ hidden_size = self.hparams["hidden_size"]
313
+ split_arg_list_qkvz = [
314
+ head_k_dim, # q partition
315
+ head_k_dim, # k partition
316
+ (num_v_heads // num_k_heads * head_v_dim), # v partition
317
+ (num_v_heads // num_k_heads * head_v_dim), # z partition
318
+ ]
319
+ # view as (n_embd, head_count, [q+k+v+z])
320
+ data_torch = data_torch.permute(1, 0).contiguous()
321
+ data_torch = data_torch.view(-1, num_k_heads, sum(split_arg_list_qkvz))
322
+ # split into q, k, v, z
323
+ q, k, v, z = torch.split(data_torch, split_arg_list_qkvz, dim=-1)
324
+ # flatten dim + head_count
325
+ q = q.contiguous().view(hidden_size, -1)
326
+ k = k.contiguous().view(hidden_size, -1)
327
+ v = v.contiguous().view(hidden_size, -1)
328
+ z = z.contiguous().view(hidden_size, -1)
329
+ # stack back
330
+ qkv = torch.cat([q, k, v], dim=-1).permute(1, 0).contiguous()
331
+ z = z.permute(1, 0).contiguous()
332
+ yield (self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_QKV, bid, ".weight"), qkv)
333
+ yield (self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_GATE, bid, ".weight"), z)
334
+ else:
335
+ yield from super().modify_tensors(data_torch, name, bid)
336
+
337
+
338
+ @ModelBase.register("RND1")
339
+ class RND1Model(Qwen2MoeModel):
340
+ model_arch = gguf.MODEL_ARCH.RND1
341
+
342
+ def set_gguf_parameters(self):
343
+ super().set_gguf_parameters()
344
+
345
+ # RND1 specific parameters
346
+ # RND1 uses bidirectional attention
347
+ self.gguf_writer.add_causal_attention(False)
348
+
349
+ if (mask_token_id := self.hparams.get("mask_token_id")) is not None:
350
+ self.gguf_writer.add_mask_token_id(mask_token_id)
351
+
352
+
353
+ class _LinearAttentionVReorderBase(Qwen3NextModel):
354
+ model_arch = gguf.MODEL_ARCH.QWEN3NEXT # overridden by subclasses
355
+ """reorders V heads from grouped to tiled order for ggml broadcast
356
+
357
+ see https://github.com/ggml-org/llama.cpp/pull/19468#discussion_r2786394306
358
+
359
+ Linear attention may has num_k_heads < num_v_heads. The HF weights store
360
+ V heads grouped by K head: [G0_v0..v{r-1}, G1_v0..v{r-1}, ...].
361
+ ggml binary ops use tiled broadcast: [K0, K1, ..., K0, K1, ...].
362
+ We reorder V heads to tiled order so ggml_repeat can replace the expensive
363
+ interleaved repeat: [G0_v0, G1_v0, ..., G0_v1, G1_v1, ...].
364
+ """
365
+
366
+ @staticmethod
367
+ def _reorder_v_heads(tensor: Tensor, dim: int, num_k_heads: int, num_v_per_k: int, head_dim: int) -> Tensor:
368
+ """Reorder V heads from grouped (by K head) to tiled order along the given dimension."""
369
+ shape = list(tensor.shape)
370
+ if dim < 0:
371
+ dim += len(shape)
372
+ new_shape = shape[:dim] + [num_k_heads, num_v_per_k, head_dim] + shape[dim + 1:]
373
+ tensor = tensor.reshape(*new_shape)
374
+ perm = list(range(len(new_shape)))
375
+ perm[dim], perm[dim + 1] = perm[dim + 1], perm[dim]
376
+ return tensor.permute(*perm).contiguous().reshape(*shape)
377
+
378
+ def _transform_nvfp4_weight(self, name: str, weight: Tensor, scale: Tensor) -> tuple[Tensor, Tensor]:
379
+ if not name.endswith((
380
+ ".linear_attn.in_proj_qkv.weight",
381
+ ".linear_attn.in_proj_z.weight",
382
+ ".linear_attn.in_proj_a.weight",
383
+ ".linear_attn.in_proj_b.weight",
384
+ ".linear_attn.out_proj.weight",
385
+ )):
386
+ return weight, scale
387
+
388
+ num_k_heads = self.hparams["linear_num_key_heads"]
389
+ num_v_heads = self.hparams["linear_num_value_heads"]
390
+ head_k_dim = self.hparams["linear_key_head_dim"]
391
+ head_v_dim = self.hparams["linear_value_head_dim"]
392
+ num_v_per_k = num_v_heads // num_k_heads
393
+
394
+ def unpack_nibbles(qs: Tensor) -> Tensor:
395
+ lo = torch.bitwise_and(qs, 0x0F)
396
+ hi = torch.bitwise_right_shift(qs, 4)
397
+ return torch.stack((lo, hi), dim=-1).reshape(*qs.shape[:-1], qs.shape[-1] * 2)
398
+
399
+ def pack_nibbles(codes: Tensor) -> Tensor:
400
+ codes = codes.reshape(*codes.shape[:-1], codes.shape[-1] // 2, 2)
401
+ lo = torch.bitwise_and(codes[..., 0], 0x0F)
402
+ hi = torch.bitwise_left_shift(torch.bitwise_and(codes[..., 1], 0x0F), 4)
403
+ return torch.bitwise_or(lo, hi).contiguous()
404
+
405
+ def apply_col_perm(qs: Tensor, scales: Tensor, col_perm: Tensor) -> tuple[Tensor, Tensor]:
406
+ assert qs.ndim >= 2
407
+ assert scales.ndim >= 2
408
+
409
+ k = qs.shape[-1] * 2
410
+ assert col_perm.numel() == k
411
+ assert k % 16 == 0
412
+
413
+ group_cols = col_perm.reshape(-1, 16)
414
+ group_starts = group_cols[:, 0]
415
+ expected = group_starts.unsqueeze(1) + torch.arange(16, dtype=col_perm.dtype)
416
+ assert torch.equal(group_cols, expected)
417
+ assert torch.all(group_starts % 16 == 0)
418
+
419
+ group_perm = (group_starts // 16).to(dtype=torch.long)
420
+ expected_groups = torch.arange(scales.shape[-1], dtype=torch.long)
421
+ assert group_perm.numel() == scales.shape[-1]
422
+ assert torch.equal(torch.sort(group_perm).values, expected_groups)
423
+
424
+ codes = unpack_nibbles(qs)
425
+ codes = codes.index_select(-1, col_perm.to(device=qs.device, dtype=torch.long))
426
+ qs = pack_nibbles(codes)
427
+ scales = scales.index_select(-1, group_perm.to(device=scales.device))
428
+ return qs, scales
429
+
430
+ def reorder_rows(qs: Tensor, scales: Tensor, head_dim: int) -> tuple[Tensor, Tensor]:
431
+ row_perm = self._reorder_v_heads(
432
+ torch.arange(num_v_heads * head_dim, dtype=torch.long).unsqueeze(-1),
433
+ 0, num_k_heads, num_v_per_k, head_dim,
434
+ ).squeeze(-1)
435
+ return (
436
+ qs.index_select(0, row_perm.to(device=qs.device)),
437
+ scales.index_select(0, row_perm.to(device=scales.device)),
438
+ )
439
+
440
+ if name.endswith(".linear_attn.in_proj_qkv.weight"):
441
+ q_dim = head_k_dim * num_k_heads
442
+ k_dim = head_k_dim * num_k_heads
443
+ q = weight[:q_dim]
444
+ k = weight[q_dim:q_dim + k_dim]
445
+ v = weight[q_dim + k_dim:]
446
+ q_scale = scale[:q_dim]
447
+ k_scale = scale[q_dim:q_dim + k_dim]
448
+ v_scale = scale[q_dim + k_dim:]
449
+ v, v_scale = reorder_rows(v, v_scale, head_v_dim)
450
+ return torch.cat([q, k, v], dim=0), torch.cat([q_scale, k_scale, v_scale], dim=0)
451
+
452
+ if name.endswith(".linear_attn.in_proj_z.weight"):
453
+ weight, scale = reorder_rows(weight, scale, head_v_dim)
454
+ elif name.endswith((".linear_attn.in_proj_a.weight", ".linear_attn.in_proj_b.weight")):
455
+ weight, scale = reorder_rows(weight, scale, 1)
456
+ elif name.endswith(".linear_attn.out_proj.weight"):
457
+ col_perm = self._reorder_v_heads(
458
+ torch.arange(num_v_heads * head_v_dim, dtype=torch.long).unsqueeze(0),
459
+ 1, num_k_heads, num_v_per_k, head_v_dim,
460
+ ).squeeze(0)
461
+ weight, scale = apply_col_perm(weight, scale, col_perm)
462
+
463
+ return weight, scale
464
+
465
+ def _repack_nvfp4(self, name: str, weight: Tensor, scale: Tensor, scale2: Tensor, input_scale: Tensor):
466
+ weight, scale = self._transform_nvfp4_weight(name, weight, scale)
467
+ super()._repack_nvfp4(name, weight, scale, scale2, input_scale)
468
+
469
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
470
+ num_k_heads = self.hparams.get("linear_num_key_heads", 0)
471
+ num_v_heads = self.hparams.get("linear_num_value_heads", 0)
472
+
473
+ if num_k_heads > 0 and num_v_heads > 0 and num_k_heads != num_v_heads and "linear_attn." in name:
474
+ head_k_dim = self.hparams["linear_key_head_dim"]
475
+ head_v_dim = self.hparams["linear_value_head_dim"]
476
+ num_v_per_k = num_v_heads // num_k_heads
477
+
478
+ if ".in_proj_qkv." in name:
479
+ # QKV weight: reorder only the V rows
480
+ q_dim = head_k_dim * num_k_heads
481
+ k_dim = head_k_dim * num_k_heads
482
+ q = data_torch[:q_dim]
483
+ k = data_torch[q_dim:q_dim + k_dim]
484
+ v = data_torch[q_dim + k_dim:]
485
+ v = self._reorder_v_heads(v, 0, num_k_heads, num_v_per_k, head_v_dim)
486
+ data_torch = torch.cat([q, k, v], dim=0)
487
+
488
+ elif ".in_proj_z." in name:
489
+ # Z gate weight: reorder rows (num_v_heads * head_v_dim)
490
+ data_torch = self._reorder_v_heads(data_torch, 0, num_k_heads, num_v_per_k, head_v_dim)
491
+
492
+ elif ".in_proj_b." in name or ".in_proj_a." in name:
493
+ # Beta/Alpha weight: reorder rows (num_v_heads, head_dim=1)
494
+ data_torch = self._reorder_v_heads(data_torch, 0, num_k_heads, num_v_per_k, 1)
495
+
496
+ elif ".A_log" in name or ".dt_bias" in name or ".dt_proj" in name:
497
+ # A_log / dt_bias: 1D parameters with num_v_heads elements
498
+ if data_torch.ndim == 1:
499
+ data_torch = self._reorder_v_heads(
500
+ data_torch.unsqueeze(-1), 0, num_k_heads, num_v_per_k, 1
501
+ ).squeeze(-1)
502
+ else:
503
+ data_torch = self._reorder_v_heads(data_torch, -1, num_k_heads, num_v_per_k, 1)
504
+
505
+ elif ".conv1d" in name:
506
+ # Conv1d kernel: reorder only the V channel portion
507
+ data = data_torch.squeeze()
508
+ qk_channels = head_k_dim * num_k_heads * 2
509
+ qk_part = data[:qk_channels]
510
+ v_part = data[qk_channels:]
511
+ v_part = self._reorder_v_heads(v_part, 0, num_k_heads, num_v_per_k, head_v_dim)
512
+ data_torch = torch.cat([qk_part, v_part], dim=0)
513
+
514
+ elif ".out_proj." in name:
515
+ # Out projection weight: reorder columns (input dimension)
516
+ data_torch = self._reorder_v_heads(data_torch, 1, num_k_heads, num_v_per_k, head_v_dim)
517
+
518
+ yield from super().modify_tensors(data_torch, name, bid)
519
+
520
+
521
+ class _Qwen35MRopeMixin:
522
+ # Qwen3.5 always applies interleaved MRoPE (see Qwen3_5RotaryEmbedding in transformers);
523
+ # the upstream default mrope_section is [11, 11, 10] and llama.cpp's QWEN35 / QWEN35MOE
524
+ # loaders treat qwen35.rope.dimension_sections as required, so make sure it is always
525
+ # written even when a particular checkpoint omits the field in `rope_parameters`.
526
+ _QWEN35_DEFAULT_MROPE_SECTION = [11, 11, 10, 0]
527
+
528
+ gguf_writer: gguf.GGUFWriter
529
+ rope_parameters: dict
530
+
531
+ def set_gguf_parameters(self):
532
+ super().set_gguf_parameters() # ty: ignore[unresolved-attribute]
533
+ if "mrope_section" not in self.rope_parameters:
534
+ self.gguf_writer.add_rope_dimension_sections(self._QWEN35_DEFAULT_MROPE_SECTION)
535
+
536
+
537
+ class _Qwen35MtpMixin:
538
+ """Shared MTP wiring for Qwen3.5/3.6 text variants. The HF config carries
539
+ the MTP block under `mtp_num_hidden_layers` and the tensors under
540
+ `mtp.*`; we extend block_count, emit the nextn metadata key, and remap
541
+ `mtp.*` to the standard layer-indexed nextn naming so the existing
542
+ tensor_map handles them."""
543
+
544
+ hparams: dict[str, Any]
545
+ model_arch: gguf.MODEL_ARCH
546
+ gguf_writer: gguf.GGUFWriter
547
+ block_count: int
548
+ tensor_map: gguf.TensorNameMap
549
+ no_mtp: bool
550
+ mtp_only: bool
551
+ _original_block_count: int | None = None
552
+
553
+ def __init__(self, *args, **kwargs):
554
+ super().__init__(*args, **kwargs)
555
+ self.block_count = self.hparams["num_hidden_layers"]
556
+ if not self.no_mtp:
557
+ self.block_count += self.hparams.get("mtp_num_hidden_layers", 0)
558
+ self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count)
559
+
560
+ def index_tensors(self, remote_hf_model_id: str | None = None) -> dict[str, Callable[[], Tensor]]:
561
+ hparams = {**self.hparams, **self.hparams.get("text_config", {})}
562
+ key = next((k for k in ["n_layers", "num_hidden_layers", "n_layer", "num_layers"] if k in hparams), None)
563
+ type(self)._original_block_count = hparams.get(key)
564
+ return super().index_tensors(remote_hf_model_id=remote_hf_model_id) # ty: ignore[unresolved-attribute]
565
+
566
+ @classmethod
567
+ def filter_tensors(cls, item):
568
+ assert cls._original_block_count is not None
569
+ # TODO: change TextModel to super()
570
+ if (titem := TextModel.filter_tensors(item)) is None:
571
+ return None
572
+ name, gen = titem
573
+ if name.startswith("model.mtp."):
574
+ name = name.replace("model.", "", 1)
575
+ if name.startswith("mtp."):
576
+ if cls.no_mtp:
577
+ return None
578
+ remapper = {
579
+ "fc": "eh_proj",
580
+ "pre_fc_norm_embedding": "enorm",
581
+ "pre_fc_norm_hidden": "hnorm",
582
+ "norm": "shared_head.norm",
583
+ }
584
+ parts = name.split(".", 3)
585
+ if len(parts) == 4 and parts[1] == "layers" and parts[2].isdecimal():
586
+ mtp_idx = int(parts[2])
587
+ name = f"model.layers.{cls._original_block_count + mtp_idx}.{parts[3]}"
588
+ elif len(parts) == 3 and parts[1] in remapper:
589
+ name = f"model.layers.{cls._original_block_count}.{remapper[parts[1]]}.{parts[2]}"
590
+ elif cls.mtp_only:
591
+ keep = name in (
592
+ "model.embed_tokens.weight", "model.norm.weight", "lm_head.weight",
593
+ "embed_tokens.weight", "norm.weight",
594
+ )
595
+ if not keep:
596
+ return None
597
+ return name, gen
598
+
599
+ def set_gguf_parameters(self):
600
+ super().set_gguf_parameters() # ty: ignore[unresolved-attribute]
601
+ if self.no_mtp:
602
+ return
603
+ if (n := self.hparams.get("mtp_num_hidden_layers", 0)) > 0:
604
+ self.gguf_writer.add_nextn_predict_layers(n)
605
+
606
+ def prepare_metadata(self, vocab_only: bool):
607
+ from_dir = self.fname_out.is_dir()
608
+ super().prepare_metadata(vocab_only=vocab_only) # ty: ignore[unresolved-attribute]
609
+
610
+ if not self.mtp_only or not from_dir:
611
+ return
612
+
613
+ output_type: str = self.ftype.name.partition("_")[2] # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
614
+ fname_default: str = gguf.naming_convention(
615
+ self.metadata.name, self.metadata.basename, self.metadata.finetune, # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
616
+ self.metadata.version, size_label=None, output_type=output_type, model_type=None) # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
617
+ self.fname_out = self.fname_out.parent / f"mtp-{fname_default}.gguf"
618
+
619
+
620
+ @ModelBase.register("Qwen3_5ForConditionalGeneration", "Qwen3_5ForCausalLM")
621
+ class Qwen3_5TextModel(_Qwen35MtpMixin, _Qwen35MRopeMixin, _LinearAttentionVReorderBase):
622
+ model_arch = gguf.MODEL_ARCH.QWEN35
623
+
624
+
625
+ @ModelBase.register("Qwen3_5MoeForConditionalGeneration", "Qwen3_5MoeForCausalLM")
626
+ class Qwen3_5MoeTextModel(_Qwen35MtpMixin, _Qwen35MRopeMixin, _LinearAttentionVReorderBase):
627
+ model_arch = gguf.MODEL_ARCH.QWEN35MOE
628
+
629
+
630
+ @ModelBase.register("DFlashDraftModel")
631
+ class DFlashModel(Qwen3Model):
632
+ model_arch = gguf.MODEL_ARCH.DFLASH
633
+
634
+ def set_vocab(self):
635
+ if self.target_model_dir is None:
636
+ raise ValueError(
637
+ "DFlash draft model requires --target-model-dir to be specified. "
638
+ "Please provide the path to the target model directory containing the tokenizer."
639
+ )
640
+ logger.info(f"DFlash: Using tokenizer from target model: {self.target_model_dir}")
641
+ original_dir = self.dir_model
642
+ self.dir_model = self.target_model_dir
643
+ super().set_vocab()
644
+ self.dir_model = original_dir
645
+
646
+ mask_token_id = self.hparams.get("dflash_config", {}).get("mask_token_id")
647
+ if mask_token_id is not None:
648
+ self.gguf_writer.add_mask_token_id(mask_token_id)
649
+
650
+ def set_gguf_parameters(self):
651
+ super().set_gguf_parameters()
652
+
653
+ block_size = self.hparams.get("block_size", 16)
654
+ self.gguf_writer.add_block_size(block_size)
655
+ dflash_config = self.hparams.get("dflash_config", {})
656
+
657
+ target_layer_ids = dflash_config.get("target_layer_ids", [])
658
+ if target_layer_ids:
659
+ extract_layer_ids = [i + 1 for i in target_layer_ids]
660
+ self.gguf_writer.add_target_layers(extract_layer_ids)
661
+
662
+ use_sliding_window = self.hparams.get("use_sliding_window", False)
663
+ sliding_window = self.hparams.get("sliding_window")
664
+ layer_types = self.hparams.get("layer_types")
665
+ if use_sliding_window and sliding_window and layer_types:
666
+ is_swa = [lt == "sliding_attention" for lt in layer_types]
667
+ self.gguf_writer.add_sliding_window(sliding_window)
668
+ self.gguf_writer.add_sliding_window_pattern(is_swa)
669
+
670
+ @classmethod
671
+ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
672
+ name, gen = item
673
+ if not name.startswith("model."):
674
+ name = "model." + name
675
+ return super().filter_tensors((name, gen))
llama.cpp/conversion/qwen3vl.py ADDED
@@ -0,0 +1,360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ from typing import Any, Callable, Iterable, TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from torch import Tensor
9
+
10
+ from .base import MmprojModel, ModelBase, gguf, logger
11
+
12
+ from .qwen import Qwen3Model, Qwen3MoeModel
13
+ from .qwenvl import Qwen25AudioModel
14
+
15
+
16
+ @ModelBase.register("Qwen3VLForConditionalGeneration", "Qwen3VLMoeForConditionalGeneration", "Qwen3_5ForConditionalGeneration", "Qwen3_5MoeForConditionalGeneration")
17
+ class Qwen3VLVisionModel(MmprojModel):
18
+ def __init__(self, *args, **kwargs):
19
+ super().__init__(*args, **kwargs)
20
+ if self.hparams_vision is None:
21
+ logger.info("No vision config found, skipping vision tensor processing")
22
+ return
23
+
24
+ # Compute image_size if not present
25
+ if "image_size" not in self.hparams_vision:
26
+ # For Qwen3VL/Qwen3VLMoe, compute from num_position_embeddings
27
+ num_pos = self.hparams_vision.get("num_position_embeddings", 2304)
28
+ patch_size = self.hparams_vision.get("patch_size", 16)
29
+ # num_position_embeddings = (image_size / patch_size) ** 2
30
+ # So image_size = sqrt(num_position_embeddings) * patch_size
31
+ image_size = int(num_pos**0.5 * patch_size)
32
+ self.hparams_vision["image_size"] = image_size
33
+
34
+ # Rename config values for compatibility
35
+ self.hparams_vision["num_attention_heads"] = self.hparams_vision.get("num_heads")
36
+ self.hparams_vision["num_hidden_layers"] = self.hparams_vision.get("depth")
37
+
38
+ self.is_deepstack_layers = [False] * int(self.hparams_vision["num_hidden_layers"] or 0)
39
+ for idx in self.hparams_vision.get("deepstack_visual_indexes", []):
40
+ self.is_deepstack_layers[idx] = True
41
+
42
+ def set_gguf_parameters(self):
43
+ super().set_gguf_parameters()
44
+ # in case mixed modalities, the arch will be handled by subclass
45
+ if not self.has_audio_encoder:
46
+ self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.QWEN3VL)
47
+ self.gguf_writer.add_vision_use_gelu(True)
48
+
49
+ if self.hparams_vision is not None:
50
+ merge_size = self.hparams_vision.get("spatial_merge_size")
51
+ if merge_size is not None:
52
+ self.gguf_writer.add_vision_spatial_merge_size(int(merge_size))
53
+
54
+ # Use text config's rms_norm_eps for vision attention layernorm eps
55
+ rms_norm_eps = self.global_config.get("text_config", {}).get("rms_norm_eps", 1e-6)
56
+ self.gguf_writer.add_vision_attention_layernorm_eps(rms_norm_eps)
57
+
58
+ if self.is_deepstack_layers:
59
+ self.gguf_writer.add_vision_is_deepstack_layers(self.is_deepstack_layers)
60
+
61
+ @classmethod
62
+ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
63
+ name, gen = item
64
+
65
+ # Skip text model tensors
66
+ if name.startswith("lm_head."):
67
+ return None
68
+
69
+ # Skip MTP tensors
70
+ if name.startswith("mtp."):
71
+ return None
72
+
73
+ if name.startswith("model.visual."):
74
+ name = name.replace("model.visual.", "visual.", 1)
75
+
76
+ if not name.startswith("visual."):
77
+ return None
78
+
79
+ return super().filter_tensors((name, gen))
80
+
81
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
82
+ assert self.hparams_vision is not None
83
+
84
+ if name.startswith("visual.deepstack_merger_list."):
85
+ prefix, rest = name.split(".", maxsplit=3)[2:]
86
+ # prefix is the layer index, convert to absolute clip layer index!
87
+ idx = self.hparams_vision.get("deepstack_visual_indexes", [])[int(prefix)]
88
+ target = rest
89
+
90
+ tensor_type: gguf.MODEL_TENSOR
91
+ if target.startswith("norm."):
92
+ tensor_type = gguf.MODEL_TENSOR.V_DS_NORM
93
+ suffix = target.split(".", 1)[1]
94
+ elif target.startswith("linear_fc1."):
95
+ tensor_type = gguf.MODEL_TENSOR.V_DS_FC1
96
+ suffix = target.split(".", 1)[1]
97
+ elif target.startswith("linear_fc2."):
98
+ tensor_type = gguf.MODEL_TENSOR.V_DS_FC2
99
+ suffix = target.split(".", 1)[1]
100
+ else:
101
+ raise ValueError(f"Unexpected deepstack tensor: {name}")
102
+
103
+ new_name = self.format_tensor_name(tensor_type, idx, suffix=f".{suffix}")
104
+ yield from super().modify_tensors(data_torch, new_name, bid)
105
+ return
106
+
107
+ if name.startswith("visual.merger."):
108
+ suffix = name.split(".", 2)[2]
109
+ if suffix.startswith("linear_fc"):
110
+ fc_idx_str, tail = suffix.split(".", 1)
111
+ fc_num = int(fc_idx_str.replace("linear_fc", ""))
112
+ # Qwen3VL has linear_fc1 and linear_fc2
113
+ # Map to indices 0 and 2 (matching Qwen2VL which uses indices 0 and 2)
114
+ if fc_num == 1:
115
+ fc_idx = 0
116
+ elif fc_num == 2:
117
+ fc_idx = 2
118
+ else:
119
+ raise ValueError(f"unexpected fc index {fc_num} in {name}")
120
+ new_name = self.format_tensor_name(gguf.MODEL_TENSOR.V_MMPROJ, fc_idx, suffix=f".{tail}")
121
+ elif suffix.startswith("norm."):
122
+ new_name = self.format_tensor_name(gguf.MODEL_TENSOR.V_POST_NORM, suffix=f".{suffix.split('.', 1)[1]}")
123
+ else:
124
+ raise ValueError(f"Unexpected merger tensor: {name}")
125
+ yield (new_name, data_torch)
126
+ return
127
+
128
+ if name == "visual.patch_embed.proj.weight":
129
+ # split Conv3D into Conv2Ds along temporal dimension
130
+ c1, c2, kt, _, _ = data_torch.shape
131
+ del c1, c2
132
+ if kt != 2:
133
+ raise ValueError("Current implementation only supports temporal_patch_size of 2")
134
+ yield (gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.V_ENC_EMBD_PATCH] + ".weight", data_torch[:, :, 0, ...])
135
+ yield (gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.V_ENC_EMBD_PATCH] + ".weight.1", data_torch[:, :, 1, ...])
136
+ return
137
+
138
+ if name == "visual.patch_embed.proj.bias":
139
+ # Include the bias - it's used by the C++ code
140
+ yield (gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.V_ENC_EMBD_PATCH] + ".bias", data_torch)
141
+ return
142
+
143
+ yield from MmprojModel.modify_tensors(self, data_torch, name, bid)
144
+
145
+
146
+ @ModelBase.register("Qwen3OmniMoeForConditionalGeneration")
147
+ class Qwen3OmniMmprojModel(Qwen3VLVisionModel, Qwen25AudioModel):
148
+ has_audio_encoder = True
149
+ has_vision_encoder = True
150
+
151
+ def get_vision_config(self) -> dict[str, Any] | None:
152
+ if self.has_vision_encoder:
153
+ return self.global_config["thinker_config"].get("vision_config")
154
+ else:
155
+ return None
156
+
157
+ def get_audio_config(self) -> dict[str, Any] | None:
158
+ if self.has_audio_encoder:
159
+ return self.global_config["thinker_config"].get("audio_config")
160
+ else:
161
+ return None
162
+
163
+ def set_gguf_parameters(self):
164
+ if self.has_vision_encoder:
165
+ Qwen3VLVisionModel.set_gguf_parameters(self)
166
+ self.gguf_writer.add_clip_vision_projector_type(gguf.VisionProjectorType.QWEN3VL)
167
+ if self.has_audio_encoder:
168
+ Qwen25AudioModel.set_gguf_parameters(self)
169
+ self.gguf_writer.add_clip_audio_projector_type(gguf.VisionProjectorType.QWEN3A)
170
+
171
+ @classmethod
172
+ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
173
+ name, gen = item
174
+
175
+ # Skip text model tensors
176
+ if name.startswith("lm_head."):
177
+ return None
178
+
179
+ # Skip MTP tensors
180
+ if name.startswith("mtp."):
181
+ return None
182
+
183
+ if name.startswith("model.visual."):
184
+ name = name.replace("model.visual.", "visual.", 1)
185
+
186
+ if name.startswith("thinker.audio_tower."):
187
+ name = name.replace("thinker.audio_tower.", "audio_tower.", 1)
188
+
189
+ if "visual." not in name and "audio_tower." not in name:
190
+ return None
191
+
192
+ return MmprojModel.filter_tensors((name, gen))
193
+
194
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
195
+ if "visual." in name:
196
+ if not self.has_vision_encoder:
197
+ raise ValueError(f"Model does not have vision encoder, but found tensor {name}")
198
+ # need to transform vision tensor naming, so that modify_tensors() logic can be used correctly
199
+ name = name.replace("thinker.visual.", "model.visual.")
200
+ if ".merger_list." in name:
201
+ name = name.replace(".merger_list.", ".deepstack_merger_list.")
202
+ name = name.replace(".ln_q", ".norm")
203
+ name = name.replace(".mlp.0", ".linear_fc1")
204
+ name = name.replace(".mlp.2", ".linear_fc2")
205
+ elif ".merger." in name:
206
+ name = name.replace(".ln_q", ".norm")
207
+ name = name.replace(".mlp.0", ".linear_fc1")
208
+ name = name.replace(".mlp.2", ".linear_fc2")
209
+ yield from Qwen3VLVisionModel.modify_tensors(self, data_torch, name, bid)
210
+ elif "audio_tower." in name:
211
+ if not self.has_audio_encoder:
212
+ raise ValueError(f"Model does not have audio encoder, but found tensor {name}")
213
+ if "conv2d" in name and name.endswith(".bias"):
214
+ # transform conv2d bias [n_embd] --> [1, 1, n_embd]
215
+ data_torch = data_torch.unsqueeze(-1).unsqueeze(-1)
216
+ yield from Qwen25AudioModel.modify_tensors(self, data_torch, name, bid)
217
+
218
+
219
+ @ModelBase.register("Qwen3ASRForConditionalGeneration")
220
+ class Qwen3ASRMmprojModel(Qwen3OmniMmprojModel):
221
+ has_audio_encoder = True
222
+ has_vision_encoder = False
223
+
224
+
225
+ @ModelBase.register("Glm4vForConditionalGeneration", "Glm4vMoeForConditionalGeneration", "GlmOcrForConditionalGeneration")
226
+ class Glm4VVisionModel(Qwen3VLVisionModel):
227
+ def set_gguf_parameters(self):
228
+ MmprojModel.set_gguf_parameters(self) # skip Qwen3VLVisionModel parameters
229
+ assert self.hparams_vision is not None
230
+ self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.GLM4V)
231
+
232
+ hidden_act = str(self.hparams_vision.get("hidden_act", "")).lower()
233
+ if hidden_act == "gelu":
234
+ self.gguf_writer.add_vision_use_gelu(True)
235
+ elif hidden_act == "silu":
236
+ self.gguf_writer.add_vision_use_silu(True)
237
+
238
+ rms_norm_eps = self.hparams_vision.get("rms_norm_eps", 1e-5)
239
+ self.gguf_writer.add_vision_attention_layernorm_eps(rms_norm_eps)
240
+
241
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
242
+ if name.startswith("visual.merger."):
243
+ yield from ModelBase.modify_tensors(self, data_torch, name, bid)
244
+ return
245
+ yield from super().modify_tensors(data_torch, name, bid)
246
+
247
+
248
+ @ModelBase.register("Qwen3VLForConditionalGeneration")
249
+ class Qwen3VLTextModel(Qwen3Model):
250
+ model_arch = gguf.MODEL_ARCH.QWEN3VL
251
+
252
+ def set_gguf_parameters(self):
253
+ super().set_gguf_parameters()
254
+ if "thinker_config" in self.hparams:
255
+ vision_config = self.hparams["thinker_config"].get("vision_config", {})
256
+ else:
257
+ vision_config = self.hparams.get("vision_config", {})
258
+ deepstack_layer_num = len(vision_config.get("deepstack_visual_indexes", []))
259
+ self.gguf_writer.add_num_deepstack_layers(deepstack_layer_num)
260
+
261
+ @classmethod
262
+ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
263
+ name, gen = item
264
+
265
+ name = name.replace("thinker.", "")
266
+
267
+ return super().filter_tensors((name, gen))
268
+
269
+
270
+ @ModelBase.register("Qwen3VLMoeForConditionalGeneration")
271
+ class Qwen3VLMoeTextModel(Qwen3MoeModel):
272
+ model_arch = gguf.MODEL_ARCH.QWEN3VLMOE
273
+
274
+ def set_gguf_parameters(self):
275
+ super().set_gguf_parameters()
276
+ vision_config = self.hparams.get("vision_config", {})
277
+ deepstack_layer_num = len(vision_config.get("deepstack_visual_indexes", []))
278
+ self.gguf_writer.add_num_deepstack_layers(deepstack_layer_num)
279
+
280
+ @classmethod
281
+ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
282
+ name, gen = item
283
+
284
+ name = name.replace("thinker.", "")
285
+
286
+ return super().filter_tensors((name, gen))
287
+
288
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
289
+ # Qwen3VL has transposed packed tensors, so we treat it differently from general Qwen2MoE packed tensors
290
+ if name.endswith("mlp.experts.down_proj") or name.endswith("mlp.experts.down_proj.weight"):
291
+ mapped = f"{name}.weight" if not name.endswith(".weight") else name
292
+ permuted = data_torch.permute(0, 2, 1).contiguous()
293
+ yield from ModelBase.modify_tensors(self, permuted, mapped, bid)
294
+ return
295
+
296
+ if name.endswith("mlp.experts.gate_up_proj") or name.endswith("mlp.experts.gate_up_proj.weight"):
297
+ if data_torch.ndim < 3 or data_torch.shape[-1] % 2 != 0:
298
+ raise ValueError(f"Unexpected gate_up_proj shape for {name}: {tuple(data_torch.shape)}")
299
+ split_dim = data_torch.shape[-1] // 2
300
+ gate = data_torch[..., :split_dim].contiguous()
301
+ up = data_torch[..., split_dim:].contiguous()
302
+ # Input gate/up: (n_expert=128, n_embd=2048, n_ff_exp=768)
303
+ # Want GGML ne: {n_embd, n_ff_exp, n_expert} = {2048, 768, 128}
304
+ # Need PyTorch: (128, 768, 2048) [reversed of GGML]
305
+ # So: permute(0, 2, 1): (128, 2048, 768) -> (128, 768, 2048)
306
+ base_name = name.removesuffix(".weight")
307
+ base = base_name.rsplit('.', 1)[0]
308
+ mapped_gate = f"{base}.gate_proj.weight"
309
+ mapped_up = f"{base}.up_proj.weight"
310
+ perm_gate = gate.permute(0, 2, 1).contiguous()
311
+ perm_up = up.permute(0, 2, 1).contiguous()
312
+ yield from ModelBase.modify_tensors(self, perm_gate, mapped_gate, bid)
313
+ yield from ModelBase.modify_tensors(self, perm_up, mapped_up, bid)
314
+ return
315
+
316
+ yield from super().modify_tensors(data_torch, name, bid)
317
+
318
+
319
+ @ModelBase.register("Qwen3OmniMoeForConditionalGeneration")
320
+ class Qwen3OmniMoeTextModel(Qwen3VLMoeTextModel):
321
+ model_arch = gguf.MODEL_ARCH.QWEN3VLMOE
322
+
323
+ def set_vocab(self):
324
+ super().set_vocab()
325
+ # correct BOS/EOS tokens
326
+ with open(self.dir_model / "tokenizer_config.json", "r", encoding="utf-8") as f:
327
+ tokenizer_config = json.load(f)
328
+ added_tokens = tokenizer_config.get("added_tokens_decoder", {})
329
+ for token_id, data in added_tokens.items():
330
+ if data.get("content") == "<|im_end|>":
331
+ self.gguf_writer.add_bos_token_id(int(token_id))
332
+ self.gguf_writer.add_eos_token_id(int(token_id))
333
+ break
334
+
335
+ def set_gguf_parameters(self):
336
+ super().set_gguf_parameters()
337
+ self.gguf_writer.add_num_deepstack_layers(0)
338
+
339
+
340
+ @ModelBase.register("Qwen3ASRForConditionalGeneration")
341
+ class Qwen3ASRTextModel(Qwen3VLTextModel):
342
+ model_arch = gguf.MODEL_ARCH.QWEN3VL
343
+
344
+ def set_gguf_parameters(self):
345
+ super().set_gguf_parameters()
346
+ self.gguf_writer.add_num_deepstack_layers(0)
347
+
348
+ def set_vocab(self):
349
+ super().set_vocab()
350
+ # fix chat template, use correct chatml format
351
+ self.gguf_writer.add_chat_template("{% for message in messages %}{{'<|im_start|>' + message['role'] + '\\n' + message['content'] + '<|im_end|>' + '\\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\\n' }}{% endif %}")
352
+ # correct BOS/EOS tokens
353
+ with open(self.dir_model / "tokenizer_config.json", "r", encoding="utf-8") as f:
354
+ tokenizer_config = json.load(f)
355
+ added_tokens = tokenizer_config.get("added_tokens_decoder", {})
356
+ for token_id, data in added_tokens.items():
357
+ if data.get("content") == "<|im_end|>":
358
+ self.gguf_writer.add_bos_token_id(int(token_id))
359
+ self.gguf_writer.add_eos_token_id(int(token_id))
360
+ break
llama.cpp/conversion/qwenvl.py ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Iterable, TYPE_CHECKING
4
+
5
+ import numpy as np
6
+ import torch
7
+
8
+ if TYPE_CHECKING:
9
+ from torch import Tensor
10
+
11
+ from .base import MmprojModel, ModelBase, TextModel, gguf
12
+
13
+
14
+ @ModelBase.register(
15
+ "Qwen2VLModel",
16
+ "Qwen2VLForConditionalGeneration",
17
+ "Qwen2_5_VLForConditionalGeneration",
18
+ "Qwen2_5OmniModel",
19
+ )
20
+ class Qwen2VLModel(TextModel):
21
+ model_arch = gguf.MODEL_ARCH.QWEN2VL
22
+
23
+ def set_gguf_parameters(self):
24
+ super().set_gguf_parameters()
25
+
26
+ def set_vocab(self):
27
+ try:
28
+ self._set_vocab_sentencepiece()
29
+ except FileNotFoundError:
30
+ self._set_vocab_gpt2()
31
+
32
+ @classmethod
33
+ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
34
+ name, gen = item
35
+
36
+ if name.startswith("thinker."):
37
+ name = name.replace("thinker.", "")
38
+
39
+ return super().filter_tensors((name, gen))
40
+
41
+
42
+ @ModelBase.register("Qwen2VLModel", "Qwen2VLForConditionalGeneration", "Qwen2_5_VLForConditionalGeneration")
43
+ class Qwen2VLVisionModel(MmprojModel):
44
+ def __init__(self, *args, **kwargs):
45
+ super().__init__(*args, **kwargs)
46
+ assert self.hparams_vision is not None
47
+ self.hparams_vision["image_size"] = self.hparams_vision.get("image_size", 560)
48
+ # rename config.json values
49
+ self.hparams_vision["num_attention_heads"] = self.hparams_vision.get("num_heads")
50
+ self.hparams_vision["num_hidden_layers"] = self.hparams_vision.get("depth")
51
+ if "embed_dim" in self.hparams_vision: # qwen2vl
52
+ self.hparams_vision["intermediate_size"] = self.hparams_vision.get("hidden_size")
53
+ self.hparams_vision["hidden_size"] = self.hparams_vision.get("embed_dim")
54
+
55
+ def set_gguf_parameters(self):
56
+ super().set_gguf_parameters()
57
+ assert self.hparams_vision is not None
58
+ hparams = self.hparams_vision
59
+ model_type = self.global_config['model_type']
60
+ if model_type == 'qwen2_vl':
61
+ self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.QWEN2VL)
62
+ elif model_type == 'qwen2_5_vl' or model_type == 'qwen2_5_omni':
63
+ if model_type == 'qwen2_5_omni':
64
+ self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.QWEN25O)
65
+ else:
66
+ self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.QWEN25VL)
67
+ self.gguf_writer.add_vision_use_silu(True)
68
+ # find n_wa_pattern (window attention pattern)
69
+ fullatt_block_indexes = hparams.get("fullatt_block_indexes")
70
+ assert fullatt_block_indexes is not None, "fullatt_block_indexes is required for qwen2_5_vl"
71
+ n_wa_pattern = fullatt_block_indexes[0] + 1
72
+ # validate n_wa_pattern
73
+ for i in range(1, len(fullatt_block_indexes)):
74
+ if fullatt_block_indexes[i] - fullatt_block_indexes[i - 1] != n_wa_pattern:
75
+ raise ValueError(f"Invalid fullatt_block_indexes: {fullatt_block_indexes}")
76
+ self.gguf_writer.add_vision_n_wa_pattern(n_wa_pattern)
77
+ else:
78
+ raise ValueError(f"Unknown QwenVL model type: {self.global_config['model_type']}")
79
+ # default values below are taken from HF tranformers code
80
+ self.gguf_writer.add_vision_attention_layernorm_eps(self.global_config.get("rms_norm_eps", 1e-6))
81
+
82
+ def tensor_force_quant(self, name, new_name, bid, n_dims):
83
+ if ".position_embd." in new_name:
84
+ return gguf.GGMLQuantizationType.F32
85
+ return super().tensor_force_quant(name, new_name, bid, n_dims)
86
+
87
+ @classmethod
88
+ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
89
+ name, gen = item
90
+
91
+ if not name.startswith("visual."):
92
+ return None
93
+
94
+ return super().filter_tensors(item)
95
+
96
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
97
+ # split QKV tensors if needed
98
+ if ".qkv." in name:
99
+ if data_torch.ndim == 2: # weight
100
+ c3, _ = data_torch.shape
101
+ else: # bias
102
+ c3 = data_torch.shape[0]
103
+ assert c3 % 3 == 0
104
+ c = c3 // 3
105
+ wq = data_torch[:c]
106
+ wk = data_torch[c: c * 2]
107
+ wv = data_torch[c * 2:]
108
+ yield from super().modify_tensors(wq, name.replace("qkv", "q"), bid)
109
+ yield from super().modify_tensors(wk, name.replace("qkv", "k"), bid)
110
+ yield from super().modify_tensors(wv, name.replace("qkv", "v"), bid)
111
+ elif 'patch_embed.proj.weight' in name:
112
+ # split Conv3D into Conv2Ds
113
+ c1, c2, kt, kh, kw = data_torch.shape
114
+ del c1, c2, kh, kw # unused
115
+ assert kt == 2, "Current implementation only support temporal_patch_size of 2"
116
+ yield (gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.V_ENC_EMBD_PATCH] + ".weight" , data_torch[:, :, 0, ...])
117
+ yield (gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.V_ENC_EMBD_PATCH] + ".weight.1", data_torch[:, :, 1, ...])
118
+ else:
119
+ yield from super().modify_tensors(data_torch, name, bid)
120
+
121
+
122
+ class Qwen25AudioModel(MmprojModel):
123
+ has_audio_encoder = True
124
+
125
+ def __init__(self, *args, **kwargs):
126
+ super().__init__(*args, **kwargs)
127
+ assert self.hparams_audio is not None
128
+ self.hparams_audio["hidden_size"] = self.hparams_audio["d_model"]
129
+ self.hparams_audio["intermediate_size"] = self.hparams_audio["encoder_ffn_dim"]
130
+ self.hparams_audio["num_attention_heads"] = self.hparams_audio["encoder_attention_heads"]
131
+
132
+ def set_gguf_parameters(self):
133
+ super().set_gguf_parameters()
134
+ assert self.hparams_audio is not None
135
+ self.gguf_writer.add_audio_num_mel_bins(self.hparams_audio["num_mel_bins"])
136
+ self.gguf_writer.add_audio_attention_layernorm_eps(self.hparams_audio.get("layer_norm_eps", 1e-5))
137
+
138
+ def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]:
139
+ # SinusoidsPositionEmbedding
140
+ assert self.hparams_audio is not None
141
+ max_timescale = 10000
142
+ length = 1500
143
+ channels = self.hparams_audio["hidden_size"]
144
+ log_timescale_increment = np.log(max_timescale) / (channels // 2 - 1)
145
+ inv_timescales = torch.exp(-log_timescale_increment * torch.arange(channels // 2).float())
146
+ scaled_time = torch.arange(length)[:, np.newaxis] * inv_timescales[np.newaxis, :]
147
+ pos_embd = torch.cat([torch.sin(scaled_time), torch.cos(scaled_time)], dim=1).to(dtype=torch.float32)
148
+ yield ("audio_tower.embed_positions.weight", pos_embd)
149
+
150
+ def tensor_force_quant(self, name, new_name, bid, n_dims):
151
+ if ".conv" in name and ".weight" in name:
152
+ return gguf.GGMLQuantizationType.F16
153
+ return super().tensor_force_quant(name, new_name, bid, n_dims)
154
+
155
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
156
+ if "conv1.bias" in name or "conv2.bias" in name:
157
+ # transpose conv1 and conv2 bias
158
+ data_torch = data_torch.unsqueeze(-1)
159
+
160
+ yield from MmprojModel.modify_tensors(self, data_torch, name, bid)
161
+
162
+
163
+ @ModelBase.register("Qwen2_5OmniModel")
164
+ class Qwen25OmniModel(Qwen2VLVisionModel, Qwen25AudioModel):
165
+ has_audio_encoder = True
166
+ has_vision_encoder = True
167
+
168
+ def get_vision_config(self) -> dict[str, Any] | None:
169
+ return self.global_config["thinker_config"].get("vision_config")
170
+
171
+ def get_audio_config(self) -> dict[str, Any] | None:
172
+ return self.global_config["thinker_config"].get("audio_config")
173
+
174
+ def set_gguf_parameters(self):
175
+ super().set_gguf_parameters()
176
+ self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.QWEN25O)
177
+
178
+ @classmethod
179
+ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
180
+ name, gen = item
181
+
182
+ if not name.startswith("visual.") and not name.startswith("audio_tower."):
183
+ return None
184
+
185
+ if name.startswith("thinker."):
186
+ name = name.replace("thinker.", "")
187
+
188
+ if "audio_bos_eos_token" in name:
189
+ # this tensor is left unused in transformers code
190
+ # https://github.com/huggingface/transformers/blob/6e3063422c4b1c014aa60c32b9254fd2902f0f28/src/transformers/models/qwen2_5_omni/modular_qwen2_5_omni.py#L1809
191
+ return None
192
+
193
+ return MmprojModel.filter_tensors((name, gen))
194
+
195
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
196
+ if "visual." in name:
197
+ yield from Qwen2VLVisionModel.modify_tensors(self, data_torch, name, bid)
198
+ elif "audio_tower." in name:
199
+ yield from Qwen25AudioModel.modify_tensors(self, data_torch, name, bid)
200
+ return # skip other tensors
llama.cpp/conversion/refact.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Iterable, TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from torch import Tensor
7
+
8
+ from .base import ModelBase, TextModel, gguf
9
+
10
+
11
+ @ModelBase.register("GPTRefactForCausalLM")
12
+ class RefactModel(TextModel):
13
+ model_arch = gguf.MODEL_ARCH.REFACT
14
+
15
+ def set_vocab(self):
16
+ super().set_vocab()
17
+
18
+ # TODO: how to determine special FIM tokens automatically?
19
+ special_vocab = gguf.SpecialVocab(self.dir_model, load_merges=False,
20
+ special_token_types = ['prefix', 'suffix', 'middle', 'eot'])
21
+ special_vocab._set_special_token("prefix", 1)
22
+ special_vocab._set_special_token("suffix", 3)
23
+ special_vocab._set_special_token("middle", 2)
24
+ special_vocab.chat_template = None # do not add it twice
25
+ special_vocab.add_to_gguf(self.gguf_writer)
26
+
27
+ def set_gguf_parameters(self):
28
+ hidden_dim = self.hparams["n_embd"]
29
+ inner_dim = 4 * hidden_dim
30
+ hidden_dim = int(2 * inner_dim / 3)
31
+ multiple_of = 256
32
+ ff_dim = multiple_of * ((hidden_dim + multiple_of - 1) // multiple_of)
33
+
34
+ # refact uses Alibi. So this is from config.json which might be used by training.
35
+ self.gguf_writer.add_context_length(self.hparams["n_positions"])
36
+ self.gguf_writer.add_embedding_length(self.hparams["n_embd"])
37
+
38
+ self.gguf_writer.add_feed_forward_length(ff_dim)
39
+ self.gguf_writer.add_block_count(self.block_count)
40
+ self.gguf_writer.add_head_count(self.hparams["n_head"])
41
+ self.gguf_writer.add_head_count_kv(1)
42
+ self.gguf_writer.add_layer_norm_rms_eps(self.hparams["layer_norm_epsilon"])
43
+ self.gguf_writer.add_file_type(self.ftype)
44
+
45
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
46
+ hidden_dim = self.hparams["n_embd"]
47
+ inner_dim = 4 * hidden_dim
48
+ hidden_dim = int(2 * inner_dim / 3)
49
+ multiple_of = 256
50
+ ff_dim = multiple_of * ((hidden_dim + multiple_of - 1) // multiple_of)
51
+ n_head = self.hparams["n_head"]
52
+ n_head_kv = 1
53
+ head_dim = self.hparams["n_embd"] // n_head
54
+
55
+ if bid is not None:
56
+ if name == f"transformer.h.{bid}.attn.kv.weight":
57
+ yield from super().modify_tensors(data_torch[:n_head_kv * head_dim], self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_K, bid), bid)
58
+ yield from super().modify_tensors(data_torch[n_head_kv * head_dim:], self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_V, bid), bid)
59
+ return
60
+ if name == f"transformer.h.{bid}.attn.q.weight":
61
+ yield from super().modify_tensors(data_torch, self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_Q, bid), bid)
62
+ return
63
+ if name == f"transformer.h.{bid}.mlp.gate_up_proj.weight":
64
+ yield from super().modify_tensors(data_torch[:ff_dim], self.format_tensor_name(gguf.MODEL_TENSOR.FFN_GATE, bid), bid)
65
+ yield from super().modify_tensors(data_torch[ff_dim:], self.format_tensor_name(gguf.MODEL_TENSOR.FFN_UP, bid), bid)
66
+ return
67
+
68
+ yield from super().modify_tensors(data_torch, name, bid)
llama.cpp/conversion/rwkv.py ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable, Iterable, TYPE_CHECKING
4
+
5
+ import torch
6
+
7
+ if TYPE_CHECKING:
8
+ from torch import Tensor
9
+
10
+ from .base import ModelBase, TextModel, gguf
11
+
12
+
13
+ @ModelBase.register("Rwkv6ForCausalLM")
14
+ class Rwkv6Model(TextModel):
15
+ model_arch = gguf.MODEL_ARCH.RWKV6
16
+
17
+ def set_vocab(self):
18
+ self._set_vocab_rwkv_world()
19
+
20
+ def set_gguf_parameters(self):
21
+ head_size = self.hparams["head_size"]
22
+ hidden_size = self.hparams["hidden_size"]
23
+ layer_norm_eps = self.hparams["layer_norm_epsilon"]
24
+ rescale_every_n_layers = self.hparams["rescale_every"]
25
+ intermediate_size = self.hparams["intermediate_size"] if self.hparams["intermediate_size"] is not None else int((hidden_size * 3.5) // 32 * 32)
26
+ time_mix_extra_dim = 64 if hidden_size == 4096 else 32
27
+ time_decay_extra_dim = 128 if hidden_size == 4096 else 64
28
+
29
+ # RWKV isn't context limited
30
+ self.gguf_writer.add_context_length(1048576)
31
+ self.gguf_writer.add_embedding_length(hidden_size)
32
+ self.gguf_writer.add_block_count(self.block_count)
33
+ self.gguf_writer.add_layer_norm_eps(layer_norm_eps)
34
+ self.gguf_writer.add_rescale_every_n_layers(rescale_every_n_layers)
35
+ self.gguf_writer.add_wkv_head_size(head_size)
36
+ self.gguf_writer.add_time_mix_extra_dim(time_mix_extra_dim)
37
+ self.gguf_writer.add_time_decay_extra_dim(time_decay_extra_dim)
38
+ self.gguf_writer.add_feed_forward_length(intermediate_size)
39
+ self.gguf_writer.add_file_type(self.ftype)
40
+
41
+ # required by llama.cpp, unused
42
+ self.gguf_writer.add_head_count(0)
43
+
44
+ lerp_weights: dict[int, dict[str, Tensor]] = {}
45
+
46
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
47
+ new_name = self.map_tensor_name(name)
48
+
49
+ if not (new_name.endswith(".weight") or new_name.endswith(".bias")):
50
+ new_name += ".weight"
51
+
52
+ if new_name.endswith("time_mix_w1.weight") or new_name.endswith("time_mix_decay_w1.weight") or new_name.endswith("time_mix_decay_w2.weight"):
53
+ data_torch = data_torch.transpose(0, 1)
54
+
55
+ if new_name.endswith("time_mix_w2.weight"):
56
+ data_torch = data_torch.permute(0, 2, 1)
57
+
58
+ if new_name.endswith("time_mix_decay.weight") or "lerp" in new_name:
59
+ data_torch = data_torch.squeeze()
60
+
61
+ try:
62
+ rescale_every_n_layers = self.hparams["rescale_every"]
63
+ if rescale_every_n_layers > 0:
64
+ if new_name.endswith("time_mix_output.weight") or new_name.endswith("channel_mix_value.weight"):
65
+ data_torch = data_torch.div_(2 ** int(bid // rescale_every_n_layers))
66
+ except KeyError:
67
+ pass
68
+
69
+ # concat time_mix_lerp weights to reduce some cpu overhead
70
+ # also reduces the number of tensors in the model
71
+ if bid is not None and "time_mix_lerp" in new_name and "time_mix_lerp_x" not in new_name:
72
+ try:
73
+ self.lerp_weights[bid][new_name] = data_torch
74
+ except KeyError:
75
+ self.lerp_weights[bid] = {new_name: data_torch}
76
+ if all(f"blk.{bid}.time_mix_lerp_{i}.weight" in self.lerp_weights[bid].keys() for i in ["w", "k", "v", "r", "g"]):
77
+ new_name = f"blk.{bid}.time_mix_lerp_fused.weight"
78
+ data = torch.stack([self.lerp_weights[bid][f"blk.{bid}.time_mix_lerp_{i}.weight"].unsqueeze(0) for i in ["w", "k", "v", "r", "g"]], dim=0).unsqueeze(1)
79
+ yield (new_name, data)
80
+ return
81
+
82
+ yield (new_name, data_torch)
83
+
84
+
85
+ @ModelBase.register("RWKV6Qwen2ForCausalLM")
86
+ class RWKV6Qwen2Model(Rwkv6Model):
87
+ model_arch = gguf.MODEL_ARCH.RWKV6QWEN2
88
+
89
+ def set_vocab(self):
90
+ try:
91
+ self._set_vocab_sentencepiece()
92
+ except FileNotFoundError:
93
+ self._set_vocab_gpt2()
94
+
95
+ def set_gguf_parameters(self):
96
+ num_attention_heads = self.hparams["num_attention_heads"]
97
+ num_key_value_heads = self.hparams["num_key_value_heads"]
98
+ hidden_size = self.hparams["hidden_size"]
99
+ head_size = hidden_size // num_attention_heads
100
+ rms_norm_eps = self.hparams["rms_norm_eps"]
101
+ intermediate_size = self.hparams["intermediate_size"]
102
+ time_mix_extra_dim = self.hparams.get("lora_rank_tokenshift", 64 if hidden_size >= 4096 else 32)
103
+ time_decay_extra_dim = self.hparams.get("lora_rank_decay", 128 if hidden_size >= 4096 else 64)
104
+
105
+ # RWKV isn't context limited
106
+ self.gguf_writer.add_context_length(1048576)
107
+ self.gguf_writer.add_embedding_length(hidden_size)
108
+ self.gguf_writer.add_block_count(self.block_count)
109
+ self.gguf_writer.add_wkv_head_size(head_size)
110
+ self.gguf_writer.add_time_mix_extra_dim(time_mix_extra_dim)
111
+ self.gguf_writer.add_time_decay_extra_dim(time_decay_extra_dim)
112
+ self.gguf_writer.add_feed_forward_length(intermediate_size)
113
+ self.gguf_writer.add_file_type(self.ftype)
114
+
115
+ # special parameters for time_mixing in RWKV6QWEN2
116
+ self.gguf_writer.add_layer_norm_rms_eps(rms_norm_eps)
117
+ self.gguf_writer.add_token_shift_count(1)
118
+ # RWKV6QWEN2 use grouped key/value like GQA
119
+ self.gguf_writer.add_head_count_kv(num_key_value_heads)
120
+
121
+ # required by llama.cpp, unused
122
+ self.gguf_writer.add_head_count(0)
123
+
124
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
125
+ for new_name, data in super().modify_tensors(data_torch, name, bid):
126
+ if "time_mix_w1" in new_name or "time_mix_w2" in new_name:
127
+ data = data.view(5, -1, data.shape[-1])
128
+ # rwkv6qwen2 has a different order of rkvwg instead of the original wkvrg
129
+ # permute them here to avoid code changes
130
+ data = torch.stack([data[3], data[1], data[2], data[0], data[4]], dim=0).view(-1, data.shape[-1])
131
+ if "w2" in new_name:
132
+ data = data.view(5, -1, data.shape[-1])
133
+ yield (new_name, data)
134
+ continue
135
+ yield (new_name, data)
136
+
137
+
138
+ @ModelBase.register("Rwkv7ForCausalLM", "RWKV7ForCausalLM")
139
+ class Rwkv7Model(TextModel):
140
+ model_arch = gguf.MODEL_ARCH.RWKV7
141
+
142
+ def set_vocab(self):
143
+ self._set_vocab_rwkv_world()
144
+
145
+ def calc_lora_rank(self, hidden_size, exponent, multiplier):
146
+ return max(1, round(hidden_size ** exponent * multiplier / 32)) * 32
147
+
148
+ def set_gguf_parameters(self):
149
+ try:
150
+ head_size = self.hparams["head_size"]
151
+ layer_norm_eps = self.hparams["layer_norm_epsilon"]
152
+ except KeyError:
153
+ head_size = self.hparams["head_dim"]
154
+ layer_norm_eps = self.hparams["norm_eps"]
155
+ hidden_size = self.hparams["hidden_size"]
156
+ intermediate_size = self.hparams["intermediate_size"] if self.hparams["intermediate_size"] is not None else (hidden_size * 4)
157
+
158
+ # ICLR: In-Context-Learning-Rate
159
+ try:
160
+ lora_rank_decay = self.hparams["lora_rank_decay"] if self.hparams["lora_rank_decay"] is not None else self.calc_lora_rank(hidden_size, 0.5, 1.8)
161
+ lora_rank_iclr = self.hparams["lora_rank_iclr"] if self.hparams["lora_rank_iclr"] is not None else self.calc_lora_rank(hidden_size, 0.5, 1.8)
162
+ lora_rank_value_residual_mix = self.hparams["lora_rank_value_residual_mix"] if self.hparams["lora_rank_value_residual_mix"] is not None else self.calc_lora_rank(hidden_size, 0.5, 1.3)
163
+ lora_rank_gate = self.hparams["lora_rank_gate"] if self.hparams["lora_rank_gate"] is not None else self.calc_lora_rank(hidden_size, 0.8, 0.6)
164
+ except KeyError:
165
+ lora_rank_decay = self.hparams["decay_low_rank_dim"] if self.hparams["decay_low_rank_dim"] is not None else self.calc_lora_rank(hidden_size, 0.5, 1.8)
166
+ lora_rank_iclr = self.hparams["a_low_rank_dim"] if self.hparams["a_low_rank_dim"] is not None else self.calc_lora_rank(hidden_size, 0.5, 1.8)
167
+ lora_rank_value_residual_mix = self.hparams["v_low_rank_dim"] if self.hparams["v_low_rank_dim"] is not None else self.calc_lora_rank(hidden_size, 0.5, 1.3)
168
+ lora_rank_gate = self.hparams["gate_low_rank_dim"] if self.hparams["gate_low_rank_dim"] is not None else self.calc_lora_rank(hidden_size, 0.8, 0.6)
169
+
170
+ # RWKV isn't context limited
171
+ self.gguf_writer.add_context_length(1048576)
172
+ self.gguf_writer.add_embedding_length(hidden_size)
173
+ self.gguf_writer.add_block_count(self.block_count)
174
+ self.gguf_writer.add_layer_norm_eps(layer_norm_eps)
175
+ self.gguf_writer.add_wkv_head_size(head_size)
176
+ self.gguf_writer.add_decay_lora_rank(lora_rank_decay)
177
+ self.gguf_writer.add_iclr_lora_rank(lora_rank_iclr)
178
+ self.gguf_writer.add_value_residual_mix_lora_rank(lora_rank_value_residual_mix)
179
+ self.gguf_writer.add_gate_lora_rank(lora_rank_gate)
180
+ self.gguf_writer.add_feed_forward_length(intermediate_size)
181
+ self.gguf_writer.add_file_type(self.ftype)
182
+
183
+ # required by llama.cpp, unused
184
+ self.gguf_writer.add_head_count(0)
185
+
186
+ lerp_weights: dict[int, dict[str, Tensor]] = {}
187
+ lora_needs_transpose: bool = True
188
+
189
+ @classmethod
190
+ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
191
+ name, gen = item
192
+
193
+ # unify tensor names here to make life easier
194
+ name = name.replace("blocks", "layers").replace("ffn", "feed_forward")
195
+ name = name.replace("self_attn", "attention").replace("attn", "attention")
196
+ name = name.replace("time_mixer.", "")
197
+
198
+ name = name.replace("feed_forward_norm", "ln2")
199
+ name = name.replace("g_norm", "ln_x")
200
+
201
+ return super().filter_tensors((name, gen))
202
+
203
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
204
+ # lora layer names in fla-hub's impl
205
+ if "_lora.lora" in name:
206
+ self.lora_needs_transpose = False
207
+ name = name.replace("_lora.lora.0.weight", "1.weight")
208
+ name = name.replace("_lora.lora.2.weight", "2.weight")
209
+ name = name.replace("_lora.lora.2.bias", "0.weight")
210
+
211
+ if "attention.v" in name and "value" not in self.map_tensor_name(name) and bid == 0:
212
+ # some models have dummy v0/v1/v2 on first layer while others don't
213
+ # ignore them all since they are not used
214
+ return
215
+
216
+ wkv_has_gate = self.hparams.get("wkv_has_gate", True)
217
+ lerp_list = ["r", "w", "k", "v", "a", "g"] if wkv_has_gate else ["r", "w", "k", "v", "a"]
218
+
219
+ if bid is not None and "attention.x_" in name:
220
+ if "attention.x_x" in name:
221
+ # already concatenated
222
+ new_name = f"blk.{bid}.time_mix_lerp_fused.weight"
223
+ data = data_torch.reshape(len(lerp_list), 1, 1, -1)
224
+ yield (new_name, data)
225
+ else:
226
+ try:
227
+ self.lerp_weights[bid][name] = data_torch
228
+ except KeyError:
229
+ self.lerp_weights[bid] = {name: data_torch}
230
+ if all(f"model.layers.{bid}.attention.x_{i}" in self.lerp_weights[bid].keys() for i in lerp_list):
231
+ new_name = f"blk.{bid}.time_mix_lerp_fused.weight"
232
+ data = torch.stack([self.lerp_weights[bid][f"model.layers.{bid}.attention.x_{i}"] for i in lerp_list], dim=0)
233
+ yield (new_name, data)
234
+ return
235
+ else:
236
+ data_torch = data_torch.squeeze()
237
+ new_name = self.map_tensor_name(name)
238
+
239
+ if not (new_name.endswith(".weight") or new_name.endswith(".bias")):
240
+ new_name += ".weight"
241
+
242
+ if self.lora_needs_transpose and any(
243
+ new_name.endswith(t) for t in [
244
+ "time_mix_w1.weight", "time_mix_w2.weight",
245
+ "time_mix_a1.weight", "time_mix_a2.weight",
246
+ "time_mix_v1.weight", "time_mix_v2.weight",
247
+ "time_mix_g1.weight", "time_mix_g2.weight",
248
+ ]
249
+ ):
250
+ data_torch = data_torch.transpose(0, 1)
251
+
252
+ if 'r_k' in new_name:
253
+ data_torch = data_torch.flatten()
254
+
255
+ if bid == 0 and "time_mix_a" in new_name:
256
+ # dummy v0/v1/v2 on first layer
257
+ # easiest way to make llama happy
258
+ yield (new_name.replace("time_mix_a", "time_mix_v"), data_torch)
259
+
260
+ yield (new_name, data_torch)
261
+
262
+
263
+ @ModelBase.register("RwkvHybridForCausalLM")
264
+ class ARwkv7Model(Rwkv7Model):
265
+ model_arch = gguf.MODEL_ARCH.ARWKV7
266
+
267
+ def set_vocab(self):
268
+ try:
269
+ self._set_vocab_sentencepiece()
270
+ except FileNotFoundError:
271
+ self._set_vocab_gpt2()
272
+
273
+ def set_gguf_parameters(self):
274
+ hidden_size = self.hparams["hidden_size"]
275
+ head_size = self.hparams["head_size"]
276
+ rms_norm_eps = self.hparams["rms_norm_eps"]
277
+ intermediate_size = self.hparams["intermediate_size"]
278
+ wkv_has_gate = self.hparams["wkv_has_gate"]
279
+ assert self.hparams["wkv_version"] == 7
280
+
281
+ # ICLR: In-Context-Learning-Rate
282
+ lora_rank_decay = 64
283
+ lora_rank_iclr = 64
284
+ lora_rank_value_residual_mix = 32
285
+ lora_rank_gate = 128 if wkv_has_gate else 0
286
+
287
+ # RWKV isn't context limited
288
+ self.gguf_writer.add_context_length(1048576)
289
+ self.gguf_writer.add_embedding_length(hidden_size)
290
+ self.gguf_writer.add_block_count(self.block_count)
291
+ self.gguf_writer.add_layer_norm_rms_eps(rms_norm_eps)
292
+ self.gguf_writer.add_wkv_head_size(head_size)
293
+ self.gguf_writer.add_decay_lora_rank(lora_rank_decay)
294
+ self.gguf_writer.add_iclr_lora_rank(lora_rank_iclr)
295
+ self.gguf_writer.add_value_residual_mix_lora_rank(lora_rank_value_residual_mix)
296
+ self.gguf_writer.add_gate_lora_rank(lora_rank_gate)
297
+ self.gguf_writer.add_feed_forward_length(intermediate_size)
298
+ self.gguf_writer.add_file_type(self.ftype)
299
+ self.gguf_writer.add_token_shift_count(1)
300
+
301
+ # required by llama.cpp, unused
302
+ self.gguf_writer.add_head_count(0)
llama.cpp/conversion/sarashina2.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable, TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from torch import Tensor
7
+
8
+ from .base import ModelBase, gguf
9
+
10
+ from .llama import LlamaModel
11
+ from .qwenvl import Qwen2VLVisionModel
12
+
13
+
14
+ @ModelBase.register("Sarashina2VisionForCausalLM")
15
+ class Sarashina2VLTextModel(LlamaModel):
16
+ model_arch = gguf.MODEL_ARCH.LLAMA
17
+
18
+ @classmethod
19
+ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
20
+ name, gen = item
21
+ if name.startswith("llm."):
22
+ name = name.replace("llm.", "", 1)
23
+ elif name.startswith("norm."):
24
+ return None
25
+ return super().filter_tensors((name, gen))
26
+
27
+
28
+ @ModelBase.register("Sarashina2VisionForCausalLM")
29
+ class Sarashina2VLVisionModel(Qwen2VLVisionModel):
30
+ def __init__(self, *args, **kwargs):
31
+ super().__init__(*args, **kwargs)
32
+ self.global_config['model_type'] = "qwen2_vl"
llama.cpp/conversion/smallthinker.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Iterable, TYPE_CHECKING
4
+
5
+ import torch
6
+
7
+ if TYPE_CHECKING:
8
+ from torch import Tensor
9
+
10
+ from .base import ModelBase, TextModel, gguf, logger
11
+
12
+
13
+ @ModelBase.register("SmallThinkerForCausalLM")
14
+ class SmallThinkerModel(TextModel):
15
+ model_arch = gguf.MODEL_ARCH.SMALLTHINKER
16
+
17
+ def set_gguf_parameters(self):
18
+ super().set_gguf_parameters()
19
+ if (n_experts := self.hparams.get("moe_num_primary_experts")) is not None:
20
+ self.gguf_writer.add_expert_count(n_experts)
21
+ if (n_experts_used := self.hparams.get("moe_num_active_primary_experts")) is not None:
22
+ self.gguf_writer.add_expert_used_count(n_experts_used)
23
+ if (moe_intermediate_size := self.hparams.get("moe_ffn_hidden_size")) is not None:
24
+ self.gguf_writer.add_expert_feed_forward_length(moe_intermediate_size)
25
+ self.gguf_writer.add_feed_forward_length(moe_intermediate_size)
26
+ logger.info(f"gguf: expert feed forward length = {moe_intermediate_size}")
27
+ if (self.hparams.get('moe_primary_router_apply_softmax')):
28
+ self.gguf_writer.add_expert_gating_func(gguf.ExpertGatingFuncType.SOFTMAX)
29
+ else:
30
+ self.gguf_writer.add_expert_gating_func(gguf.ExpertGatingFuncType.SIGMOID)
31
+
32
+ sliding_window_layout = self.hparams.get("sliding_window_layout")
33
+ if sliding_window_layout:
34
+ for i in sliding_window_layout:
35
+ if i != 0:
36
+ sliding_window = self.hparams.get("sliding_window_size")
37
+ if sliding_window:
38
+ self.gguf_writer.add_sliding_window(sliding_window)
39
+ break
40
+
41
+ _experts: list[dict[str, Tensor]] | None = None
42
+
43
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
44
+ # process the experts separately
45
+ if name.find("experts") != -1:
46
+ n_experts = self.hparams.get("moe_num_primary_experts") or self.find_hparam(["num_local_experts", "num_experts"])
47
+ assert bid is not None
48
+
49
+ if self._experts is None:
50
+ self._experts = [{} for _ in range(self.block_count)]
51
+
52
+ self._experts[bid][name] = data_torch
53
+
54
+ if len(self._experts[bid]) >= n_experts * 3:
55
+ # merge the experts into a single 3d tensor
56
+ for w_name in ["down", "gate", "up"]:
57
+ datas: list[Tensor] = []
58
+
59
+ for xid in range(n_experts):
60
+ ename = f"model.layers.{bid}.block_sparse_moe.experts.{xid}.{w_name}.weight"
61
+ datas.append(self._experts[bid][ename])
62
+ del self._experts[bid][ename]
63
+
64
+ data_torch = torch.stack(datas, dim=0)
65
+
66
+ merged_name = f"model.layers.{bid}.block_sparse_moe.experts.{w_name}.weight"
67
+
68
+ yield from super().modify_tensors(data_torch, merged_name, bid)
69
+ return
70
+ else:
71
+ return
72
+
73
+ yield from super().modify_tensors(data_torch, name, bid)
74
+
75
+ def prepare_tensors(self):
76
+ super().prepare_tensors()
77
+
78
+ if self._experts is not None:
79
+ # flatten `list[dict[str, Tensor]]` into `list[str]`
80
+ experts = [k for d in self._experts for k in d.keys()]
81
+ if len(experts) > 0:
82
+ raise ValueError(f"Unprocessed experts: {experts}")
llama.cpp/conversion/smolvlm.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable, TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from torch import Tensor
7
+
8
+ from .base import MmprojModel, ModelBase, gguf
9
+
10
+
11
+ @ModelBase.register("Idefics3ForConditionalGeneration", "SmolVLMForConditionalGeneration")
12
+ class SmolVLMModel(MmprojModel):
13
+ def __init__(self, *args, **kwargs):
14
+ super().__init__(*args, **kwargs)
15
+ if self.hparams["model_type"] == "smolvlm_vision":
16
+ # fix for SmolVLM2, missing some keys in config.json
17
+ # default values are taken from transformers code
18
+ self.hparams["hidden_size"] = self.hparams.get("hidden_size", 1152)
19
+ self.hparams["num_attention_heads"] = self.hparams.get("num_attention_heads", 16)
20
+ self.hparams["intermediate_size"] = self.hparams.get("intermediate_size", 3072)
21
+
22
+ def set_gguf_parameters(self):
23
+ super().set_gguf_parameters()
24
+ self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.IDEFICS3)
25
+ self.gguf_writer.add_vision_attention_layernorm_eps(self.hparams.get("layer_norm_eps", 1e-5))
26
+ self.gguf_writer.add_vision_projector_scale_factor(self.global_config.get("scale_factor", 2))
27
+ self.gguf_writer.add_vision_use_gelu(True)
28
+
29
+ # Add the preprocessor longest edge size
30
+ preproc_image_size = self.preprocessor_config.get("size", {}).get("longest_edge", self.image_size)
31
+ self.gguf_writer.add_vision_preproc_image_size(preproc_image_size)
32
+
33
+ def tensor_force_quant(self, name, new_name, bid, n_dims):
34
+ if ".embeddings." in name:
35
+ return gguf.GGMLQuantizationType.F32
36
+ return super().tensor_force_quant(name, new_name, bid, n_dims)
37
+
38
+ @classmethod
39
+ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
40
+ name, gen = item
41
+
42
+ is_vision_tensor = "vision_tower" in name or "vision_model" in name or "model.connector" in name
43
+
44
+ if not is_vision_tensor:
45
+ return None
46
+
47
+ return super().filter_tensors(item)
llama.cpp/conversion/stablelm.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Iterable, TYPE_CHECKING
4
+
5
+ import torch
6
+
7
+ if TYPE_CHECKING:
8
+ from torch import Tensor
9
+
10
+ from .base import ModelBase, TextModel, gguf
11
+
12
+
13
+ @ModelBase.register("StableLmForCausalLM", "StableLMEpochForCausalLM", "LlavaStableLMEpochForCausalLM")
14
+ class StableLMModel(TextModel):
15
+ model_arch = gguf.MODEL_ARCH.STABLELM
16
+
17
+ def set_vocab(self):
18
+ if (self.dir_model / "tokenizer.json").is_file():
19
+ self._set_vocab_gpt2()
20
+ else:
21
+ # StableLM 2 1.6B used to have a vocab in a similar format to Qwen's vocab
22
+ self._set_vocab_qwen()
23
+
24
+ def set_gguf_parameters(self):
25
+ hparams = self.hparams
26
+
27
+ self.gguf_writer.add_context_length(hparams["max_position_embeddings"])
28
+ self.gguf_writer.add_embedding_length(hparams["hidden_size"])
29
+ self.gguf_writer.add_block_count(self.block_count)
30
+ self.gguf_writer.add_feed_forward_length(hparams["intermediate_size"])
31
+ rotary_factor = self.rope_parameters["partial_rotary_factor"]
32
+ self.gguf_writer.add_rope_dimension_count(int(rotary_factor * (hparams["hidden_size"] // hparams["num_attention_heads"])))
33
+ self.gguf_writer.add_head_count(hparams["num_attention_heads"])
34
+ self.gguf_writer.add_head_count_kv(hparams["num_key_value_heads"])
35
+ self.gguf_writer.add_parallel_residual(hparams["use_parallel_residual"] if "use_parallel_residual" in hparams else True)
36
+ self.gguf_writer.add_layer_norm_eps(self.find_hparam(["layer_norm_eps", "norm_eps"]))
37
+ self.gguf_writer.add_file_type(self.ftype)
38
+
39
+ _q_norms: list[dict[str, Tensor]] | None = None
40
+ _k_norms: list[dict[str, Tensor]] | None = None
41
+
42
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
43
+ n_head = self.hparams["num_attention_heads"]
44
+ n_kv_head = self.hparams["num_key_value_heads"]
45
+
46
+ if name.find("q_layernorm.norms") != -1:
47
+ assert bid is not None
48
+
49
+ if self._q_norms is None:
50
+ self._q_norms = [{} for _ in range(self.block_count)]
51
+
52
+ self._q_norms[bid][name] = data_torch
53
+
54
+ if len(self._q_norms[bid]) >= n_head:
55
+ return self._stack_qk_norm(bid, n_head, self._q_norms[bid], "q_layernorm")
56
+ else:
57
+ return
58
+
59
+ if name.find("k_layernorm.norms") != -1:
60
+ assert bid is not None
61
+
62
+ if self._k_norms is None:
63
+ self._k_norms = [{} for _ in range(self.block_count)]
64
+
65
+ self._k_norms[bid][name] = data_torch
66
+
67
+ if len(self._k_norms[bid]) >= n_kv_head:
68
+ return self._stack_qk_norm(bid, n_kv_head, self._k_norms[bid], "k_layernorm")
69
+ else:
70
+ return
71
+
72
+ yield from super().modify_tensors(data_torch, name, bid)
73
+
74
+ def _stack_qk_norm(self, bid: int, n_head: int, norms: dict[str, Tensor], layer_name: str = "q_layernorm"):
75
+ datas: list[Tensor] = []
76
+ # extract the norms in order
77
+ for xid in range(n_head):
78
+ ename = f"model.layers.{bid}.self_attn.{layer_name}.norms.{xid}.weight"
79
+ datas.append(norms[ename])
80
+ del norms[ename]
81
+ data_torch = torch.stack(datas, dim=0)
82
+
83
+ merged_name = f"model.layers.{bid}.self_attn.{layer_name}.weight"
84
+
85
+ yield from super().modify_tensors(data_torch, merged_name, bid)
86
+
87
+ def prepare_tensors(self):
88
+ super().prepare_tensors()
89
+
90
+ if self._q_norms is not None or self._k_norms is not None:
91
+ # flatten two `list[dict[str, Tensor]]` into a single `list[str]`
92
+ norms = (
93
+ [k for d in self._q_norms for k in d.keys()] if self._q_norms is not None else []
94
+ ) + (
95
+ [k for d in self._k_norms for k in d.keys()] if self._k_norms is not None else []
96
+ )
97
+ if len(norms) > 0:
98
+ raise ValueError(f"Unprocessed norms: {norms}")
llama.cpp/conversion/starcoder.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from .base import ModelBase, TextModel, gguf
4
+
5
+
6
+ @ModelBase.register("GPTBigCodeForCausalLM")
7
+ class StarCoderModel(TextModel):
8
+ model_arch = gguf.MODEL_ARCH.STARCODER
9
+
10
+ def set_gguf_parameters(self):
11
+ self.gguf_writer.add_context_length(self.hparams["n_positions"])
12
+ self.gguf_writer.add_embedding_length(self.hparams["n_embd"])
13
+ self.gguf_writer.add_feed_forward_length(4 * self.hparams["n_embd"])
14
+ self.gguf_writer.add_block_count(self.block_count)
15
+ self.gguf_writer.add_head_count(self.hparams["n_head"])
16
+ self.gguf_writer.add_head_count_kv(1)
17
+ self.gguf_writer.add_layer_norm_eps(self.hparams["layer_norm_epsilon"])
18
+ self.gguf_writer.add_file_type(self.ftype)
19
+
20
+
21
+ @ModelBase.register("Starcoder2ForCausalLM")
22
+ class StarCoder2Model(TextModel):
23
+ model_arch = gguf.MODEL_ARCH.STARCODER2
llama.cpp/conversion/step3.py ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ import re
5
+
6
+ from typing import Callable, Iterable, TYPE_CHECKING
7
+
8
+ import torch
9
+
10
+ if TYPE_CHECKING:
11
+ from torch import Tensor
12
+
13
+ from .base import MmprojModel, ModelBase, TextModel, _MISTRAL_COMMON_DATASET_MEAN, _MISTRAL_COMMON_DATASET_STD, gguf
14
+
15
+ from .qwen import Qwen3Model
16
+
17
+
18
+ @ModelBase.register("StepVLForConditionalGeneration", "Step3p7ForConditionalGeneration")
19
+ class Step3VLVisionModel(MmprojModel):
20
+ def __init__(self, *args, **kwargs):
21
+ super().__init__(*args, **kwargs)
22
+ assert self.hparams_vision is not None
23
+
24
+ if not self.hparams_vision.get("intermediate_size"):
25
+ hidden_size = self.hparams_vision.get("hidden_size") or self.hparams_vision.get("width") or 0
26
+ assert hidden_size > 0
27
+ mlp_ratio = float(self.hparams_vision.get("mlp_ratio", 8960 / 1536))
28
+ self.hparams_vision["intermediate_size"] = int(round(hidden_size * mlp_ratio))
29
+
30
+ self.preprocessor_config.setdefault("image_mean", list(_MISTRAL_COMMON_DATASET_MEAN))
31
+ self.preprocessor_config.setdefault("image_std", list(_MISTRAL_COMMON_DATASET_STD))
32
+
33
+ def set_gguf_parameters(self):
34
+ super().set_gguf_parameters()
35
+ assert self.hparams_vision is not None
36
+
37
+ projector_stride = int(self.global_config.get("understand_projector_stride", -1))
38
+ hidden_size = int(self.hparams_vision.get("hidden_size", self.hparams_vision.get("width", -1)))
39
+ num_layers = int(self.hparams_vision.get("num_hidden_layers", self.hparams_vision.get("layers", -1)))
40
+ assert (projector_stride, int(self.hparams_vision.get("image_size", -1)), hidden_size, num_layers) == (2, 728, 1536, 47), (
41
+ "current Step3-VL conversion path is only validated for Step3-VL-10B"
42
+ )
43
+
44
+ self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.STEP3VL)
45
+ self.gguf_writer.add_vision_attention_layernorm_eps(float(self.hparams_vision.get("layer_norm_eps", 1e-5)))
46
+ self.gguf_writer.add_vision_projector_scale_factor(projector_stride ** 2)
47
+ # 3024 max resize comes from step3-vl-10b processing_step3.py.
48
+ self.gguf_writer.add_vision_preproc_image_size(3024)
49
+
50
+ def tensor_force_quant(self, name, new_name, bid, n_dims):
51
+ if ".position_embd." in new_name:
52
+ return gguf.GGMLQuantizationType.F32
53
+ if ("mm.0." in new_name or "mm.1." in new_name) and new_name.endswith(".weight"):
54
+ return gguf.GGMLQuantizationType.F16 if self.ftype == gguf.LlamaFileType.MOSTLY_F16 else gguf.GGMLQuantizationType.F32
55
+ return super().tensor_force_quant(name, new_name, bid, n_dims)
56
+
57
+ @classmethod
58
+ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
59
+ name, gen = item
60
+
61
+ if name.startswith(("model.", "lm_head.")):
62
+ return None
63
+
64
+ return super().filter_tensors(item)
65
+
66
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
67
+ if name.startswith("vision_model.vit_downsampler"):
68
+ match = re.match(r"vision_model\.vit_downsampler(\d+)\.(weight|bias)", name)
69
+ if match is None:
70
+ raise ValueError(f"Unexpected Step3-VL projector tensor {name!r}")
71
+
72
+ proj_id = int(match.group(1)) - 1
73
+ suffix = f".{match.group(2)}"
74
+ yield (self.format_tensor_name(gguf.MODEL_TENSOR.V_MMPROJ, proj_id, suffix=suffix), data_torch)
75
+ return
76
+
77
+ if name == "vit_large_projector.weight":
78
+ yield (self.format_tensor_name(gguf.MODEL_TENSOR.V_MMPROJ_FC), data_torch)
79
+ return
80
+
81
+ if name.startswith("vision_model."):
82
+ if name == "vision_model.positional_embedding":
83
+ name += ".weight"
84
+ elif name.endswith(".gamma") and ".ls_" in name:
85
+ name = name.removesuffix(".gamma") + ".weight"
86
+
87
+ name = name.replace("attn.in_proj_weight", "attn.in_proj.weight")
88
+ name = name.replace("attn.in_proj_bias", "attn.in_proj.bias")
89
+
90
+ yield from super().modify_tensors(data_torch, name, bid)
91
+
92
+
93
+ @ModelBase.register("StepVLForConditionalGeneration")
94
+ class Step3VLTextModel(Qwen3Model):
95
+ model_arch = gguf.MODEL_ARCH.QWEN3
96
+
97
+
98
+ @ModelBase.register("Step3p5ForCausalLM", "Step3p7ForConditionalGeneration")
99
+ class Step35Model(TextModel):
100
+ model_arch = gguf.MODEL_ARCH.STEP35
101
+
102
+ # The --mtp / --no-mtp toggles are ModelBase.mtp_only / no_mtp (set in
103
+ # convert_hf_to_gguf.py main()). Unlike Qwen3.5, which stores MTP under a
104
+ # `mtp.*` namespace, Step3.5 appends MTP layers at
105
+ # `model.layers.{num_hidden_layers + i}`, so we filter them by layer index.
106
+ # The trunk layer count is captured before indexing so the classmethod
107
+ # filter_tensors can tell the appended MTP block(s) apart from the trunk.
108
+ _n_main_layers: int | None = None
109
+
110
+ def __init__(self, *args, **kwargs):
111
+ super().__init__(*args, **kwargs)
112
+ # NextN/MTP layers are appended past num_hidden_layers; extend the
113
+ # tensor map to cover them so the MTP block's tensors get correctly
114
+ # indexed names. When --no-mtp drops the MTP blocks, fall back to the
115
+ # base num_hidden_layers so we don't reserve unused slots.
116
+ n_nextn = int(self.hparams.get("num_nextn_predict_layers", 0))
117
+ if n_nextn > 0 and not self.no_mtp:
118
+ self.block_count += n_nextn
119
+ self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count)
120
+
121
+ def index_tensors(self, remote_hf_model_id: str | None = None):
122
+ # filter_tensors is a classmethod and can't reach self.hparams; stash
123
+ # the trunk layer count here (before indexing runs) so it can detect
124
+ # the appended MTP layers by index.
125
+ hparams = {**self.hparams, **self.hparams.get("text_config", {})}
126
+ key = next((k for k in ["n_layers", "num_hidden_layers", "n_layer", "num_layers"] if k in hparams), None)
127
+ type(self)._n_main_layers = hparams.get(key)
128
+ return super().index_tensors(remote_hf_model_id=remote_hf_model_id)
129
+
130
+ def set_gguf_parameters(self):
131
+ rope_theta = self.hparams.get("rope_theta")
132
+ if isinstance(rope_theta, list):
133
+ self.hparams["rope_theta"] = float(rope_theta[0])
134
+ self.hparams["local_rope_theta"] = float(rope_theta[1])
135
+ self.rope_parameters["rope_theta"] = self.hparams["rope_theta"]
136
+ self.rope_parameters["sliding_attention"] = {"rope_theta": self.hparams["local_rope_theta"]}
137
+
138
+ super().set_gguf_parameters()
139
+
140
+ layer_types = self.hparams.get("layer_types") or []
141
+ partial_rotary_factors = self.hparams.get("partial_rotary_factors") or []
142
+ attn_other = self.hparams.get("attention_other_setting") or {}
143
+
144
+ n_head_base = self.hparams["num_attention_heads"]
145
+ n_kv_base = self.hparams["num_attention_groups"]
146
+
147
+ n_head_swa = attn_other.get("num_attention_heads", n_head_base)
148
+ n_kv_swa = attn_other.get("num_attention_groups", n_kv_base)
149
+
150
+ n_nextn = int(self.hparams.get("num_nextn_predict_layers", 0))
151
+
152
+ # The Step3p5 HF checkpoint stores layer_types/partial_rotary_factors
153
+ # entries for the MTP blocks past num_hidden_layers; preserve them so
154
+ # the MTP layer's attention shape, SWA flag, and partial RoPE dim are
155
+ # set correctly. Pad with full-attention defaults if the checkpoint
156
+ # truncated them.
157
+ def _pad(arr, n, default):
158
+ arr = list(arr)
159
+ if len(arr) < n:
160
+ arr = arr + [default] * (n - len(arr))
161
+ return arr[:n]
162
+
163
+ layer_types = _pad(layer_types, self.block_count, "full_attention")
164
+ partial_rotary_factors = _pad(
165
+ partial_rotary_factors,
166
+ self.block_count,
167
+ 0.5, # full_attention default for Step3p5
168
+ )
169
+ assert [1.0 if lt == "sliding_attention" else 0.5 for lt in layer_types] == partial_rotary_factors
170
+ head_arr = [n_head_swa if lt == "sliding_attention" else n_head_base for lt in layer_types]
171
+ kv_arr = [n_kv_swa if lt == "sliding_attention" else n_kv_base for lt in layer_types]
172
+ swa_pat = [lt == "sliding_attention" for lt in layer_types]
173
+
174
+ self.gguf_writer.add_head_count(head_arr)
175
+ self.gguf_writer.add_head_count_kv(kv_arr)
176
+
177
+ self.gguf_writer.add_sliding_window(self.hparams["sliding_window"])
178
+ self.gguf_writer.add_sliding_window_pattern(swa_pat)
179
+
180
+ self.gguf_writer.add_value_length(self.hparams["head_dim"])
181
+
182
+ # MoE params
183
+ self.gguf_writer.add_expert_count(self.hparams["moe_num_experts"])
184
+ self.gguf_writer.add_expert_used_count(self.hparams["moe_top_k"])
185
+ self.gguf_writer.add_expert_feed_forward_length(self.hparams["moe_intermediate_size"])
186
+ self.gguf_writer.add_expert_shared_feed_forward_length(self.hparams["share_expert_dim"])
187
+
188
+ if (moe_router_scaling_factor := self.hparams.get("moe_router_scaling_factor")) is not None:
189
+ self.gguf_writer.add_expert_weights_scale(moe_router_scaling_factor)
190
+ if (norm_expert_weight := self.hparams.get("norm_expert_weight")) is not None:
191
+ self.gguf_writer.add_expert_weights_norm(norm_expert_weight)
192
+
193
+ # leading dense blocks
194
+ leading_dense = 0
195
+ moe_layers_enum = self.hparams.get("moe_layers_enum")
196
+ if isinstance(moe_layers_enum, str) and moe_layers_enum.strip():
197
+ moe_layers = sorted(int(i) for i in moe_layers_enum.strip().split(","))
198
+ if moe_layers:
199
+ leading_dense = max(0, moe_layers[0])
200
+ self.gguf_writer.add_leading_dense_block_count(leading_dense)
201
+ self.gguf_writer.add_moe_every_n_layers(int(self.hparams.get("moe_every_n_layer", 1)))
202
+
203
+ self.gguf_writer.add_layer_norm_rms_eps(self.hparams.get("rms_norm_eps", 1e-5))
204
+
205
+ # Optional per-layer SwiGLU clamps. MTP layers default to no clamping (0.0).
206
+ if (limits := self.hparams.get("swiglu_limits")) is not None:
207
+ limits_f = _pad(
208
+ [0.0 if v is None else float(v) for v in limits],
209
+ self.block_count,
210
+ 0.0,
211
+ )
212
+ self.gguf_writer.add_swiglu_clamp_exp(limits_f)
213
+ if (limits_shared := self.hparams.get("swiglu_limits_shared")) is not None:
214
+ limits_shared_f = _pad(
215
+ [0.0 if v is None else float(v) for v in limits_shared],
216
+ self.block_count,
217
+ 0.0,
218
+ )
219
+ self.gguf_writer.add_swiglu_clamp_shexp(limits_shared_f)
220
+
221
+ if n_nextn > 0 and not self.no_mtp:
222
+ self.gguf_writer.add_nextn_predict_layers(n_nextn)
223
+
224
+ @classmethod
225
+ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
226
+ if (titem := super().filter_tensors(item)) is None:
227
+ return None
228
+ name, gen = titem
229
+
230
+ # Map router bias (expert selection bias) to a GGUF bias tensor
231
+ if name.endswith(".moe.router_bias"):
232
+ name += ".bias"
233
+
234
+ # Step3.5 appends the MTP block(s) past num_hidden_layers.
235
+ assert cls._n_main_layers is not None
236
+ is_mtp = (m := re.match(r"model\.layers\.(\d+)\.", name)) is not None and int(m.group(1)) >= cls._n_main_layers
237
+
238
+ # --no-mtp: drop the appended MTP block(s) entirely.
239
+ if is_mtp and cls.no_mtp:
240
+ return None
241
+ # --mtp: keep ONLY MTP-block tensors plus the shared embeddings/norm/
242
+ # lm_head (so the resulting GGUF carries just the draft head).
243
+ if cls.mtp_only and not is_mtp and name not in (
244
+ "model.embed_tokens.weight", "model.norm.weight", "lm_head.weight",
245
+ ):
246
+ return None
247
+
248
+ # The checkpoint nests the per-MTP-layer shared head under
249
+ # `model.layers.{N+i}.transformer.shared_head.{norm,output}.weight`;
250
+ # strip the `transformer.` infix and rename `output` → `head` so the
251
+ # existing NEXTN_SHARED_HEAD_{NORM,HEAD} tensor mapping picks them up.
252
+ # Mirrors vllm's `_rewrite_spec_layer_name` (step3p5_mtp.py).
253
+ if is_mtp:
254
+ name = name.replace(".transformer.", ".")
255
+ name = name.replace("shared_head.output", "shared_head.head")
256
+
257
+ return name, gen
258
+
259
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None):
260
+ if name.endswith("norm.weight"):
261
+ data_torch += 1.0
262
+
263
+ if name.endswith((".self_attn.g_proj.weight", ".moe.gate.weight", ".moe.up_proj.weight", ".moe.gate_proj.weight", ".moe.down_proj.weight")):
264
+ data_torch = data_torch.squeeze().contiguous()
265
+
266
+ yield from super().modify_tensors(data_torch, name, bid)
267
+
268
+ def prepare_metadata(self, vocab_only: bool):
269
+ from_dir = self.fname_out.is_dir()
270
+ super().prepare_metadata(vocab_only=vocab_only)
271
+
272
+ # Mirror Qwen3.5's behavior: when emitting a draft-only file into a
273
+ # directory, prefix with "mtp-" so it doesn't collide with the trunk.
274
+ if not self.mtp_only or not from_dir:
275
+ return
276
+
277
+ output_type: str = self.ftype.name.partition("_")[2]
278
+ fname_default: str = gguf.naming_convention(
279
+ self.metadata.name, self.metadata.basename, self.metadata.finetune,
280
+ self.metadata.version, size_label=None, output_type=output_type, model_type=None)
281
+ self.fname_out = self.fname_out.parent / f"mtp-{fname_default}.gguf"
282
+
283
+ def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]:
284
+ # Step35 can optionally use Llama-3 style RoPE scaling (HF: rope_scaling.rope_type == "llama3").
285
+ # llama.cpp represents this via a single extra tensor: "rope_freqs.weight" (aka MODEL_TENSOR.ROPE_FREQS).
286
+ rope_params = self.rope_parameters.get("full_attention", self.rope_parameters)
287
+ rope_type = rope_params.get("rope_type") or ""
288
+ if rope_type.lower() != "llama3":
289
+ return
290
+
291
+ # Step35 configs can carry per-layer rope_theta as a list; for llama3 rope factors we use the base value.
292
+ rope_theta = self.hparams.get("rope_theta", 10000.0)
293
+ if isinstance(rope_theta, list):
294
+ rope_theta = rope_theta[0]
295
+ base = float(rope_theta)
296
+
297
+ if (storage_dim := self.hparams.get("head_dim")) is None:
298
+ storage_dim = self.hparams["hidden_size"] // self.hparams["num_attention_heads"]
299
+ storage_dim = int(storage_dim)
300
+
301
+ # Llama 3 factors apply only to the rotary dims used by full_attention layers
302
+ # (partial_rotary_factor * head_dim). Remaining slots are padded with 1.0 so
303
+ # sliding_attention layers remain unaffected. set_gguf_parameters already
304
+ # guarantees at least one full_attention layer.
305
+ layer_types = (self.hparams.get("layer_types") or [])[: self.block_count]
306
+ partial_rotary_factors = (self.hparams.get("partial_rotary_factors") or [])[: self.block_count]
307
+ full_attention_factor = next(
308
+ float(f) for lt, f in zip(layer_types, partial_rotary_factors) if lt == "full_attention"
309
+ )
310
+ rotary_dim = int(storage_dim * full_attention_factor)
311
+
312
+ freqs = 1.0 / (base ** (torch.arange(0, rotary_dim, 2, dtype=torch.float32) / rotary_dim))
313
+
314
+ factor = float(rope_params.get("factor", 8.0))
315
+ low_freq_factor = float(rope_params.get("low_freq_factor", 1.0))
316
+ high_freq_factor = float(rope_params.get("high_freq_factor", 4.0))
317
+ old_context_len = int(rope_params.get("original_max_position_embeddings", 8192))
318
+
319
+ low_freq_wavelen = old_context_len / low_freq_factor
320
+ high_freq_wavelen = old_context_len / high_freq_factor
321
+
322
+ rope_factors: list[float] = []
323
+ for freq in freqs:
324
+ wavelen = 2 * math.pi / float(freq)
325
+ if wavelen < high_freq_wavelen:
326
+ rope_factors.append(1.0)
327
+ elif wavelen > low_freq_wavelen:
328
+ rope_factors.append(factor)
329
+ else:
330
+ smooth = (old_context_len / wavelen - low_freq_factor) / (high_freq_factor - low_freq_factor)
331
+ rope_factors.append(1.0 / ((1.0 - smooth) / factor + smooth))
332
+
333
+ # Pad to head_dim/2 with 1.0 so non-scaled layers remain neutral.
334
+ if len(rope_factors) < storage_dim // 2:
335
+ rope_factors.extend([1.0] * (storage_dim // 2 - len(rope_factors)))
336
+
337
+ yield (self.format_tensor_name(gguf.MODEL_TENSOR.ROPE_FREQS), torch.tensor(rope_factors, dtype=torch.float32))
llama.cpp/conversion/t5.py ADDED
@@ -0,0 +1,286 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+
6
+ from typing import Iterable, TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from torch import Tensor
10
+
11
+ from .base import ModelBase, SentencePieceTokenTypes, TextModel, gguf, logger
12
+
13
+
14
+ @ModelBase.register("T5WithLMHeadModel")
15
+ @ModelBase.register("T5ForConditionalGeneration")
16
+ @ModelBase.register("MT5ForConditionalGeneration")
17
+ @ModelBase.register("UMT5ForConditionalGeneration")
18
+ @ModelBase.register("UMT5Model")
19
+ class T5Model(TextModel):
20
+ model_arch = gguf.MODEL_ARCH.T5
21
+
22
+ def __init__(self, *args, **kwargs):
23
+ super().__init__(*args, **kwargs)
24
+ self.shared_token_embeddings_found = False
25
+
26
+ def set_vocab(self):
27
+ # to avoid TypeError: Descriptors cannot be created directly
28
+ # exception when importing sentencepiece_model_pb2
29
+ os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python"
30
+ from sentencepiece import SentencePieceProcessor
31
+ from sentencepiece import sentencepiece_model_pb2 as model
32
+
33
+ tokenizer_path = self.dir_model / 'tokenizer.model'
34
+
35
+ # many older models use spiece.model tokenizer model filename
36
+ if not tokenizer_path.is_file():
37
+ tokenizer_path = self.dir_model / 'spiece.model'
38
+
39
+ if not tokenizer_path.is_file():
40
+ raise FileNotFoundError(f"File not found: {tokenizer_path}")
41
+
42
+ sentencepiece_model = model.ModelProto() # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
43
+ sentencepiece_model.ParseFromString(open(tokenizer_path, "rb").read())
44
+
45
+ # some models like Pile-T5 family use BPE tokenizer instead of Unigram
46
+ if sentencepiece_model.trainer_spec.model_type == 2: # BPE
47
+ # assure the tokenizer model file name is correct
48
+ assert tokenizer_path.name == 'tokenizer.model'
49
+ return self._set_vocab_sentencepiece()
50
+ else:
51
+ assert sentencepiece_model.trainer_spec.model_type == 1 # UNIGRAM
52
+
53
+ add_prefix = sentencepiece_model.normalizer_spec.add_dummy_prefix
54
+ remove_whitespaces = sentencepiece_model.normalizer_spec.remove_extra_whitespaces
55
+ precompiled_charsmap = sentencepiece_model.normalizer_spec.precompiled_charsmap
56
+
57
+ tokenizer = SentencePieceProcessor()
58
+ tokenizer.LoadFromFile(str(tokenizer_path))
59
+
60
+ vocab_size = self.hparams.get('vocab_size', tokenizer.vocab_size())
61
+
62
+ tokens: list[bytes] = [f"[PAD{i}]".encode("utf-8") for i in range(vocab_size)]
63
+ scores: list[float] = [-10000.0] * vocab_size
64
+ toktypes: list[int] = [SentencePieceTokenTypes.UNUSED] * vocab_size
65
+
66
+ for token_id in range(tokenizer.vocab_size()):
67
+ piece = tokenizer.IdToPiece(token_id)
68
+ text = piece.encode("utf-8")
69
+ score = tokenizer.GetScore(token_id)
70
+
71
+ toktype = SentencePieceTokenTypes.NORMAL
72
+ if tokenizer.IsUnknown(token_id):
73
+ toktype = SentencePieceTokenTypes.UNKNOWN
74
+ elif tokenizer.IsControl(token_id):
75
+ toktype = SentencePieceTokenTypes.CONTROL
76
+ elif tokenizer.IsUnused(token_id):
77
+ toktype = SentencePieceTokenTypes.UNUSED
78
+ elif tokenizer.IsByte(token_id):
79
+ toktype = SentencePieceTokenTypes.BYTE
80
+
81
+ tokens[token_id] = text
82
+ scores[token_id] = score
83
+ toktypes[token_id] = toktype
84
+
85
+ added_tokens_file = self.dir_model / 'added_tokens.json'
86
+ if added_tokens_file.is_file():
87
+ with open(added_tokens_file, "r", encoding="utf-8") as f:
88
+ added_tokens_json = json.load(f)
89
+ for key in added_tokens_json:
90
+ token_id = added_tokens_json[key]
91
+ if token_id >= vocab_size:
92
+ logger.warning(f'ignore token {token_id}: id is out of range, max={vocab_size - 1}')
93
+ continue
94
+
95
+ tokens[token_id] = key.encode("utf-8")
96
+ scores[token_id] = -1000.0
97
+ toktypes[token_id] = SentencePieceTokenTypes.USER_DEFINED
98
+
99
+ if vocab_size > len(tokens):
100
+ pad_count = vocab_size - len(tokens)
101
+ logger.debug(f"Padding vocab with {pad_count} token(s) - [PAD1] through [PAD{pad_count}]")
102
+ for i in range(1, pad_count + 1):
103
+ tokens.append(bytes(f"[PAD{i}]", encoding="utf-8"))
104
+ scores.append(-1000.0)
105
+ toktypes.append(SentencePieceTokenTypes.UNUSED)
106
+
107
+ self.gguf_writer.add_tokenizer_model("t5")
108
+ self.gguf_writer.add_tokenizer_pre("default")
109
+ self.gguf_writer.add_token_list(tokens)
110
+ self.gguf_writer.add_token_scores(scores)
111
+ self.gguf_writer.add_token_types(toktypes)
112
+ self.gguf_writer.add_add_space_prefix(add_prefix)
113
+ self.gguf_writer.add_remove_extra_whitespaces(remove_whitespaces)
114
+ if precompiled_charsmap:
115
+ self.gguf_writer.add_precompiled_charsmap(precompiled_charsmap)
116
+
117
+ special_vocab = gguf.SpecialVocab(self.dir_model, n_vocab=len(tokens))
118
+ special_vocab.add_to_gguf(self.gguf_writer)
119
+
120
+ def set_gguf_parameters(self):
121
+ if (n_ctx := self.find_hparam(["n_positions"], optional=True)) is None:
122
+ logger.warning("Couldn't find context length in config.json, assuming default value of 512")
123
+ n_ctx = 512
124
+ self.gguf_writer.add_context_length(n_ctx)
125
+ self.gguf_writer.add_embedding_length(self.hparams["d_model"])
126
+ self.gguf_writer.add_feed_forward_length(self.hparams["d_ff"])
127
+ self.gguf_writer.add_block_count(self.block_count)
128
+ if (dec_n_layer := self.hparams.get("num_decoder_layers")) is not None:
129
+ self.gguf_writer.add_decoder_block_count(dec_n_layer)
130
+ self.gguf_writer.add_head_count(self.hparams["num_heads"])
131
+ self.gguf_writer.add_key_length(self.hparams["d_kv"])
132
+ self.gguf_writer.add_value_length(self.hparams["d_kv"])
133
+ self.gguf_writer.add_layer_norm_eps(self.hparams["layer_norm_epsilon"])
134
+ self.gguf_writer.add_relative_attn_buckets_count(self.hparams["relative_attention_num_buckets"])
135
+ self.gguf_writer.add_layer_norm_rms_eps(self.hparams["layer_norm_epsilon"])
136
+ self.gguf_writer.add_decoder_start_token_id(self.hparams["decoder_start_token_id"])
137
+ self.gguf_writer.add_file_type(self.ftype)
138
+
139
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
140
+ # T5 based models contain shared token embeddings tensors saved randomly as either "encoder.embed_tokens.weight",
141
+ # "decoder.embed_tokens.weight" or "shared.weight" tensor. In some models there are even multiple of them stored
142
+ # in the safetensors files. We use the first tensor from these three as the token embeddings for both encoder
143
+ # and decoder and ignore the remaining ones.
144
+ if name in ["decoder.embed_tokens.weight", "encoder.embed_tokens.weight", "shared.weight"]:
145
+ if not self.shared_token_embeddings_found:
146
+ name = "shared.weight"
147
+ self.shared_token_embeddings_found = True
148
+ else:
149
+ logger.debug(f"Skipping shared tensor {name!r} in safetensors so that convert can end normally.")
150
+ return
151
+
152
+ yield from super().modify_tensors(data_torch, name, bid)
153
+
154
+
155
+ @ModelBase.register("T5EncoderModel")
156
+ class T5EncoderModel(TextModel):
157
+ model_arch = gguf.MODEL_ARCH.T5ENCODER
158
+
159
+ def __init__(self, *args, **kwargs):
160
+ super().__init__(*args, **kwargs)
161
+ self.shared_token_embeddings_found = False
162
+
163
+ def set_vocab(self):
164
+ # to avoid TypeError: Descriptors cannot be created directly
165
+ # exception when importing sentencepiece_model_pb2
166
+ os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python"
167
+ from sentencepiece import SentencePieceProcessor
168
+ from sentencepiece import sentencepiece_model_pb2 as model
169
+
170
+ tokenizer_path = self.dir_model / 'tokenizer.model'
171
+
172
+ # many older models use spiece.model tokenizer model filename
173
+ if not tokenizer_path.is_file():
174
+ tokenizer_path = self.dir_model / 'spiece.model'
175
+
176
+ if not tokenizer_path.is_file():
177
+ raise FileNotFoundError(f"File not found: {tokenizer_path}")
178
+
179
+ sentencepiece_model = model.ModelProto() # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
180
+ sentencepiece_model.ParseFromString(open(tokenizer_path, "rb").read())
181
+
182
+ # some models like Pile-T5 family use BPE tokenizer instead of Unigram
183
+ if sentencepiece_model.trainer_spec.model_type == 2: # BPE
184
+ # assure the tokenizer model file name is correct
185
+ assert tokenizer_path.name == 'tokenizer.model'
186
+ return self._set_vocab_sentencepiece()
187
+ else:
188
+ assert sentencepiece_model.trainer_spec.model_type == 1 # UNIGRAM
189
+
190
+ add_prefix = sentencepiece_model.normalizer_spec.add_dummy_prefix
191
+ remove_whitespaces = sentencepiece_model.normalizer_spec.remove_extra_whitespaces
192
+ precompiled_charsmap = sentencepiece_model.normalizer_spec.precompiled_charsmap
193
+
194
+ tokenizer = SentencePieceProcessor()
195
+ tokenizer.LoadFromFile(str(tokenizer_path))
196
+
197
+ vocab_size = self.hparams.get('vocab_size', tokenizer.vocab_size())
198
+
199
+ tokens: list[bytes] = [f"[PAD{i}]".encode("utf-8") for i in range(vocab_size)]
200
+ scores: list[float] = [-10000.0] * vocab_size
201
+ toktypes: list[int] = [SentencePieceTokenTypes.UNUSED] * vocab_size
202
+
203
+ for token_id in range(tokenizer.vocab_size()):
204
+ piece = tokenizer.IdToPiece(token_id)
205
+ text = piece.encode("utf-8")
206
+ score = tokenizer.GetScore(token_id)
207
+
208
+ toktype = SentencePieceTokenTypes.NORMAL
209
+ if tokenizer.IsUnknown(token_id):
210
+ toktype = SentencePieceTokenTypes.UNKNOWN
211
+ elif tokenizer.IsControl(token_id):
212
+ toktype = SentencePieceTokenTypes.CONTROL
213
+ elif tokenizer.IsUnused(token_id):
214
+ toktype = SentencePieceTokenTypes.UNUSED
215
+ elif tokenizer.IsByte(token_id):
216
+ toktype = SentencePieceTokenTypes.BYTE
217
+
218
+ tokens[token_id] = text
219
+ scores[token_id] = score
220
+ toktypes[token_id] = toktype
221
+
222
+ added_tokens_file = self.dir_model / 'added_tokens.json'
223
+ if added_tokens_file.is_file():
224
+ with open(added_tokens_file, "r", encoding="utf-8") as f:
225
+ added_tokens_json = json.load(f)
226
+ for key in added_tokens_json:
227
+ token_id = added_tokens_json[key]
228
+ if token_id >= vocab_size:
229
+ logger.warning(f'ignore token {token_id}: id is out of range, max={vocab_size - 1}')
230
+ continue
231
+
232
+ tokens[token_id] = key.encode("utf-8")
233
+ scores[token_id] = -1000.0
234
+ toktypes[token_id] = SentencePieceTokenTypes.USER_DEFINED
235
+
236
+ if vocab_size > len(tokens):
237
+ pad_count = vocab_size - len(tokens)
238
+ logger.debug(f"Padding vocab with {pad_count} token(s) - [PAD1] through [PAD{pad_count}]")
239
+ for i in range(1, pad_count + 1):
240
+ tokens.append(bytes(f"[PAD{i}]", encoding="utf-8"))
241
+ scores.append(-1000.0)
242
+ toktypes.append(SentencePieceTokenTypes.UNUSED)
243
+
244
+ self.gguf_writer.add_tokenizer_model("t5")
245
+ self.gguf_writer.add_tokenizer_pre("default")
246
+ self.gguf_writer.add_token_list(tokens)
247
+ self.gguf_writer.add_token_scores(scores)
248
+ self.gguf_writer.add_token_types(toktypes)
249
+ self.gguf_writer.add_add_space_prefix(add_prefix)
250
+ self.gguf_writer.add_remove_extra_whitespaces(remove_whitespaces)
251
+ if precompiled_charsmap:
252
+ self.gguf_writer.add_precompiled_charsmap(precompiled_charsmap)
253
+
254
+ special_vocab = gguf.SpecialVocab(self.dir_model, n_vocab=len(tokens))
255
+ special_vocab.add_to_gguf(self.gguf_writer)
256
+
257
+ def set_gguf_parameters(self):
258
+ if (n_ctx := self.find_hparam(["n_positions"], optional=True)) is None:
259
+ logger.warning("Couldn't find context length in config.json, assuming default value of 512")
260
+ n_ctx = 512
261
+ self.gguf_writer.add_context_length(n_ctx)
262
+ self.gguf_writer.add_embedding_length(self.hparams["d_model"])
263
+ self.gguf_writer.add_feed_forward_length(self.hparams["d_ff"])
264
+ self.gguf_writer.add_block_count(self.block_count)
265
+ self.gguf_writer.add_head_count(self.hparams["num_heads"])
266
+ self.gguf_writer.add_key_length(self.hparams["d_kv"])
267
+ self.gguf_writer.add_value_length(self.hparams["d_kv"])
268
+ self.gguf_writer.add_layer_norm_eps(self.hparams["layer_norm_epsilon"])
269
+ self.gguf_writer.add_relative_attn_buckets_count(self.hparams["relative_attention_num_buckets"])
270
+ self.gguf_writer.add_layer_norm_rms_eps(self.hparams["layer_norm_epsilon"])
271
+ self.gguf_writer.add_file_type(self.ftype)
272
+
273
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
274
+ # T5 based models contain shared token embeddings tensors saved randomly as either "encoder.embed_tokens.weight",
275
+ # "decoder.embed_tokens.weight" or "shared.weight" tensor. In some models there are even multiple of them stored
276
+ # in the safetensors files. We use the first tensor from these three as the token embeddings for both encoder
277
+ # and decoder and ignore the remaining ones.
278
+ if name in ["decoder.embed_tokens.weight", "encoder.embed_tokens.weight", "shared.weight"]:
279
+ if not self.shared_token_embeddings_found:
280
+ name = "shared.weight"
281
+ self.shared_token_embeddings_found = True
282
+ else:
283
+ logger.debug(f"Skipping shared tensor {name!r} in safetensors so that convert can end normally.")
284
+ return
285
+
286
+ yield from super().modify_tensors(data_torch, name, bid)
llama.cpp/conversion/talkie.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Iterable, TYPE_CHECKING
4
+
5
+ import torch
6
+
7
+ if TYPE_CHECKING:
8
+ from torch import Tensor
9
+
10
+ from .base import LazyTorchTensor, ModelBase, TextModel, gguf
11
+
12
+
13
+ @ModelBase.register("TalkieForCausalLM")
14
+ class TalkieModel(TextModel):
15
+ model_arch = gguf.MODEL_ARCH.TALKIE
16
+
17
+ def set_gguf_parameters(self):
18
+ super().set_gguf_parameters()
19
+ # Talkie used F.rms_norm without an explicit eps
20
+ self.gguf_writer.add_layer_norm_rms_eps(torch.finfo(torch.float32).eps)
21
+
22
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
23
+ prefix = f"model.blocks.{bid}." if bid is not None else ""
24
+ suffix = name.removeprefix(prefix)
25
+
26
+ if suffix == "attn_gain.a_g":
27
+ yield self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_OUT, bid, ".scale"), data_torch
28
+ return
29
+ elif suffix == "mlp_gain.a_g":
30
+ yield self.format_tensor_name(gguf.MODEL_TENSOR.FFN_DOWN, bid, ".scale"), data_torch
31
+ return
32
+ elif suffix == "lm_head_gain.w_g":
33
+ self.gguf_writer.add_logit_scale(LazyTorchTensor.to_eager(data_torch).item())
34
+ return
35
+ elif suffix in ("attn.attn_query.weight", "attn.attn_key.weight"):
36
+ # absorb inverse rope
37
+ head_dim = self.hparams["head_dim"]
38
+ shape = data_torch.shape
39
+ data_torch = torch.reshape(data_torch, (-1, head_dim, shape[-1]))
40
+ signs = torch.ones((1, head_dim, 1), dtype=data_torch.dtype)
41
+ signs[:, head_dim // 2 :, :] = -1
42
+ if self.lazy:
43
+ signs = LazyTorchTensor.from_eager(signs)
44
+ # (n_head, head_dim, n_in) -> (n_out, n_in)
45
+ data_torch = torch.reshape(data_torch * signs, shape)
46
+ elif suffix == "attn.head_gain.head_g":
47
+ # allow head gain to broadcast
48
+ data_torch = data_torch.unsqueeze(-1)
49
+
50
+ if not name.endswith(".weight"):
51
+ name += ".weight"
52
+
53
+ yield from super().modify_tensors(data_torch, name, bid)
llama.cpp/conversion/ultravox.py ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Iterable, TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from torch import Tensor
7
+
8
+ from .base import MmprojModel, ModelBase, TextModel, gguf
9
+
10
+
11
+ @ModelBase.register("UltravoxModel")
12
+ class UltravoxModel(TextModel):
13
+ model_arch = gguf.MODEL_ARCH.LLAMA # dummy
14
+
15
+ def __init__(self, *args, **kwargs):
16
+ super().__init__(*args, **kwargs)
17
+ raise NotImplementedError("Ultravox does not have text decoder. Instead, it uses Llama or other models for text. If you want to get the audio encoder, please use --mmproj argument")
18
+
19
+
20
+ @ModelBase.register("GlmasrModel")
21
+ class GlmASRWhisperEncoderModel(MmprojModel):
22
+ has_vision_encoder = False
23
+ has_audio_encoder = True
24
+
25
+ def __init__(self, *args, **kwargs):
26
+ super().__init__(*args, **kwargs)
27
+ if "hidden_size" not in self.hparams and "intermediate_size" not in self.hparams:
28
+ self.hparams["hidden_size"] = self.hparams["d_model"]
29
+ self.hparams["intermediate_size"] = self.hparams["encoder_ffn_dim"]
30
+ self.hparams["num_attention_heads"] = self.hparams["encoder_attention_heads"]
31
+
32
+ def set_gguf_parameters(self):
33
+ super().set_gguf_parameters()
34
+ self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.GLMA)
35
+ self.gguf_writer.add_audio_num_mel_bins(self.hparams["num_mel_bins"])
36
+ self.gguf_writer.add_audio_attention_layernorm_eps(self.hparams.get("layer_norm_eps", 1e-5))
37
+ self.gguf_writer.add_audio_stack_factor(self.global_config["merge_factor"])
38
+
39
+ def tensor_force_quant(self, name, new_name, bid, n_dims):
40
+ if ".conv" in name and ".weight" in name:
41
+ return gguf.GGMLQuantizationType.F16
42
+ return super().tensor_force_quant(name, new_name, bid, n_dims)
43
+
44
+ @classmethod
45
+ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
46
+ name, gen = item
47
+
48
+ if name.startswith(("model.", "lm_head.")):
49
+ # skip language model tensors
50
+ return None
51
+
52
+ if name.startswith("audio_encoder.whisper."):
53
+ name = name.replace("audio_encoder.whisper.","audio_tower.")
54
+ if "audio_encoder.layer_norm." in name or "audio_encoder.proj." in name:
55
+ name = name.replace("audio_encoder.", "audio_encoder.adapting.")
56
+ if name.startswith("audio_encoder.adapting."):
57
+ name = name.replace("audio_encoder.adapting.","audio.multi_modal_projector.")
58
+ if ".layer_norm." in name:
59
+ name = name.replace(".layer_norm.", ".ln_pre.")
60
+ if ".0." in name:
61
+ name = name.replace(".0.", ".linear_1.")
62
+ if ".2." in name:
63
+ name = name.replace(".2.", ".linear_2.")
64
+
65
+ return super().filter_tensors((name, gen))
66
+
67
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
68
+ if name.startswith("audio_encoder.audio_bos_eos_token."):
69
+ yield from super().modify_tensors(data_torch[0], "model.vision.boi", bid)
70
+ yield from super().modify_tensors(data_torch[1], "model.vision.eoi", bid)
71
+ return
72
+
73
+ if name.startswith("audio_encoder.adapting."):
74
+ if ".proj." in name:
75
+ return
76
+
77
+ if "conv1.bias" in name or "conv2.bias" in name:
78
+ # transpose conv1 and conv2 bias
79
+ data_torch = data_torch.unsqueeze(-1)
80
+
81
+ yield from super().modify_tensors(data_torch, name, bid)
82
+
83
+
84
+ @ModelBase.register("Qwen2AudioForConditionalGeneration")
85
+ class WhisperEncoderModel(MmprojModel):
86
+ has_vision_encoder = False # no vision encoder
87
+ has_audio_encoder = True
88
+
89
+ def __init__(self, *args, **kwargs):
90
+ super().__init__(*args, **kwargs)
91
+ if "hidden_size" not in self.hparams and "intermediate_size" not in self.hparams:
92
+ self.hparams["hidden_size"] = self.hparams["d_model"]
93
+ self.hparams["intermediate_size"] = self.hparams["encoder_ffn_dim"]
94
+ self.hparams["num_attention_heads"] = self.hparams["encoder_attention_heads"]
95
+
96
+ def set_gguf_parameters(self):
97
+ super().set_gguf_parameters()
98
+ self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.QWEN2A)
99
+ self.gguf_writer.add_audio_num_mel_bins(self.hparams["num_mel_bins"])
100
+ self.gguf_writer.add_audio_attention_layernorm_eps(self.hparams.get("layer_norm_eps", 1e-5))
101
+
102
+ def tensor_force_quant(self, name, new_name, bid, n_dims):
103
+ if ".conv" in name and ".weight" in name:
104
+ return gguf.GGMLQuantizationType.F16
105
+ return super().tensor_force_quant(name, new_name, bid, n_dims)
106
+
107
+ @classmethod
108
+ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
109
+ name, gen = item
110
+
111
+ # prevent clash naming with vision tensors
112
+ if name.startswith("multi_modal_projector"):
113
+ name = "audio." + name
114
+
115
+ return super().filter_tensors((name, gen))
116
+
117
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
118
+ if "conv1.bias" in name or "conv2.bias" in name:
119
+ # transpose conv1 and conv2 bias
120
+ data_torch = data_torch.unsqueeze(-1)
121
+
122
+ yield from super().modify_tensors(data_torch, name, bid)
123
+
124
+
125
+ @ModelBase.register("UltravoxModel")
126
+ class UltravoxWhisperEncoderModel(WhisperEncoderModel):
127
+ has_vision_encoder = False # no vision encoder
128
+ has_audio_encoder = True
129
+
130
+ def set_gguf_parameters(self):
131
+ super().set_gguf_parameters()
132
+ self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.ULTRAVOX)
133
+ self.gguf_writer.add_audio_stack_factor(self.global_config["stack_factor"])
134
+
135
+
136
+ @ModelBase.register("MERaLiON2ForConditionalGeneration")
137
+ class MERaLiONWhisperEncoderModel(WhisperEncoderModel):
138
+ has_vision_encoder = False
139
+ has_audio_encoder = True
140
+
141
+ def get_audio_config(self) -> dict[str, Any] | None:
142
+ return self.global_config.get("speech_config")
143
+
144
+ def set_gguf_parameters(self):
145
+ super().set_gguf_parameters()
146
+ self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.MERALION)
147
+ self.gguf_writer.add_audio_stack_factor(self.global_config.get("speech_mlp_scale_factor", 15))
148
+
149
+ @classmethod
150
+ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
151
+ name, gen = item
152
+
153
+ if name.startswith("text_decoder."):
154
+ return None
155
+
156
+ if name.startswith("speech_encoder."):
157
+ name = name.replace("speech_encoder.", "audio_tower.")
158
+
159
+ return super().filter_tensors((name, gen))
160
+
161
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
162
+ suffix = "." + name.rsplit(".", 1)[-1]
163
+
164
+ if name.startswith("ln_speech."):
165
+ yield (self.format_tensor_name(gguf.MODEL_TENSOR.A_MM_NORM_PRE, suffix=suffix), data_torch)
166
+ return
167
+
168
+ if name.startswith("speech_audio_adapter."):
169
+ if ".mlp_adapter.0." in name:
170
+ yield (self.format_tensor_name(gguf.MODEL_TENSOR.A_MMPROJ, 0, suffix=suffix), data_torch)
171
+ elif ".gate_proj." in name:
172
+ yield (self.format_tensor_name(gguf.MODEL_TENSOR.A_MMPROJ, 1, suffix=suffix), data_torch)
173
+ elif ".pool_proj." in name:
174
+ yield (self.format_tensor_name(gguf.MODEL_TENSOR.A_MMPROJ, 2, suffix=suffix), data_torch)
175
+ elif ".out_proj." in name:
176
+ yield (self.format_tensor_name(gguf.MODEL_TENSOR.A_MMPROJ, 3, suffix=suffix), data_torch)
177
+ return
178
+
179
+ yield from super().modify_tensors(data_torch, name, bid)
180
+
181
+
182
+ @ModelBase.register("VoxtralForConditionalGeneration")
183
+ class VoxtralWhisperEncoderModel(WhisperEncoderModel):
184
+ has_vision_encoder = False # no vision encoder
185
+ has_audio_encoder = True
186
+
187
+ def set_gguf_parameters(self):
188
+ super().set_gguf_parameters()
189
+ self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.VOXTRAL)
190
+ self.gguf_writer.add_audio_stack_factor(4) # == intermediate_size // hidden_size
191
+
192
+
193
+ @ModelBase.register("AudioFlamingo3ForConditionalGeneration")
194
+ class AudioFlamingo3WhisperEncoderModel(WhisperEncoderModel):
195
+ def set_gguf_parameters(self):
196
+ super().set_gguf_parameters()
197
+ self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.MUSIC_FLAMINGO)
198
+
199
+ def tensor_force_quant(self, name, new_name, bid, n_dims):
200
+ if ".conv" in name and ".weight" in name:
201
+ # Was trained in BF16, being safe, avoiding quantizing to FP16
202
+ return gguf.GGMLQuantizationType.F32
203
+ return super().tensor_force_quant(name, new_name, bid, n_dims)
llama.cpp/conversion/wavtokenizer.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable, TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from torch import Tensor
7
+
8
+ from .base import ModelBase, TextModel, gguf, logger
9
+
10
+
11
+ @ModelBase.register("WavTokenizerDec")
12
+ class WavTokenizerDecModel(TextModel):
13
+ model_arch = gguf.MODEL_ARCH.WAVTOKENIZER_DEC
14
+
15
+ @classmethod
16
+ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
17
+ name, gen = item
18
+
19
+ if \
20
+ name.endswith("codebook.cluster_size") or \
21
+ name.endswith("codebook.embed_avg") or \
22
+ name.endswith("codebook.inited"):
23
+ logger.debug(f"Skipping {name!r}")
24
+ return None
25
+
26
+ return super().filter_tensors(item)
27
+
28
+ def set_vocab(self):
29
+ self._set_vocab_none()
30
+
31
+ def set_gguf_parameters(self):
32
+ super().set_gguf_parameters()
33
+ self.gguf_writer.add_vocab_size (self.hparams["vocab_size"])
34
+ self.gguf_writer.add_features_length (self.hparams["n_embd_features"])
35
+ self.gguf_writer.add_feed_forward_length(self.hparams["n_ff"])
36
+ self.gguf_writer.add_group_norm_eps (self.hparams["group_norm_epsilon"])
37
+ self.gguf_writer.add_group_norm_groups (self.hparams["group_norm_groups"])
38
+
39
+ self.gguf_writer.add_posnet_embedding_length(self.hparams["posnet"]["n_embd"])
40
+ self.gguf_writer.add_posnet_block_count (self.hparams["posnet"]["n_layer"])
41
+
42
+ self.gguf_writer.add_convnext_embedding_length(self.hparams["convnext"]["n_embd"])
43
+ self.gguf_writer.add_convnext_block_count (self.hparams["convnext"]["n_layer"])
44
+
45
+ self.gguf_writer.add_causal_attention(False)
llama.cpp/conversion/xverse.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from typing import Iterable, TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from torch import Tensor
9
+
10
+ from .base import ModelBase, TextModel, gguf
11
+
12
+
13
+ @ModelBase.register("XverseForCausalLM")
14
+ class XverseModel(TextModel):
15
+ model_arch = gguf.MODEL_ARCH.XVERSE
16
+
17
+ def set_vocab(self):
18
+ assert (self.dir_model / "tokenizer.json").is_file()
19
+ dir_model = self.dir_model
20
+ hparams = self.hparams
21
+
22
+ tokens: list[bytes] = []
23
+ toktypes: list[int] = []
24
+
25
+ from transformers import AutoTokenizer
26
+ tokenizer = AutoTokenizer.from_pretrained(dir_model)
27
+ vocab_size = hparams.get("vocab_size", len(tokenizer.vocab)) # ty: ignore[unresolved-attribute]
28
+ # Since we are checking the maximum index, we need to ensure it's strictly less than vocab_size,
29
+ # because vocab_size is the count of items, and indexes start at 0.
30
+ max_vocab_index = max(tokenizer.get_vocab().values()) # ty: ignore[unresolved-attribute]
31
+ if max_vocab_index >= vocab_size:
32
+ raise ValueError("Vocabulary size exceeds expected maximum size.")
33
+
34
+ reverse_vocab: dict[int, str] = {id_: encoded_tok for encoded_tok, id_ in tokenizer.vocab.items()} # ty: ignore[unresolved-attribute]
35
+ added_vocab = tokenizer.get_added_vocab() # ty: ignore[unresolved-attribute]
36
+
37
+ for token_id in range(vocab_size):
38
+ token_text = reverse_vocab[token_id].encode('utf-8')
39
+ # replace "\x00" to string with length > 0
40
+ if token_text == b"\x00":
41
+ toktype = gguf.TokenType.BYTE # special
42
+ token_text = f"<{token_text}>".encode('utf-8')
43
+ elif re.fullmatch(br"<0x[0-9A-Fa-f]{2}>", token_text):
44
+ toktype = gguf.TokenType.BYTE # special
45
+ elif reverse_vocab[token_id] in added_vocab:
46
+ if tokenizer.added_tokens_decoder[token_id].special: # ty: ignore[unresolved-attribute]
47
+ toktype = gguf.TokenType.CONTROL
48
+ else:
49
+ toktype = gguf.TokenType.USER_DEFINED
50
+ else:
51
+ toktype = gguf.TokenType.NORMAL
52
+
53
+ tokens.append(token_text)
54
+ toktypes.append(toktype)
55
+
56
+ self.gguf_writer.add_tokenizer_model("llama")
57
+ self.gguf_writer.add_tokenizer_pre("default")
58
+ self.gguf_writer.add_token_list(tokens)
59
+ self.gguf_writer.add_token_types(toktypes)
60
+
61
+ special_vocab = gguf.SpecialVocab(dir_model, n_vocab=len(tokens))
62
+ special_vocab.add_to_gguf(self.gguf_writer)
63
+
64
+ def set_gguf_parameters(self):
65
+ super().set_gguf_parameters()
66
+
67
+ self.gguf_writer.add_tensor_data_layout("Meta AI original pth")
68
+ self.gguf_writer.add_rope_dimension_count(self.hparams["hidden_size"] // self.hparams["num_attention_heads"])
69
+
70
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
71
+ head_count = self.hparams["num_attention_heads"]
72
+ head_count_kv = self.hparams.get("num_key_value_heads", head_count)
73
+
74
+ # HF models permute some of the tensors, so we need to undo that
75
+ if name.endswith("q_proj.weight"):
76
+ data_torch = self._reverse_hf_permute(data_torch, head_count, head_count)
77
+ if name.endswith("k_proj.weight"):
78
+ data_torch = self._reverse_hf_permute(data_torch, head_count, head_count_kv)
79
+
80
+ yield from super().modify_tensors(data_torch, name, bid)
81
+
82
+ def _reverse_hf_permute(self, weights: Tensor, n_head: int, n_kv_head: int | None = None) -> Tensor:
83
+ if n_kv_head is not None and n_head != n_kv_head:
84
+ n_head //= n_kv_head
85
+
86
+ return (
87
+ weights.reshape(n_head, 2, weights.shape[0] // n_head // 2, *weights.shape[1:])
88
+ .swapaxes(1, 2)
89
+ .reshape(weights.shape)
90
+ )
llama.cpp/conversion/youtuvl.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable, Iterable, TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from torch import Tensor
7
+
8
+ from .base import MmprojModel, ModelBase, gguf, logger
9
+
10
+
11
+ @ModelBase.register("YoutuVLForConditionalGeneration")
12
+ class YoutuVLVisionModel(MmprojModel):
13
+ def __init__(self, *args, **kwargs):
14
+ super().__init__(*args, **kwargs)
15
+ assert self.hparams_vision is not None
16
+ self.hparams_vision["image_size"] = self.hparams_vision.get("image_size", 560)
17
+
18
+ def set_gguf_parameters(self):
19
+ super().set_gguf_parameters()
20
+
21
+ self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.YOUTUVL)
22
+ self.gguf_writer.add_vision_attention_layernorm_eps(self.hparams.get("layer_norm_eps", 1e-6))
23
+
24
+ # Handle activation function
25
+ hidden_act = str(self.hparams.get("hidden_act", "gelu_pytorch_tanh")).lower()
26
+ if hidden_act in ("gelu", "gelu_pytorch_tanh", "gelu_fast", "gelu_new", "gelu_accurate"):
27
+ self.gguf_writer.add_vision_use_gelu(True)
28
+ elif hidden_act == "silu":
29
+ self.gguf_writer.add_vision_use_silu(True)
30
+ else:
31
+ raise ValueError(f"Unsupported activation function for YOUTUVL: {hidden_act}")
32
+
33
+ self.gguf_writer.add_vision_spatial_merge_size(self.hparams.get("spatial_merge_size", 2))
34
+
35
+ window_size = self.hparams.get("window_size")
36
+ if window_size is not None:
37
+ self.gguf_writer.add_vision_window_size(window_size)
38
+ # fullatt_block_indexes contains explicit layer indices that use full attention
39
+ # e.g., [2, 5, 8, 11] means layers 2, 5, 8, 11 use full attention
40
+ # All other layers use window attention
41
+ fullatt_block_indexes = self.hparams.get("fullatt_block_indexes")
42
+ assert fullatt_block_indexes is not None, "fullatt_block_indexes is required for youtuvl"
43
+ # Store the explicit layer indices for YoutuVL (irregular pattern approach)
44
+ self.gguf_writer.add_vision_wa_layer_indexes(layers=fullatt_block_indexes)
45
+
46
+ @classmethod
47
+ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
48
+ name, gen = item
49
+
50
+ # Skip language model tensors
51
+ skip_prefixes = ('lm_head.', 'model.layers.', 'model.embed_tokens.', 'model.norm.')
52
+ if name.startswith(skip_prefixes):
53
+ return None
54
+
55
+ return super().filter_tensors(item)
56
+
57
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
58
+ # Try to map the tensor using TensorNameMap (handles vision encoder and projector)
59
+ try:
60
+ yield from super().modify_tensors(data_torch, name, bid)
61
+ except ValueError:
62
+ # If mapping fails, log warning and skip
63
+ logger.warning(f"Cannot map tensor: {name}")
64
+ return
llama.cpp/convert_hf_to_gguf.py ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import logging
8
+ import os
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ import torch
13
+
14
+ if 'NO_LOCAL_GGUF' not in os.environ:
15
+ sys.path.insert(1, str(Path(__file__).parent / 'gguf-py'))
16
+ import gguf
17
+
18
+ from conversion import (
19
+ ModelBase,
20
+ ModelType,
21
+ get_model_architecture,
22
+ get_model_class,
23
+ logger,
24
+ print_registered_models,
25
+ _mistral_common_installed,
26
+ _mistral_import_error_msg,
27
+ )
28
+
29
+
30
+ def split_str_to_n_bytes(split_str: str) -> int:
31
+ if split_str.endswith("K"):
32
+ n = int(split_str[:-1]) * 1000
33
+ elif split_str.endswith("M"):
34
+ n = int(split_str[:-1]) * 1000 * 1000
35
+ elif split_str.endswith("G"):
36
+ n = int(split_str[:-1]) * 1000 * 1000 * 1000
37
+ elif split_str.isnumeric():
38
+ n = int(split_str)
39
+ else:
40
+ raise ValueError(f"Invalid split size: {split_str}, must be a number, optionally followed by K, M, or G")
41
+
42
+ if n < 0:
43
+ raise ValueError(f"Invalid split size: {split_str}, must be positive")
44
+
45
+ return n
46
+
47
+
48
+ def parse_args() -> argparse.Namespace:
49
+ parser = argparse.ArgumentParser(
50
+ description="Convert a huggingface model to a GGML compatible file")
51
+ parser.add_argument(
52
+ "--vocab-only", action="store_true",
53
+ help="extract only the vocab",
54
+ )
55
+ parser.add_argument(
56
+ "--outfile", type=Path,
57
+ help="path to write to; default: based on input. {ftype} will be replaced by the outtype.",
58
+ )
59
+ parser.add_argument(
60
+ "--outtype", type=str, choices=["f32", "f16", "bf16", "q8_0", "tq1_0", "tq2_0", "auto"], default="auto",
61
+ help="output format - use f32 for float32, f16 for float16, bf16 for bfloat16, q8_0 for Q8_0, tq1_0 or tq2_0 for ternary, and auto for the highest-fidelity 16-bit float type",
62
+ )
63
+ parser.add_argument(
64
+ "--bigendian", action="store_true",
65
+ help="model is executed on big endian machine",
66
+ )
67
+ parser.add_argument(
68
+ "model", type=str,
69
+ help="directory containing model file or huggingface repository ID (if --remote)",
70
+ nargs="?",
71
+ )
72
+ parser.add_argument(
73
+ "--use-temp-file", action="store_true",
74
+ help="use the tempfile library while processing (helpful when running out of memory, process killed)",
75
+ )
76
+ parser.add_argument(
77
+ "--no-lazy", action="store_true",
78
+ help="use more RAM by computing all outputs before writing (use in case lazy evaluation is broken)",
79
+ )
80
+ parser.add_argument(
81
+ "--model-name", type=str, default=None,
82
+ help="name of the model",
83
+ )
84
+ parser.add_argument(
85
+ "--verbose", action="store_true",
86
+ help="increase output verbosity",
87
+ )
88
+ parser.add_argument(
89
+ "--split-max-tensors", type=int, default=0,
90
+ help="max tensors in each split",
91
+ )
92
+ parser.add_argument(
93
+ "--split-max-size", type=str, default="0",
94
+ help="max size per split N(M|G)",
95
+ )
96
+ parser.add_argument(
97
+ "--dry-run", action="store_true",
98
+ help="only print out a split plan and exit, without writing any new files",
99
+ )
100
+ parser.add_argument(
101
+ "--no-tensor-first-split", action="store_true",
102
+ help="do not add tensors to the first split (disabled by default)"
103
+ )
104
+ parser.add_argument(
105
+ "--metadata", type=Path,
106
+ help="Specify the path for an authorship metadata override file"
107
+ )
108
+ parser.add_argument(
109
+ "--print-supported-models", action="store_true",
110
+ help="Print the supported models"
111
+ )
112
+ parser.add_argument(
113
+ "--remote", action="store_true",
114
+ help="(Experimental) Read safetensors file remotely without downloading to disk. Config and tokenizer files will still be downloaded. To use this feature, you need to specify Hugging Face model repo name instead of a local directory. For example: 'HuggingFaceTB/SmolLM2-1.7B-Instruct'. Note: To access gated repo, set HF_TOKEN environment variable to your Hugging Face token.",
115
+ )
116
+ parser.add_argument(
117
+ "--mmproj", action="store_true",
118
+ help="Export multimodal projector (mmproj) for vision models. This will only work on some vision models. An 'mmproj-' prefix will be added to the output file name.",
119
+ )
120
+ parser.add_argument(
121
+ "--mtp", action="store_true",
122
+ help="Export only the multi-token prediction (MTP) head as a separate GGUF, suitable for use as a speculative draft. An 'mtp-' prefix will be added to the output file name.",
123
+ )
124
+ parser.add_argument(
125
+ "--no-mtp", action="store_true",
126
+ help="Exclude the multi-token prediction (MTP) head from the converted GGUF. Pair with --mtp on a second run to publish trunk and MTP as two files. Note: the split form duplicates embeddings, but even though the bundled default is more space-efficient overall, this allows differing quantization which may be more performant.",
127
+ )
128
+ parser.add_argument(
129
+ "--mistral-format", action="store_true",
130
+ help="Whether the model is stored following the Mistral format.",
131
+ )
132
+ parser.add_argument(
133
+ "--disable-mistral-community-chat-template", action="store_true",
134
+ help=(
135
+ "Whether to disable usage of Mistral community chat templates. If set, use the Mistral official `mistral-common` library for tokenization and detokenization of Mistral models. "
136
+ "Using `mistral-common` ensure correctness and zero-day support of tokenization for models converted from the Mistral format but requires to manually setup the tokenization server."
137
+ )
138
+ )
139
+
140
+ parser.add_argument(
141
+ "--sentence-transformers-dense-modules", action="store_true",
142
+ help=("Whether to include sentence-transformers dense modules. "
143
+ "It can be used for sentence-transformers models, like google/embeddinggemma-300m. "
144
+ "Default these modules are not included.")
145
+ )
146
+
147
+ parser.add_argument(
148
+ "--fuse-gate-up-exps", action="store_true",
149
+ help="Fuse gate_exps and up_exps tensors into a single gate_up_exps tensor for MoE models.",
150
+ )
151
+ parser.add_argument(
152
+ "--fp8-as-q8", action="store_true",
153
+ help="Store tensors dequantized from FP8 as Q8_0 instead of BF16/F16.",
154
+ )
155
+
156
+ parser.add_argument(
157
+ "--target-model-dir", type=str, default=None,
158
+ help=(
159
+ "path to the target model directory; required when converting a standalone draft model "
160
+ "(e.g. EAGLE3 / DFlash) that needs target-model metadata such as tokenizer, hidden size, and "
161
+ "layer count to populate its GGUF."
162
+ ),
163
+ )
164
+
165
+ args = parser.parse_args()
166
+ if not args.print_supported_models and args.model is None:
167
+ parser.error("the following arguments are required: model")
168
+ return args
169
+
170
+
171
+ def main() -> None:
172
+ args = parse_args()
173
+
174
+ if args.print_supported_models:
175
+ logger.error("Supported models:")
176
+ print_registered_models()
177
+ sys.exit(0)
178
+
179
+ if args.verbose:
180
+ logging.basicConfig(level=logging.DEBUG)
181
+ else:
182
+ logging.basicConfig(level=logging.INFO)
183
+
184
+ if args.remote:
185
+ hf_repo_id = args.model
186
+ from huggingface_hub import snapshot_download
187
+ allowed_patterns = ["LICENSE", "*.json", "*.md", "*.txt", "tokenizer.model"]
188
+ if args.sentence_transformers_dense_modules:
189
+ # include sentence-transformers dense modules safetensors files
190
+ allowed_patterns.append("*.safetensors")
191
+ local_dir = snapshot_download(
192
+ repo_id=hf_repo_id,
193
+ allow_patterns=allowed_patterns)
194
+ dir_model = Path(local_dir)
195
+ logger.info(f"Downloaded config and tokenizer to {local_dir}")
196
+ else:
197
+ hf_repo_id = None
198
+ dir_model = Path(args.model)
199
+
200
+ if not dir_model.is_dir():
201
+ logger.error(f'Error: {dir_model} is not a directory')
202
+ sys.exit(1)
203
+
204
+ ftype_map: dict[str, gguf.LlamaFileType] = {
205
+ "f32": gguf.LlamaFileType.ALL_F32,
206
+ "f16": gguf.LlamaFileType.MOSTLY_F16,
207
+ "bf16": gguf.LlamaFileType.MOSTLY_BF16,
208
+ "q8_0": gguf.LlamaFileType.MOSTLY_Q8_0,
209
+ "tq1_0": gguf.LlamaFileType.MOSTLY_TQ1_0,
210
+ "tq2_0": gguf.LlamaFileType.MOSTLY_TQ2_0,
211
+ "auto": gguf.LlamaFileType.GUESSED,
212
+ }
213
+
214
+ is_split = args.split_max_tensors > 0 or args.split_max_size != "0"
215
+ if args.use_temp_file and is_split:
216
+ logger.error("Error: Cannot use temp file when splitting")
217
+ sys.exit(1)
218
+
219
+ if args.outfile is not None:
220
+ fname_out = args.outfile
221
+ elif hf_repo_id:
222
+ # if remote, use the model ID as the output file name
223
+ fname_out = Path("./" + hf_repo_id.replace("/", "-") + "-{ftype}.gguf")
224
+ else:
225
+ fname_out = dir_model
226
+
227
+ logger.info(f"Loading model: {dir_model.name}")
228
+
229
+ is_mistral_format = args.mistral_format
230
+ if is_mistral_format and not _mistral_common_installed:
231
+ raise ImportError(_mistral_import_error_msg)
232
+ disable_mistral_community_chat_template = args.disable_mistral_community_chat_template
233
+
234
+ with torch.inference_mode():
235
+ output_type = ftype_map[args.outtype]
236
+ model_type = ModelType.MMPROJ if args.mmproj else ModelType.TEXT
237
+ hparams = ModelBase.load_hparams(dir_model, is_mistral_format)
238
+ if not is_mistral_format:
239
+ model_architecture = get_model_architecture(hparams, model_type)
240
+ logger.info(f"Model architecture: {model_architecture}")
241
+ try:
242
+ model_class = get_model_class(model_architecture, mmproj=(model_type == ModelType.MMPROJ))
243
+ except NotImplementedError:
244
+ logger.error(f"Model {model_architecture} is not supported")
245
+ sys.exit(1)
246
+ elif args.mmproj:
247
+ assert hparams.get("vision_encoder") is not None, "This model does not support multimodal"
248
+ from conversion.pixtral import PixtralModel
249
+ model_class = PixtralModel
250
+ elif hparams.get("moe") is not None:
251
+ from conversion.mistral import MistralMoeModel
252
+ model_class = MistralMoeModel
253
+ else:
254
+ from conversion.mistral import MistralModel
255
+ model_class = MistralModel
256
+
257
+ if args.mtp and args.no_mtp:
258
+ logger.error("--mtp and --no-mtp are mutually exclusive")
259
+ sys.exit(1)
260
+
261
+ if args.mtp or args.no_mtp:
262
+ from conversion.qwen import _Qwen35MtpMixin
263
+ from conversion.step3 import Step35Model
264
+ if not (issubclass(model_class, _Qwen35MtpMixin) or issubclass(model_class, Step35Model)):
265
+ logger.error("--mtp / --no-mtp are only supported for Qwen3.5/3.6 and Step3.5 text variants today")
266
+ sys.exit(1)
267
+ if args.no_mtp:
268
+ model_class.no_mtp = True
269
+ if args.mtp:
270
+ model_class.mtp_only = True
271
+
272
+ model_instance = model_class(dir_model, output_type, fname_out,
273
+ is_big_endian=args.bigendian, use_temp_file=args.use_temp_file,
274
+ eager=args.no_lazy,
275
+ metadata_override=args.metadata, model_name=args.model_name,
276
+ split_max_tensors=args.split_max_tensors,
277
+ split_max_size=split_str_to_n_bytes(args.split_max_size), dry_run=args.dry_run,
278
+ small_first_shard=args.no_tensor_first_split,
279
+ remote_hf_model_id=hf_repo_id, disable_mistral_community_chat_template=disable_mistral_community_chat_template,
280
+ sentence_transformers_dense_modules=args.sentence_transformers_dense_modules,
281
+ target_model_dir=Path(args.target_model_dir) if args.target_model_dir else None,
282
+ fuse_gate_up_exps=args.fuse_gate_up_exps,
283
+ fp8_as_q8=args.fp8_as_q8,
284
+ )
285
+
286
+ if args.vocab_only:
287
+ logger.info("Exporting model vocab...")
288
+ model_instance.write_vocab()
289
+ logger.info(f"Model vocab successfully exported to {model_instance.fname_out}")
290
+ else:
291
+ logger.info("Exporting model...")
292
+ model_instance.write()
293
+ out_path = f"{model_instance.fname_out.parent}{os.sep}" if is_split else model_instance.fname_out
294
+ logger.info(f"Model successfully exported to {out_path}")
295
+
296
+
297
+ if __name__ == '__main__':
298
+ main()
llama.cpp/convert_hf_to_gguf_update.py ADDED
@@ -0,0 +1,497 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import logging
5
+ import os
6
+ import pathlib
7
+ import re
8
+
9
+ import requests
10
+ import json
11
+ import shutil
12
+ import argparse
13
+
14
+ from hashlib import sha256
15
+ from enum import IntEnum, auto
16
+ from transformers import AutoTokenizer
17
+
18
+ logging.basicConfig(level=logging.DEBUG)
19
+ logger = logging.getLogger("convert_hf_to_gguf_update")
20
+ sess = requests.Session()
21
+
22
+ convert_py_pth = pathlib.Path("conversion/base.py")
23
+ convert_py = convert_py_pth.read_text(encoding="utf-8")
24
+ hf_token_pth = pathlib.Path.home() / ".cache" / "huggingface" / "token"
25
+ hf_token = hf_token_pth.read_text(encoding="utf-8").strip() if hf_token_pth.exists() else None
26
+
27
+
28
+ class TOKENIZER_TYPE(IntEnum):
29
+ SPM = auto()
30
+ BPE = auto()
31
+ WPM = auto()
32
+ UGM = auto()
33
+
34
+
35
+ DOC_STRING = """
36
+ This script downloads the tokenizer models of the specified models from Huggingface and
37
+ generates the get_vocab_base_pre() function for convert_hf_to_gguf.py
38
+
39
+ /!\\ It is intended to be used by contributors and is not meant to be run by end users
40
+
41
+ This is necessary in order to analyze the type of pre-tokenizer used by the model and
42
+ provide the necessary information to llama.cpp via the GGUF header in order to implement
43
+ the same pre-tokenizer.
44
+
45
+ ref: https://github.com/ggml-org/llama.cpp/pull/6920
46
+
47
+ Instructions:
48
+
49
+ - Add a new model to the "models" list
50
+ - Run the script with your huggingface token
51
+ By default, token will be read from ~/.cache/huggingface/token
52
+ - The convert_hf_to_gguf.py script will have had its get_vocab_base_pre() function updated
53
+ - Update llama.cpp with the new pre-tokenizer if necessary
54
+ """
55
+ # TODO: generate tokenizer tests for llama.cpp
56
+
57
+ parser = argparse.ArgumentParser(description=DOC_STRING, formatter_class=argparse.RawTextHelpFormatter)
58
+ parser.add_argument(
59
+ "--full", action="store_true",
60
+ help="download full list of models - make sure you have access to all of them",
61
+ )
62
+ parser.add_argument(
63
+ "--check-missing", action="store_true",
64
+ help="only check for missing pre-tokenizer hashes",
65
+ )
66
+ parser.add_argument(
67
+ "hf_token",
68
+ help="optional HF token",
69
+ nargs="?",
70
+ )
71
+ args = parser.parse_args()
72
+ hf_token = args.hf_token if args.hf_token is not None else hf_token
73
+
74
+ if hf_token is None:
75
+ logger.warning("HF token not found. You can provide it as an argument or set it in ~/.cache/huggingface/token")
76
+
77
+ if args.check_missing and args.full:
78
+ logger.warning("Downloading full list of models requested, ignoring --check-missing!")
79
+ args.check_missing = False
80
+
81
+ # TODO: this string has to exercise as much pre-tokenizer functionality as possible
82
+ # will be updated with time - contributions welcome
83
+ CHK_TXT = '\n \n\n \n\n\n \t \t\t \t\n \n \n \n \n🚀 (normal) 😶‍🌫️ (multiple emojis concatenated) ✅ 🦙🦙 3 33 333 3333 33333 333333 3333333 33333333 3.3 3..3 3...3 កាន់តែពិសេសអាច😁 ?我想在apple工作1314151天~ ------======= нещо на Български \'\'\'\'\'\'```````\"\"\"\"......!!!!!!?????? I\'ve been \'told he\'s there, \'RE you sure? \'M not sure I\'ll make it, \'D you like some tea? We\'Ve a\'lL'
84
+
85
+ # TODO: add models here, base models preferred
86
+ models = [
87
+ {"name": "llama-spm", "tokt": TOKENIZER_TYPE.SPM, "repo": "https://huggingface.co/meta-llama/Llama-2-7b-hf", },
88
+ {"name": "llama-bpe", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/meta-llama/Meta-Llama-3-8B", },
89
+ {"name": "phi-3", "tokt": TOKENIZER_TYPE.SPM, "repo": "https://huggingface.co/microsoft/Phi-3-mini-4k-instruct", },
90
+ {"name": "deepseek-llm", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/deepseek-ai/deepseek-llm-7b-base", },
91
+ {"name": "deepseek-coder", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/deepseek-ai/deepseek-coder-6.7b-base", },
92
+ {"name": "falcon", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/tiiuae/falcon-7b", },
93
+ {"name": "bert-bge", "tokt": TOKENIZER_TYPE.WPM, "repo": "https://huggingface.co/BAAI/bge-small-en-v1.5", },
94
+ {"name": "falcon3", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/tiiuae/Falcon3-7B-Base", },
95
+ {"name": "bert-bge-large", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/BAAI/bge-large-zh-v1.5", },
96
+ {"name": "mpt", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/mosaicml/mpt-7b", },
97
+ {"name": "starcoder", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/bigcode/starcoder2-3b", },
98
+ {"name": "gpt-2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/openai-community/gpt2", },
99
+ {"name": "stablelm2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/stabilityai/stablelm-2-zephyr-1_6b", },
100
+ {"name": "refact", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/smallcloudai/Refact-1_6-base", },
101
+ {"name": "command-r", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/CohereForAI/c4ai-command-r-v01", },
102
+ {"name": "tiny_aya", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/CohereLabs/tiny-aya-base", },
103
+ {"name": "cohere2moe", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/CohereLabs/North-Mini-Code-1.0", },
104
+ {"name": "qwen2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/Qwen/Qwen1.5-7B", },
105
+ {"name": "olmo", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/allenai/OLMo-1.7-7B-hf", },
106
+ {"name": "dbrx", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/databricks/dbrx-base", },
107
+ {"name": "jina-v1-en", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/jinaai/jina-reranker-v1-tiny-en", },
108
+ {"name": "jina-v2-en", "tokt": TOKENIZER_TYPE.WPM, "repo": "https://huggingface.co/jinaai/jina-embeddings-v2-base-en", }, # WPM!
109
+ {"name": "jina-v2-es", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/jinaai/jina-embeddings-v2-base-es", },
110
+ {"name": "jina-v2-de", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/jinaai/jina-embeddings-v2-base-de", },
111
+ {"name": "jina-v5-nano", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/jinaai/jina-embeddings-v5-text-nano", },
112
+ {"name": "smaug-bpe", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/abacusai/Smaug-Llama-3-70B-Instruct", },
113
+ {"name": "poro-chat", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/LumiOpen/Poro-34B-chat", },
114
+ {"name": "jina-v2-code", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/jinaai/jina-embeddings-v2-base-code", },
115
+ {"name": "viking", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/LumiOpen/Viking-7B", }, # Also used for Viking 13B and 33B
116
+ {"name": "gemma", "tokt": TOKENIZER_TYPE.SPM, "repo": "https://huggingface.co/google/gemma-2b", },
117
+ {"name": "gemma-2", "tokt": TOKENIZER_TYPE.SPM, "repo": "https://huggingface.co/google/gemma-2-9b", },
118
+ {"name": "jais", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/core42/jais-13b", },
119
+ {"name": "jais-2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/inceptionai/Jais-2-8B-Chat", },
120
+ {"name": "t5", "tokt": TOKENIZER_TYPE.UGM, "repo": "https://huggingface.co/google-t5/t5-small", },
121
+ {"name": "codeshell", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/WisdomShell/CodeShell-7B", },
122
+ {"name": "tekken", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/mistralai/Mistral-Nemo-Base-2407", },
123
+ {"name": "smollm", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/HuggingFaceTB/SmolLM-135M", },
124
+ {'name': "bloom", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/bigscience/bloom", },
125
+ {'name': "gpt3-finnish", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/TurkuNLP/gpt3-finnish-small", },
126
+ {"name": "exaone", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/LGAI-EXAONE/EXAONE-3.0-7.8B-Instruct", },
127
+ {"name": "phi-2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/microsoft/phi-2", },
128
+ {"name": "chameleon", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/facebook/chameleon-7b", },
129
+ {"name": "roberta-bpe", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/sentence-transformers/stsb-roberta-base"},
130
+ {"name": "gigachat", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/ai-sage/GigaChat-20B-A3B-instruct"},
131
+ {"name": "megrez", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/Infinigence/Megrez-3B-Instruct"},
132
+ {"name": "deepseek-v3", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/deepseek-ai/DeepSeek-V3"},
133
+ {"name": "deepseek-r1-qwen", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B"},
134
+ {"name": "gpt-4o", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/Xenova/gpt-4o", },
135
+ {"name": "superbpe", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/UW/OLMo2-8B-SuperBPE-t180k", },
136
+ {"name": "trillion", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/trillionlabs/Trillion-7B-preview", },
137
+ {"name": "bailingmoe", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/inclusionAI/Ling-lite", },
138
+ {"name": "llama4", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/meta-llama/Llama-4-Scout-17B-16E-Instruct", },
139
+ {"name": "pixtral", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/mistral-community/pixtral-12b", },
140
+ {"name": "seed-coder", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/ByteDance-Seed/Seed-Coder-8B-Base", },
141
+ {"name": "a.x-4.0", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/skt/A.X-4.0", },
142
+ {"name": "midm-2.0", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/K-intelligence/Midm-2.0-Base-Instruct", },
143
+ {"name": "lfm2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/LiquidAI/LFM2.5-350M", },
144
+ {"name": "exaone4", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/LGAI-EXAONE/EXAONE-4.0-32B", },
145
+ {"name": "mellum", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/JetBrains/Mellum-4b-base", },
146
+ {"name": "modern-bert", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/answerdotai/ModernBERT-base", },
147
+ {"name": "afmoe", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/arcee-ai/Trinity-Tokenizer", },
148
+ {"name": "bailingmoe2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/inclusionAI/Ling-mini-base-2.0", },
149
+ {"name": "granite-docling", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/ibm-granite/granite-docling-258M", },
150
+ {"name": "minimax-m2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/MiniMaxAI/MiniMax-M2", },
151
+ {"name": "kormo", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/KORMo-Team/KORMo-tokenizer", },
152
+ {"name": "youtu", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/tencent/Youtu-LLM-2B", },
153
+ {"name": "solar-open", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/upstage/Solar-Open-100B", },
154
+ {"name": "exaone-moe", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/LGAI-EXAONE/K-EXAONE-236B-A23B", },
155
+ {"name": "qwen35", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/Qwen/Qwen3.5-9B-Instruct", },
156
+ {"name": "joyai-llm", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/jdopensource/JoyAI-LLM-Flash", },
157
+ {"name": "kanana2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/kakaocorp/kanana-2-30b-a3b-instruct-2601", },
158
+ {"name": "f2llmv2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/codefuse-ai/F2LLM-v2-4B", },
159
+ {"name": "sarvam-moe", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/sarvamai/sarvam-30b", },
160
+ {"name": "talkie", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/lewtun/talkie-1930-13b-it-hf", },
161
+ {"name": "minicpm5", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/openbmb/MiniCPM5-1B"},
162
+ {"name": "granite-embed-multi-97m", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/ibm-granite/granite-embedding-97m-multilingual-r2", },
163
+ {"name": "granite-embed-multi-311m", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/ibm-granite/granite-embedding-311m-multilingual-r2", },
164
+ {"name": "mellum2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/JetBrains/Mellum2-12B-A2.5B-Base"},
165
+ ]
166
+
167
+ # some models are known to be broken upstream, so we will skip them as exceptions
168
+ pre_computed_hashes = [
169
+ # chatglm-bpe has 2 hashes, why?
170
+ {"name": "chatglm-bpe", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/THUDM/glm-4-9b-chat", "chkhsh": "b6e8e1518dc4305be2fe39c313ed643381c4da5db34a98f6a04c093f8afbe99b"},
171
+ {"name": "chatglm-bpe", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/THUDM/glm-4-9b-chat", "chkhsh": "81d72c7348a9f0ebe86f23298d37debe0a5e71149e29bd283904c02262b27516"},
172
+ {"name": "glm4", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/THUDM/glm-4-9b-hf", "chkhsh": "a1336059768a55c99a734006ffb02203cd450fed003e9a71886c88acf24fdbc2"},
173
+ {"name": "glm4", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/zai-org/GLM-4.5-Air", "chkhsh": "9ca2dd618e8afaf09731a7cf6e2105b373ba6a1821559f258b272fe83e6eb902"},
174
+ {"name": "glm4", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/zai-org/GLM-4.7-Flash", "chkhsh": "cdf5f35325780597efd76153d4d1c16778f766173908894c04afc20108536267"},
175
+ {"name": "minerva-7b", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/sapienzanlp/Minerva-7B-base-v1.0", "chkhsh": "1431a23e583c97432bc230bff598d103ddb5a1f89960c8f1d1051aaa944d0b35"},
176
+ {"name": "hunyuan", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/tencent/Hunyuan-A13B-Instruct", "chkhsh": "7e57df22b1fe23a7b1e1c7f3dc4e3f96d43a4eb0836d0c6bdc3436d7b2f1c664"},
177
+ {"name": "hunyuan-dense", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/tencent/Hunyuan-4B-Instruct", "chkhsh": "bba3b3366b646dbdded5dbc42d59598b849371afc42f7beafa914afaa5b70aa6"},
178
+ # falcon-h1 series uses 4 different tokenizers across model sizes (0.5b - 34b), hence we need to define 4 different hashes
179
+ {"name": "falcon-h1", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/tiiuae/Falcon-H1-0.5B-Base", "chkhsh": "a6b57017d60e6edb4d88ecc2845188e0eb333a70357e45dcc9b53964a73bbae6"},
180
+ {"name": "falcon-h1", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/tiiuae/Falcon-H1-1B-Base", "chkhsh": "60476e1243776c4fb1b993dbd7a5f15ac22f83c80afdf425fa5ae01c8d44ef86"},
181
+ {"name": "falcon-h1", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/tiiuae/Falcon-H1-7B-Base", "chkhsh": "3eda48b4c4dc7de733d1a8b3e3b4a85243dbbf704da2ee9d42c6beced8897896"},
182
+ {"name": "falcon-h1", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/tiiuae/Falcon-H1-34B-Base", "chkhsh": "48f8e02c0359c0bbdd82f26909171fac1c18a457bb47573ed1fe3bbb2c1cfd4b"},
183
+ {"name": "kimi-k2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/moonshotai/Kimi-K2-Base", "chkhsh": "81212dc7cdb7e0c1074ca62c5aeab0d43c9f52b8a737be7b12a777c953027890"},
184
+ {"name": "qwen2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/Qwen/Qwen3-Embedding-0.6B", "chkhsh": "d4540891389ea895b53b399da6ac824becc30f2fba0e9ddbb98f92e55ca0e97c"},
185
+ {"name": "qwen35", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/openbmb/MiniCPM-V-4_6", "chkhsh": "1444df51289cfa8063b96f0e62b1125440111bc79a52003ea14b6eac7016fd5f"},
186
+ {"name": "grok-2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/alvarobartt/grok-2-tokenizer", "chkhsh": "66b8d4e19ab16c3bfd89bce5d785fb7e0155e8648708a1f42077cb9fe002c273"},
187
+ # jina-v2-de variants
188
+ {"name": "jina-v2-de", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/aari1995/German_Semantic_V3", "chkhsh": "b3d1dd861f1d4c5c0d2569ce36baf3f90fe8a102db3de50dd71ff860d91be3df"},
189
+ {"name": "gpt-2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/evilfreelancer/ruGPT3XL", "chkhsh": "0fe1cf6eda062318a1af7270f3331a85c539a01778ff948e24388e949c5282f4"},
190
+ # lfm2 variants
191
+ {"name": "lfm2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/LiquidAI/LFM2.5-8B-A1B", "chkhsh": "9e454714343b69b99b71795c1d27a68c2a1d15dab111f4d353109f966af29da7"},
192
+ ]
193
+
194
+
195
+ def download_file_with_auth(url, token, save_path):
196
+ headers = {"Authorization": f"Bearer {token}"} if token else None
197
+ response = sess.get(url, headers=headers)
198
+ response.raise_for_status()
199
+ os.makedirs(os.path.dirname(save_path), exist_ok=True)
200
+ with open(save_path, 'wb') as downloaded_file:
201
+ downloaded_file.write(response.content)
202
+ logger.info(f"File {save_path} downloaded successfully")
203
+
204
+
205
+ def download_model(model):
206
+ name = model["name"]
207
+ repo = model["repo"]
208
+ tokt = model["tokt"]
209
+
210
+ os.makedirs(f"models/tokenizers/{name}", exist_ok=True)
211
+
212
+ files = ["config.json", "tokenizer.json", "tokenizer_config.json"]
213
+
214
+ if name == "gpt-4o":
215
+ # Xenova/gpt-4o is tokenizer-only, it does not contain config.json
216
+ files = ["tokenizer.json", "tokenizer_config.json"]
217
+
218
+ if tokt == TOKENIZER_TYPE.SPM:
219
+ files.append("tokenizer.model")
220
+
221
+ if tokt == TOKENIZER_TYPE.UGM:
222
+ files.append("spiece.model")
223
+
224
+ if os.path.isdir(repo):
225
+ # If repo is a path on the file system, copy the directory
226
+ for file in files:
227
+ src_path = os.path.join(repo, file)
228
+ dst_path = f"models/tokenizers/{name}/{file}"
229
+ if os.path.isfile(dst_path):
230
+ logger.info(f"{name}: File {dst_path} already exists - skipping")
231
+ continue
232
+ if os.path.isfile(src_path):
233
+ shutil.copy2(src_path, dst_path)
234
+ logger.info(f"{name}: Copied {src_path} to {dst_path}")
235
+ else:
236
+ logger.warning(f"{name}: Source file {src_path} does not exist")
237
+ else:
238
+ # If repo is a URL, download the files
239
+ for file in files:
240
+ save_path = f"models/tokenizers/{name}/{file}"
241
+ if os.path.isfile(save_path):
242
+ logger.info(f"{name}: File {save_path} already exists - skipping")
243
+ continue
244
+ download_file_with_auth(f"{repo}/resolve/main/{file}", hf_token, save_path)
245
+
246
+
247
+ # get list of existing models and chkhsh from the convert_hf_to_gguf.py file
248
+ # returns mapping res --> chkhsh
249
+ def get_existing_models(convert_py):
250
+ pattern = r'if chkhsh == "([a-f0-9]{64})":\s*\n\s*.*\s*res = "([^"]+)"'
251
+ matches = re.findall(pattern, convert_py)
252
+ output = {}
253
+ for chkhsh, res in matches:
254
+ output[res] = chkhsh
255
+ return output
256
+
257
+
258
+ existing_models = {}
259
+ all_models = models.copy()
260
+ if not args.full:
261
+ # Filter out models that already exist in convert_hf_to_gguf.py
262
+ existing_models = get_existing_models(convert_py)
263
+ all_models = models.copy()
264
+ models = [model for model in all_models if model["name"] not in existing_models]
265
+
266
+ if not args.check_missing:
267
+ logging.info(f"Downloading {len(models)} models...")
268
+ for model in models:
269
+ try:
270
+ download_model(model)
271
+ except Exception as e:
272
+ logger.error(f"Failed to download model {model['name']}. Error: {e}")
273
+
274
+
275
+ # generate the source code for the convert_hf_to_gguf.py:get_vocab_base_pre() function:
276
+
277
+ src_ifs = ""
278
+ for model in [*pre_computed_hashes, *all_models]:
279
+ name = model["name"]
280
+ tokt = model["tokt"]
281
+ chkhsh = model.get("chkhsh")
282
+
283
+ if tokt == TOKENIZER_TYPE.SPM or tokt == TOKENIZER_TYPE.UGM:
284
+ continue
285
+
286
+ # create the tokenizer
287
+ if chkhsh is not None:
288
+ # if the model has a pre-computed hash, use it
289
+ logger.info(f"Using pre-computed hash for model {name}: {chkhsh}")
290
+ elif name in existing_models:
291
+ # if the model already exists in convert_hf_to_gguf.py, skip compute hash
292
+ chkhsh = existing_models[name]
293
+ else:
294
+ # otherwise, compute the hash of the tokenizer
295
+
296
+ # Fail if the tokenizer folder with config does not exist or there are other download issues previously
297
+ if not os.path.isfile(f"models/tokenizers/{name}/tokenizer_config.json"):
298
+ raise OSError(f"Config for tokenizer {name} not found. The model may not exist or is not accessible with the provided token.")
299
+
300
+ try:
301
+ logger.info(f"Loading tokenizer from {f'models/tokenizers/{name}'}...")
302
+ if name == "t5":
303
+ tokenizer = AutoTokenizer.from_pretrained(f"models/tokenizers/{name}", use_fast=False)
304
+ else:
305
+ tokenizer = AutoTokenizer.from_pretrained(f"models/tokenizers/{name}")
306
+ except Exception as e:
307
+ raise OSError(f"Error loading tokenizer for model {name}.") from e
308
+
309
+ chktok = tokenizer.encode(CHK_TXT) # ty: ignore[unresolved-attribute]
310
+ chkhsh = sha256(str(chktok).encode()).hexdigest()
311
+
312
+ logger.info(f"model: {name}")
313
+ logger.info(f"tokt: {tokt}")
314
+ logger.info(f"repo: {model['repo']}")
315
+ logger.info(f"chktok: {chktok}")
316
+ logger.info(f"chkhsh: {chkhsh}")
317
+
318
+ # print the "pre_tokenizer" content from the tokenizer.json
319
+ with open(f"models/tokenizers/{name}/tokenizer.json", "r", encoding="utf-8") as f:
320
+ cfg = json.load(f)
321
+ normalizer = cfg["normalizer"]
322
+ logger.info("normalizer: " + json.dumps(normalizer, indent=4))
323
+ pre_tokenizer = cfg["pre_tokenizer"]
324
+ logger.info("pre_tokenizer: " + json.dumps(pre_tokenizer, indent=4))
325
+ if "ignore_merges" in cfg["model"]:
326
+ logger.info("ignore_merges: " + json.dumps(cfg["model"]["ignore_merges"], indent=4))
327
+
328
+ logger.info("")
329
+
330
+ src_ifs += f" if chkhsh == \"{chkhsh}\":\n"
331
+ src_ifs += f" # ref: {model['repo']}\n"
332
+ src_ifs += f" res = \"{name}\"\n"
333
+
334
+ src_func = f"""
335
+ def get_vocab_base_pre(self, tokenizer) -> str:
336
+ # encoding this string and hashing the resulting tokens would (hopefully) give us a unique identifier that
337
+ # is specific for the BPE pre-tokenizer used by the model
338
+ # we will use this unique identifier to write a "tokenizer.ggml.pre" entry in the GGUF file which we can
339
+ # use in llama.cpp to implement the same pre-tokenizer
340
+
341
+ chktxt = {repr(CHK_TXT)}
342
+
343
+ chktok = tokenizer.encode(chktxt)
344
+ chkhsh = sha256(str(chktok).encode()).hexdigest()
345
+
346
+ logger.debug(f"chktok: {{chktok}}")
347
+ logger.debug(f"chkhsh: {{chkhsh}}")
348
+
349
+ res = None
350
+
351
+ # NOTE: if you get an error here, you need to update the convert_hf_to_gguf_update.py script
352
+ # or pull the latest version of the model from Huggingface
353
+ # don't edit the hashes manually!
354
+ {src_ifs}
355
+ if res is None:
356
+ logger.warning("\\n")
357
+ logger.warning("**************************************************************************************")
358
+ logger.warning("** WARNING: The BPE pre-tokenizer was not recognized!")
359
+ logger.warning("** There are 2 possible reasons for this:")
360
+ logger.warning("** - the model has not been added to convert_hf_to_gguf_update.py yet")
361
+ logger.warning("** - the pre-tokenization config has changed upstream")
362
+ logger.warning("** Check your model files and convert_hf_to_gguf_update.py and update them accordingly.")
363
+ logger.warning("** ref: https://github.com/ggml-org/llama.cpp/pull/6920")
364
+ logger.warning("**")
365
+ logger.warning(f"** chkhsh: {{chkhsh}}")
366
+ logger.warning("**************************************************************************************")
367
+ logger.warning("\\n")
368
+ raise NotImplementedError("BPE pre-tokenizer was not recognized - update get_vocab_base_pre()")
369
+
370
+ logger.debug(f"tokenizer.ggml.pre: {{repr(res)}}")
371
+ logger.debug(f"chkhsh: {{chkhsh}}")
372
+
373
+ return res
374
+ """
375
+
376
+ convert_py = re.sub(
377
+ r"(# Marker: Start get_vocab_base_pre)(.+?)( +# Marker: End get_vocab_base_pre)",
378
+ lambda m: m.group(1) + src_func + m.group(3),
379
+ convert_py,
380
+ flags=re.DOTALL | re.MULTILINE,
381
+ )
382
+
383
+ convert_py_pth.write_text(convert_py, encoding="utf-8")
384
+
385
+ logger.info(f"+++ {convert_py_pth} was updated")
386
+
387
+ # generate tests for each tokenizer model
388
+
389
+ tests = [
390
+ "ied 4 ½ months",
391
+ "Äpfel",
392
+ "",
393
+ " ",
394
+ " ",
395
+ " ",
396
+ "\t",
397
+ "\n",
398
+ "\n\n",
399
+ "\n\n\n",
400
+ "\t\n",
401
+ "Hello world",
402
+ " Hello world",
403
+ "Hello World",
404
+ " Hello World",
405
+ " Hello World!",
406
+ "Hello, world!",
407
+ " Hello, world!",
408
+ " this is 🦙.cpp",
409
+ "w048 7tuijk dsdfhu",
410
+ "нещо на Български",
411
+ "កាន់តែពិសេសអាចខលចេញ",
412
+ "🚀 (normal) 😶‍🌫️ (multiple emojis concatenated) ✅ (only emoji that has its own token)",
413
+ "Hello",
414
+ " Hello",
415
+ " Hello",
416
+ " Hello",
417
+ " Hello",
418
+ " Hello\n Hello",
419
+ " (",
420
+ "\n =",
421
+ "' era",
422
+ "Hello, y'all! How are you 😁 ?我想在apple工作1314151天~",
423
+ "!!!!!!",
424
+ "3",
425
+ "33",
426
+ "333",
427
+ "3333",
428
+ "33333",
429
+ "333333",
430
+ "3333333",
431
+ "33333333",
432
+ "333333333",
433
+ "Cửa Việt", # llama-bpe fails on this
434
+ " discards",
435
+ CHK_TXT,
436
+ ]
437
+
438
+ # write the tests to ./models/ggml-vocab-{name}.gguf.inp
439
+ # the format is:
440
+ #
441
+ # test0
442
+ # __ggml_vocab_test__
443
+ # test1
444
+ # __ggml_vocab_test__
445
+ # ...
446
+ #
447
+
448
+ # with each model, encode all tests and write the results in ./models/ggml-vocab-{name}.gguf.out
449
+ # for each test, write the resulting tokens on a separate line
450
+
451
+ for model in models:
452
+ name = model["name"]
453
+ tokt = model["tokt"]
454
+
455
+ # Skip if the tokenizer folder does not exist or there are other download issues previously
456
+ if not os.path.exists(f"models/tokenizers/{name}"):
457
+ logger.warning(f"Directory for tokenizer {name} not found. Skipping...")
458
+ continue
459
+
460
+ # create the tokenizer
461
+ try:
462
+ if name == "t5":
463
+ tokenizer = AutoTokenizer.from_pretrained(f"models/tokenizers/{name}", use_fast=False)
464
+ else:
465
+ tokenizer = AutoTokenizer.from_pretrained(f"models/tokenizers/{name}")
466
+ except (OSError, TypeError) as e:
467
+ logger.error(f"Failed to load tokenizer for model {name}. Error: {e}")
468
+ continue # Skip this model and continue with the next one in the loop
469
+
470
+ if not os.path.exists(f"models/ggml-vocab-{name}.gguf"):
471
+ logger.info(f"Skip vocab files for model {name}, no GGUF file found")
472
+ continue
473
+
474
+ with open(f"models/ggml-vocab-{name}.gguf.inp", "w", encoding="utf-8") as f:
475
+ for text in tests:
476
+ f.write(f"{text}")
477
+ f.write("\n__ggml_vocab_test__\n")
478
+
479
+ with open(f"models/ggml-vocab-{name}.gguf.out", "w") as f:
480
+ for text in tests:
481
+ res = tokenizer.encode(text, add_special_tokens=False) # ty: ignore[unresolved-attribute]
482
+ for r in res:
483
+ f.write(f" {r}")
484
+ f.write("\n")
485
+
486
+ logger.info(f"Tests for {name} written in ./models/ggml-vocab-{name}.gguf.*")
487
+
488
+ # generate commands for creating vocab files
489
+
490
+ logger.info("\nRun the following commands to generate the vocab files for testing:\n")
491
+
492
+ for model in models:
493
+ name = model["name"]
494
+
495
+ print(f"python3 convert_hf_to_gguf.py models/tokenizers/{name}/ --outfile models/ggml-vocab-{name}.gguf --vocab-only") # noqa: NP100
496
+
497
+ logger.info("\n")
llama.cpp/convert_llama_ggml_to_gguf.py ADDED
@@ -0,0 +1,450 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ import argparse
6
+ import os
7
+ import struct
8
+ import sys
9
+ from enum import IntEnum
10
+ from pathlib import Path
11
+
12
+ import numpy as np
13
+
14
+ if 'NO_LOCAL_GGUF' not in os.environ:
15
+ sys.path.insert(1, str(Path(__file__).parent / 'gguf-py'))
16
+ import gguf
17
+
18
+ logger = logging.getLogger("ggml-to-gguf")
19
+
20
+
21
+ class GGMLFormat(IntEnum):
22
+ GGML = 0
23
+ GGMF = 1
24
+ GGJT = 2
25
+
26
+
27
+ class GGMLFType(IntEnum):
28
+ ALL_F32 = 0
29
+ MOSTLY_F16 = 1
30
+ MOSTLY_Q4_0 = 2
31
+ MOSTLY_Q4_1 = 3
32
+ MOSTLY_Q4_1_SOME_F16 = 4
33
+ MOSTLY_Q8_0 = 7
34
+ MOSTLY_Q5_0 = 8
35
+ MOSTLY_Q5_1 = 9
36
+ MOSTLY_Q2_K = 10
37
+ MOSTLY_Q3_K_S = 11
38
+ MOSTLY_Q3_K_M = 12
39
+ MOSTLY_Q3_K_L = 13
40
+ MOSTLY_Q4_K_S = 14
41
+ MOSTLY_Q4_K_M = 15
42
+ MOSTLY_Q5_K_S = 16
43
+ MOSTLY_Q5_K_M = 17
44
+ MOSTLY_Q6_K = 18
45
+
46
+
47
+ class Hyperparameters:
48
+ def __init__(self):
49
+ self.n_vocab = self.n_embd = self.n_mult = self.n_head = 0
50
+ self.n_layer = self.n_rot = self.n_ff = 0
51
+ self.ftype = GGMLFType.ALL_F32
52
+
53
+ def set_n_ff(self, model):
54
+ ff_tensor_idx = model.tensor_map.get(b'layers.0.feed_forward.w1.weight')
55
+ assert ff_tensor_idx is not None, 'Missing layer 0 FF tensor'
56
+ ff_tensor = model.tensors[ff_tensor_idx]
57
+ self.n_ff = ff_tensor.dims[1]
58
+
59
+ def load(self, data, offset):
60
+ (
61
+ self.n_vocab,
62
+ self.n_embd,
63
+ self.n_mult,
64
+ self.n_head,
65
+ self.n_layer,
66
+ self.n_rot,
67
+ ftype,
68
+ ) = struct.unpack('<7I', data[offset:offset + (4 * 7)])
69
+ try:
70
+ self.ftype = GGMLFType(ftype)
71
+ except ValueError:
72
+ raise ValueError(f'Invalid ftype {ftype}')
73
+ return 4 * 7
74
+
75
+ def __str__(self):
76
+ return f'<Hyperparameters: n_vocab={self.n_vocab}, n_embd={self.n_embd}, n_mult={self.n_mult}, n_head={self.n_head}, n_layer={self.n_layer}, n_rot={self.n_rot}, n_ff={self.n_ff}, ftype={self.ftype.name}>'
77
+
78
+
79
+ class Vocab:
80
+ def __init__(self, load_scores = True):
81
+ self.items = []
82
+ self.load_scores = load_scores
83
+
84
+ def load(self, data, offset, n_vocab):
85
+ orig_offset = offset
86
+ for _ in range(n_vocab):
87
+ itemlen = struct.unpack('<I', data[offset:offset + 4])[0]
88
+ assert itemlen < 4096, 'Absurd vocab item length'
89
+ offset += 4
90
+ item_text = bytes(data[offset:offset + itemlen])
91
+ offset += itemlen
92
+ if self.load_scores:
93
+ item_score = struct.unpack('<f', data[offset:offset + 4])[0]
94
+ offset += 4
95
+ else:
96
+ item_score = 0.0
97
+ self.items.append((item_text, item_score))
98
+ return offset - orig_offset
99
+
100
+
101
+ class Tensor:
102
+ def __init__(self, use_padding = True):
103
+ self.name = None
104
+ self.dims: tuple[int, ...] = ()
105
+ self.dtype = None
106
+ self.start_offset = 0
107
+ self.len_bytes = np.int64(0)
108
+ self.use_padding = use_padding
109
+
110
+ def load(self, data, offset):
111
+ orig_offset = offset
112
+ (n_dims, name_len, dtype) = struct.unpack('<3I', data[offset:offset + 12])
113
+ assert n_dims >= 0 and n_dims <= 4, f'Invalid tensor dimensions {n_dims}'
114
+ assert name_len < 4096, 'Absurd tensor name length'
115
+ self.dtype = gguf.GGMLQuantizationType(dtype)
116
+ quant = gguf.GGML_QUANT_SIZES.get(self.dtype)
117
+ assert quant is not None, 'Unknown tensor type'
118
+ (blksize, tysize) = quant
119
+ offset += 12
120
+ self.dims = struct.unpack(f'<{n_dims}I', data[offset:offset + (4 * n_dims)])
121
+ offset += 4 * n_dims
122
+ self.name = bytes(data[offset:offset + name_len])
123
+ offset += name_len
124
+ pad = ((offset + 31) & ~31) - offset if self.use_padding else 0
125
+ offset += pad
126
+ n_elems = np.prod(self.dims)
127
+ n_bytes = np.int64(np.int64(n_elems) * np.int64(tysize)) // np.int64(blksize)
128
+ self.start_offset = offset
129
+ self.len_bytes = n_bytes
130
+ offset += n_bytes
131
+ return offset - orig_offset
132
+
133
+
134
+ class GGMLModel:
135
+
136
+ file_format: GGMLFormat
137
+ format_version: int
138
+
139
+ def __init__(self):
140
+ self.hyperparameters = None
141
+ self.vocab = None
142
+ self.tensor_map = {}
143
+ self.tensors = []
144
+
145
+ def validate_header(self, data, offset):
146
+ magic = bytes(data[offset:offset + 4])
147
+ if magic == b'GGUF':
148
+ raise ValueError('File is already in GGUF format.')
149
+ if magic == b'lmgg':
150
+ self.file_format = GGMLFormat.GGML
151
+ self.format_version = 1
152
+ return 4
153
+ version = struct.unpack('<I', data[offset + 4:offset + 8])[0]
154
+ if magic == b'fmgg':
155
+ if version != 1:
156
+ raise ValueError(f'Cannot handle unexpected GGMF file version {version}')
157
+ self.file_format = GGMLFormat.GGMF
158
+ self.format_version = version
159
+ return 8
160
+ if magic == b'tjgg':
161
+ if version < 1 or version > 3:
162
+ raise ValueError(f'Cannot handle unexpected GGJT file version {version}')
163
+ self.file_format = GGMLFormat.GGJT
164
+ self.format_version = version
165
+ return 8
166
+ raise ValueError(f"Unexpected file magic {magic!r}! This doesn't look like a GGML format file.")
167
+
168
+ def validate_conversion(self, ftype):
169
+ err = ''
170
+ if (self.file_format < GGMLFormat.GGJT or self.format_version < 2):
171
+ if ftype not in (GGMLFType.ALL_F32, GGMLFType.MOSTLY_F16):
172
+ err = 'Quantizations changed in GGJTv2. Can only convert unquantized GGML files older than GGJTv2.'
173
+ elif (self.file_format == GGMLFormat.GGJT and self.format_version == 2):
174
+ if ftype in (GGMLFType.MOSTLY_Q4_0, GGMLFType.MOSTLY_Q4_1,
175
+ GGMLFType.MOSTLY_Q4_1_SOME_F16, GGMLFType.MOSTLY_Q8_0):
176
+ err = 'Q4 and Q8 quantizations changed in GGJTv3.'
177
+ if len(err) > 0:
178
+ raise ValueError(f'{err} Sorry, your {self.file_format.name}v{self.format_version} file of type {ftype.name} is not eligible for conversion.')
179
+
180
+ def load(self, data, offset):
181
+ offset += self.validate_header(data, offset)
182
+ hp = Hyperparameters()
183
+ offset += hp.load(data, offset)
184
+ logger.info(f'* File format: {self.file_format.name}v{self.format_version} with ftype {hp.ftype.name}')
185
+ self.validate_conversion(hp.ftype)
186
+ vocab = Vocab(load_scores = self.file_format > GGMLFormat.GGML)
187
+ offset += vocab.load(data, offset, hp.n_vocab)
188
+ tensors: list[Tensor] = []
189
+ tensor_map = {}
190
+ while offset < len(data):
191
+ tensor = Tensor(use_padding = self.file_format > GGMLFormat.GGMF)
192
+ offset += tensor.load(data, offset)
193
+ tensor_map[tensor.name] = len(tensors)
194
+ tensors.append(tensor)
195
+ self.hyperparameters = hp
196
+ self.vocab = vocab
197
+ self.tensors = tensors
198
+ self.tensor_map = tensor_map
199
+ hp.set_n_ff(self)
200
+ return offset
201
+
202
+
203
+ class GGMLToGGUF:
204
+ def __init__(self, ggml_model, data, cfg, params_override = None, vocab_override = None, special_vocab = None):
205
+ hp = ggml_model.hyperparameters
206
+ self.model = ggml_model
207
+ self.data = data
208
+ self.cfg = cfg
209
+ self.params_override = params_override
210
+ self.vocab_override = vocab_override
211
+ self.special_vocab = special_vocab
212
+ if params_override is not None:
213
+ n_kv_head = params_override.n_head_kv
214
+ else:
215
+ if cfg.gqa == 1:
216
+ n_kv_head = hp.n_head
217
+ else:
218
+ gqa = float(cfg.gqa)
219
+ n_kv_head = None
220
+ for x in range(1, 256):
221
+ if float(hp.n_head) / float(x) == gqa:
222
+ n_kv_head = x
223
+ assert n_kv_head is not None, "Couldn't determine n_kv_head from GQA param"
224
+ logger.info(f'- Guessed n_kv_head = {n_kv_head} based on GQA {cfg.gqa}')
225
+ self.n_kv_head = n_kv_head
226
+ self.name_map = gguf.get_tensor_name_map(gguf.MODEL_ARCH.LLAMA, ggml_model.hyperparameters.n_layer)
227
+
228
+ def save(self):
229
+ logger.info('* Preparing to save GGUF file')
230
+ gguf_writer = gguf.GGUFWriter(
231
+ self.cfg.output,
232
+ gguf.MODEL_ARCH_NAMES[gguf.MODEL_ARCH.LLAMA],
233
+ use_temp_file = False)
234
+ self.add_params(gguf_writer)
235
+ self.add_vocab(gguf_writer)
236
+ if self.special_vocab is not None:
237
+ self.special_vocab.add_to_gguf(gguf_writer)
238
+ self.add_tensors(gguf_writer)
239
+ logger.info(" gguf: write header")
240
+ gguf_writer.write_header_to_file()
241
+ logger.info(" gguf: write metadata")
242
+ gguf_writer.write_kv_data_to_file()
243
+ logger.info(" gguf: write tensors")
244
+ gguf_writer.write_tensors_to_file()
245
+ gguf_writer.close()
246
+
247
+ def add_params(self, gguf_writer):
248
+ hp = self.model.hyperparameters
249
+ cfg = self.cfg
250
+ if cfg.desc is not None:
251
+ desc = cfg.desc
252
+ else:
253
+ desc = f'converted from legacy {self.model.file_format.name}v{self.model.format_version} {hp.ftype.name} format'
254
+ try:
255
+ # Filenames aren't necessarily valid UTF8.
256
+ name = cfg.name if cfg.name is not None else cfg.input.name
257
+ except UnicodeDecodeError:
258
+ name = None
259
+ logger.info('* Adding model parameters and KV items')
260
+ if name is not None:
261
+ gguf_writer.add_name(name)
262
+ gguf_writer.add_description(desc)
263
+ gguf_writer.add_file_type(int(hp.ftype))
264
+ if self.params_override is not None:
265
+ po = self.params_override
266
+ assert po.n_embd == hp.n_embd, 'Model hyperparams mismatch'
267
+ assert po.n_layer == hp.n_layer, 'Model hyperparams mismatch'
268
+ assert po.n_head == hp.n_head, 'Model hyperparams mismatch'
269
+ gguf_writer.add_context_length (po.n_ctx)
270
+ gguf_writer.add_embedding_length (po.n_embd)
271
+ gguf_writer.add_block_count (po.n_layer)
272
+ gguf_writer.add_feed_forward_length (po.n_ff)
273
+ gguf_writer.add_rope_dimension_count(po.n_embd // po.n_head)
274
+ gguf_writer.add_head_count (po.n_head)
275
+ gguf_writer.add_head_count_kv (po.n_head_kv)
276
+ gguf_writer.add_layer_norm_rms_eps (po.f_norm_eps)
277
+ return
278
+ gguf_writer.add_context_length(cfg.context_length)
279
+ gguf_writer.add_embedding_length(hp.n_embd)
280
+ gguf_writer.add_block_count(hp.n_layer)
281
+ gguf_writer.add_feed_forward_length(hp.n_ff)
282
+ gguf_writer.add_rope_dimension_count(hp.n_embd // hp.n_head)
283
+ gguf_writer.add_head_count(hp.n_head)
284
+ gguf_writer.add_head_count_kv(self.n_kv_head)
285
+ gguf_writer.add_layer_norm_rms_eps(float(cfg.eps))
286
+
287
+ def add_vocab(self, gguf_writer):
288
+ hp = self.model.hyperparameters
289
+ gguf_writer.add_tokenizer_model('llama')
290
+ gguf_writer.add_tokenizer_pre('default')
291
+ tokens = []
292
+ scores = []
293
+ toktypes = []
294
+ if self.vocab_override is not None:
295
+ vo = self.vocab_override
296
+ logger.info('* Adding vocab item(s)')
297
+ for (_, (vbytes, score, ttype)) in enumerate(vo.all_tokens()):
298
+ tokens.append(vbytes)
299
+ scores.append(score)
300
+ toktypes.append(ttype)
301
+ assert len(tokens) == hp.n_vocab, \
302
+ f'Override vocab has a different number of items than hyperparameters - override = {len(tokens)} but n_vocab={hp.n_vocab}'
303
+ gguf_writer.add_token_list(tokens)
304
+ gguf_writer.add_token_scores(scores)
305
+ if len(toktypes) > 0:
306
+ gguf_writer.add_token_types(toktypes)
307
+ return
308
+ logger.info(f'* Adding {hp.n_vocab} vocab item(s)')
309
+ assert len(self.model.vocab.items) >= 3, 'Cannot handle unexpectedly short model vocab'
310
+ for (tokid, (vbytes, vscore)) in enumerate(self.model.vocab.items):
311
+ tt = 1 # Normal
312
+ # Special handling for UNK, BOS, EOS tokens.
313
+ if tokid <= 2:
314
+ if tokid == 0:
315
+ vbytes = b'<unk>'
316
+ tt = 2
317
+ elif tokid == 1:
318
+ vbytes = b'<s>'
319
+ tt = 3
320
+ else:
321
+ vbytes = b'</s>'
322
+ tt = 3
323
+ elif len(vbytes) == 0:
324
+ tt = 3 # Control
325
+ elif tokid >= 3 and tokid <= 258 and len(vbytes) == 1:
326
+ vbytes = bytes(f'<0x{vbytes[0]:02X}>', encoding = 'UTF-8')
327
+ tt = 6 # Byte
328
+ else:
329
+ vbytes = vbytes.replace(b' ', b'\xe2\x96\x81')
330
+ toktypes.append(tt)
331
+ tokens.append(vbytes)
332
+ scores.append(vscore)
333
+ gguf_writer.add_token_list(tokens)
334
+ gguf_writer.add_token_scores(scores)
335
+ gguf_writer.add_token_types(toktypes)
336
+ gguf_writer.add_unk_token_id(0)
337
+ gguf_writer.add_bos_token_id(1)
338
+ gguf_writer.add_eos_token_id(2)
339
+
340
+ def add_tensors(self, gguf_writer):
341
+ tensor_map = self.name_map
342
+ data = self.data
343
+ logger.info(f'* Adding {len(self.model.tensors)} tensor(s)')
344
+ for tensor in self.model.tensors:
345
+ name = str(tensor.name, 'UTF-8')
346
+ mapped_name = tensor_map.get_name(name, try_suffixes = (".weight", ".bias"))
347
+ assert mapped_name is not None, f'Bad name {name}'
348
+ tempdims = list(tensor.dims[:])
349
+ if len(tempdims) > 1:
350
+ temp = tempdims[1]
351
+ tempdims[1] = tempdims[0]
352
+ tempdims[0] = temp
353
+ gguf_writer.add_tensor(
354
+ mapped_name,
355
+ data[tensor.start_offset:tensor.start_offset + tensor.len_bytes],
356
+ raw_shape = tempdims,
357
+ raw_dtype = tensor.dtype)
358
+
359
+
360
+ def handle_metadata(cfg, hp):
361
+ import examples.convert_legacy_llama as convert
362
+
363
+ assert cfg.model_metadata_dir.is_dir(), 'Metadata dir is not a directory'
364
+ hf_config_path = cfg.model_metadata_dir / "config.json"
365
+ orig_config_path = cfg.model_metadata_dir / "params.json"
366
+ # We pass a fake model here. "original" mode will check the shapes of some
367
+ # tensors if information is missing in the .json file: other than that, the
368
+ # model data isn't used so this should be safe (at least for now).
369
+ fakemodel = {
370
+ 'tok_embeddings.weight': convert.LazyTensor.__new__(convert.LazyTensor),
371
+ 'layers.0.feed_forward.w1.weight': convert.LazyTensor.__new__(convert.LazyTensor),
372
+ }
373
+ fakemodel['tok_embeddings.weight'].shape = [hp.n_vocab]
374
+ fakemodel['layers.0.feed_forward.w1.weight'].shape = [hp.n_ff]
375
+ if hf_config_path.exists():
376
+ params = convert.Params.loadHFTransformerJson(fakemodel, hf_config_path)
377
+ elif orig_config_path.exists():
378
+ params = convert.Params.loadOriginalParamsJson(fakemodel, orig_config_path)
379
+ else:
380
+ raise ValueError('Unable to load metadata')
381
+ vocab_path = Path(cfg.vocab_dir if cfg.vocab_dir is not None else cfg.model_metadata_dir)
382
+ vocab_factory = convert.VocabFactory(vocab_path)
383
+ vocab, special_vocab = vocab_factory.load_vocab(cfg.vocabtype.split(","), cfg.model_metadata_dir)
384
+ convert.check_vocab_size(params, vocab)
385
+ return params, vocab, special_vocab
386
+
387
+
388
+ def handle_args():
389
+ parser = argparse.ArgumentParser(description = 'Convert GGML models to GGUF')
390
+ parser.add_argument('--input', '-i', type = Path, required = True,
391
+ help = 'Input GGMLv3 filename')
392
+ parser.add_argument('--output', '-o', type = Path, required = True,
393
+ help ='Output GGUF filename')
394
+ parser.add_argument('--name',
395
+ help = 'Set model name')
396
+ parser.add_argument('--desc',
397
+ help = 'Set model description')
398
+ parser.add_argument('--gqa', type = int, default = 1,
399
+ help = 'grouped-query attention factor (use 8 for LLaMA2 70B)')
400
+ parser.add_argument('--eps', default = '5.0e-06',
401
+ help = 'RMS norm eps: Use 1e-6 for LLaMA1 and OpenLLaMA, use 1e-5 for LLaMA2')
402
+ parser.add_argument('--context-length', '-c', type=int, default = 2048,
403
+ help = 'Default max context length: LLaMA1 is typically 2048, LLaMA2 is typically 4096')
404
+ parser.add_argument('--model-metadata-dir', '-m', type = Path,
405
+ help ='Load HuggingFace/.pth vocab and metadata from the specified directory')
406
+ parser.add_argument("--vocab-dir", type=Path,
407
+ help="directory containing tokenizer.model, if separate from model file - only meaningful with --model-metadata-dir")
408
+ parser.add_argument("--vocabtype", default="spm,hfft",
409
+ help="vocab format - only meaningful with --model-metadata-dir and/or --vocab-dir (default: spm,hfft)")
410
+ parser.add_argument("--verbose", action="store_true", help="increase output verbosity")
411
+ return parser.parse_args()
412
+
413
+
414
+ def main():
415
+ cfg = handle_args()
416
+ logging.basicConfig(level=logging.DEBUG if cfg.verbose else logging.INFO)
417
+ logger.info(f'* Using config: {cfg}')
418
+ logger.warning('=== WARNING === Be aware that this conversion script is best-effort. Use a native GGUF model if possible. === WARNING ===')
419
+ if cfg.model_metadata_dir is None and (cfg.gqa == 1 or cfg.eps == '5.0e-06'):
420
+ logger.info('- Note: If converting LLaMA2, specifying "--eps 1e-5" is required. 70B models also need "--gqa 8".')
421
+ data = np.memmap(cfg.input, mode = 'r')
422
+ model = GGMLModel()
423
+ logger.info('* Scanning GGML input file')
424
+ offset = model.load(data, 0) # noqa
425
+ logger.info(f'* GGML model hyperparameters: {model.hyperparameters}')
426
+ vocab_override = None
427
+ params_override = None
428
+ special_vocab = None
429
+ if cfg.model_metadata_dir is not None:
430
+ (params_override, vocab_override, special_vocab) = handle_metadata(cfg, model.hyperparameters)
431
+ logger.info('!! Note: When overriding params the --gqa, --eps and --context-length options are ignored.')
432
+ logger.info(f'* Overriding params: {params_override}')
433
+ logger.info(f'* Overriding vocab: {vocab_override}')
434
+ logger.info(f'* Special vocab: {special_vocab}')
435
+ else:
436
+ logger.warning('\n=== WARNING === Special tokens may not be converted correctly. Use --model-metadata-dir if possible === WARNING ===\n')
437
+ if model.file_format == GGMLFormat.GGML:
438
+ logger.info('! This is a very old GGML file that does not contain vocab scores. Strongly recommend using model metadata!')
439
+ converter = GGMLToGGUF(
440
+ model, data, cfg,
441
+ params_override = params_override,
442
+ vocab_override = vocab_override,
443
+ special_vocab = special_vocab
444
+ )
445
+ converter.save()
446
+ logger.info(f'* Successful completion. Output saved to: {cfg.output}')
447
+
448
+
449
+ if __name__ == '__main__':
450
+ main()
llama.cpp/convert_lora_to_gguf.py ADDED
@@ -0,0 +1,546 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass
7
+ import logging
8
+ import argparse
9
+ import os
10
+ import sys
11
+ import json
12
+ from math import prod
13
+ from pathlib import Path
14
+ from typing import TYPE_CHECKING, Any, Callable, Iterable, Iterator, Sequence, SupportsIndex, cast
15
+ from transformers import AutoConfig, AutoTokenizer
16
+
17
+ import torch
18
+
19
+ if TYPE_CHECKING:
20
+ from torch import Tensor
21
+
22
+ if 'NO_LOCAL_GGUF' not in os.environ:
23
+ sys.path.insert(1, str(Path(__file__).parent / 'gguf-py'))
24
+ import gguf
25
+ from gguf.constants import GGUFValueType
26
+
27
+ # reuse model definitions from the conversion/ package
28
+ from conversion import LazyTorchTensor, ModelBase, get_model_class, ModelType, get_model_architecture
29
+
30
+ logger = logging.getLogger("lora-to-gguf")
31
+
32
+
33
+ @dataclass
34
+ class PartialLoraTensor:
35
+ A: Tensor | None = None
36
+ B: Tensor | None = None
37
+
38
+
39
+ # magic to support tensor shape modifications and splitting
40
+ class LoraTorchTensor:
41
+ _lora_A: Tensor # (n_rank, row_size)
42
+ _lora_B: Tensor # (col_size, n_rank)
43
+ _rank: int
44
+
45
+ def __init__(self, A: Tensor, B: Tensor):
46
+ assert len(A.shape) == len(B.shape)
47
+ assert A.shape[-2] == B.shape[-1]
48
+ if A.dtype != B.dtype:
49
+ A = A.to(torch.float32)
50
+ B = B.to(torch.float32)
51
+ self._lora_A = A
52
+ self._lora_B = B
53
+ self._rank = B.shape[-1]
54
+
55
+ def get_lora_A_B(self) -> tuple[Tensor, Tensor]:
56
+ return (self._lora_A, self._lora_B)
57
+
58
+ def __getitem__(
59
+ self,
60
+ indices: (
61
+ SupportsIndex
62
+ | slice
63
+ | tuple[SupportsIndex | slice | Tensor, ...] # TODO: add ellipsis in the type signature
64
+ ),
65
+ ) -> LoraTorchTensor:
66
+ shape = self.shape
67
+ if isinstance(indices, SupportsIndex):
68
+ if len(shape) > 2:
69
+ return LoraTorchTensor(self._lora_A[indices], self._lora_B[indices])
70
+ else:
71
+ raise NotImplementedError # can't return a vector
72
+ elif isinstance(indices, slice):
73
+ if len(shape) > 2:
74
+ return LoraTorchTensor(self._lora_A[indices], self._lora_B[indices])
75
+ else:
76
+ return LoraTorchTensor(self._lora_A, self._lora_B[indices])
77
+ elif isinstance(indices, tuple):
78
+ assert len(indices) > 0
79
+ if indices[-1] is Ellipsis:
80
+ return self[indices[:-1]]
81
+ # expand ellipsis
82
+ indices = tuple(
83
+ u
84
+ for v in (
85
+ (
86
+ (slice(None, None) for _ in range(len(indices) - 1))
87
+ if i is Ellipsis
88
+ else (i,)
89
+ )
90
+ for i in indices
91
+ )
92
+ for u in v
93
+ )
94
+
95
+ if len(indices) < len(shape):
96
+ indices = (*indices, *(slice(None, None) for _ in range(len(indices), len(shape))))
97
+
98
+ # TODO: make sure this is correct
99
+ indices_A = (
100
+ *(
101
+ (
102
+ j.__index__() % self._lora_A.shape[i]
103
+ if isinstance(j, SupportsIndex)
104
+ else slice(None, None)
105
+ )
106
+ for i, j in enumerate(indices[:-2])
107
+ ),
108
+ slice(None, None),
109
+ indices[-1],
110
+ )
111
+ indices_B = indices[:-1]
112
+ return LoraTorchTensor(self._lora_A[indices_A], self._lora_B[indices_B])
113
+ else:
114
+ raise NotImplementedError # unknown indice type
115
+
116
+ @property
117
+ def dtype(self) -> torch.dtype:
118
+ assert self._lora_A.dtype == self._lora_B.dtype
119
+ return self._lora_A.dtype
120
+
121
+ @property
122
+ def shape(self) -> tuple[int, ...]:
123
+ assert len(self._lora_A.shape) == len(self._lora_B.shape)
124
+ return (*self._lora_B.shape[:-1], self._lora_A.shape[-1])
125
+
126
+ def size(self, dim=None):
127
+ assert dim is None
128
+ return self.shape
129
+
130
+ def contiguous(self) -> LoraTorchTensor:
131
+ return LoraTorchTensor(
132
+ self._lora_A.contiguous(),
133
+ self._lora_B.contiguous(),
134
+ )
135
+
136
+ def reshape(self, *shape: int | tuple[int, ...]) -> LoraTorchTensor:
137
+ if isinstance(shape[0], tuple):
138
+ new_shape: tuple[int, ...] = shape[0]
139
+ else:
140
+ new_shape = cast(tuple[int, ...], shape)
141
+ orig_shape = self.shape
142
+ if len(new_shape) < 2:
143
+ raise NotImplementedError # can't become a vector
144
+
145
+ # expand -1 in the shape
146
+ if any(dim == -1 for dim in new_shape):
147
+ n_elems = prod(orig_shape)
148
+ n_new_elems = prod(dim if dim != -1 else 1 for dim in new_shape)
149
+ assert n_elems % n_new_elems == 0
150
+ new_shape = (*(dim if dim != -1 else n_elems // n_new_elems for dim in new_shape),)
151
+
152
+ if new_shape[-1] != orig_shape[-1]:
153
+ raise NotImplementedError # can't reshape the row size trivially
154
+
155
+ shape_A = (*(1 for _ in new_shape[:-2]), self._rank, orig_shape[-1])
156
+ shape_B = (*new_shape[:-1], self._rank)
157
+ return LoraTorchTensor(
158
+ self._lora_A.reshape(shape_A),
159
+ self._lora_B.reshape(shape_B),
160
+ )
161
+
162
+ def reshape_as(self, other: Tensor) -> LoraTorchTensor:
163
+ return self.reshape(*other.shape)
164
+
165
+ def view(self, *size: int) -> LoraTorchTensor:
166
+ return self.reshape(*size)
167
+
168
+ def permute(self, *dims: int) -> LoraTorchTensor:
169
+ shape = self.shape
170
+ dims = tuple(dim - len(shape) if dim >= 0 else dim for dim in dims)
171
+ if dims[-1] == -1:
172
+ # TODO: support higher dimensional A shapes bigger than 1
173
+ assert all(dim == 1 for dim in self._lora_A.shape[:-2])
174
+ return LoraTorchTensor(self._lora_A, self._lora_B.permute(*dims))
175
+ if len(shape) == 2 and dims[-1] == -2 and dims[-2] == -1:
176
+ return LoraTorchTensor(self._lora_B.permute(*dims), self._lora_A.permute(*dims))
177
+ else:
178
+ # TODO: compose the above two
179
+ raise NotImplementedError
180
+
181
+ def transpose(self, dim0: int, dim1: int) -> LoraTorchTensor:
182
+ shape = self.shape
183
+ dims = [i for i in range(len(shape))]
184
+ dims[dim0], dims[dim1] = dims[dim1], dims[dim0]
185
+ return self.permute(*dims)
186
+
187
+ def swapaxes(self, axis0: int, axis1: int) -> LoraTorchTensor:
188
+ return self.transpose(axis0, axis1)
189
+
190
+ def split(self, split_size: int | Sequence[int], dim: int = 0) -> tuple[LoraTorchTensor, ...]:
191
+ shape = self.shape
192
+ ndim = len(shape)
193
+ if dim < 0:
194
+ dim += ndim
195
+ if dim == ndim - 1:
196
+ A_chunks = self._lora_A.split(split_size, dim=-1)
197
+ return tuple(LoraTorchTensor(a, self._lora_B) for a in A_chunks)
198
+ elif dim == ndim - 2:
199
+ B_chunks = self._lora_B.split(split_size, dim=-2)
200
+ return tuple(LoraTorchTensor(self._lora_A, b) for b in B_chunks)
201
+ else:
202
+ B_chunks = self._lora_B.split(split_size, dim=dim)
203
+ if self._lora_A.shape[dim] == 1:
204
+ return tuple(LoraTorchTensor(self._lora_A, b) for b in B_chunks)
205
+ A_chunks = self._lora_A.split(split_size, dim=dim)
206
+ return tuple(LoraTorchTensor(a, b) for a, b in zip(A_chunks, B_chunks))
207
+
208
+ def to(self, *args, **kwargs):
209
+ return LoraTorchTensor(self._lora_A.to(*args, **kwargs), self._lora_B.to(*args, **kwargs))
210
+
211
+ def __mul__(self, other) -> LoraTorchTensor:
212
+ # Only output-side multiplication for now
213
+ # W = B @ A, so M_out * W == (M_out * B) @ A
214
+ if not isinstance(other, (int, float)) and other.shape and other.shape[-1] != 1:
215
+ raise NotImplementedError
216
+ return LoraTorchTensor(self._lora_A, self._lora_B * other)
217
+
218
+ def __rmul__(self, other) -> LoraTorchTensor:
219
+ return self * other
220
+
221
+ @classmethod
222
+ def __torch_function__(cls, func: Callable, types, args=(), kwargs=None):
223
+ del types # unused
224
+
225
+ if kwargs is None:
226
+ kwargs = {}
227
+
228
+ if func is torch.permute:
229
+ assert len(args)
230
+ return type(args[0]).permute(*args, **kwargs)
231
+ elif func is torch.reshape:
232
+ assert len(args)
233
+ return type(args[0]).reshape(*args, **kwargs)
234
+ elif func is torch.stack:
235
+ assert len(args)
236
+ assert isinstance(args[0], Sequence)
237
+ dim = kwargs.get("dim", 0)
238
+ assert dim == 0
239
+ return LoraTorchTensor(
240
+ torch.stack([a._lora_A for a in args[0]], dim),
241
+ torch.stack([b._lora_B for b in args[0]], dim),
242
+ )
243
+ elif func is torch.cat:
244
+ assert len(args)
245
+ assert isinstance(args[0], Sequence)
246
+ dim = kwargs.get("dim", 0)
247
+ assert dim == 0
248
+ if len(args[0][0].shape) > 2:
249
+ return LoraTorchTensor(
250
+ torch.cat([a._lora_A for a in args[0]], dim),
251
+ torch.cat([b._lora_B for b in args[0]], dim),
252
+ )
253
+ elif all(torch.equal(args[0][0]._lora_A, t._lora_A) for t in args[0][1:]):
254
+ return LoraTorchTensor(
255
+ args[0][0]._lora_A,
256
+ torch.cat([b._lora_B for b in args[0]], dim),
257
+ )
258
+ else:
259
+ raise NotImplementedError
260
+ elif func is torch.split:
261
+ assert len(args) and len(args) >= 2
262
+ tensor, split_size = args[0], args[1]
263
+ dim = args[2] if len(args) > 2 else kwargs.get("dim", 0)
264
+ return tensor.split(split_size, dim=dim)
265
+ else:
266
+ raise NotImplementedError
267
+
268
+
269
+ def get_base_tensor_name(lora_tensor_name: str) -> str:
270
+ base_name = lora_tensor_name.replace("base_model.model.", "")
271
+ base_name = base_name.replace(".lora_A.weight", ".weight")
272
+ base_name = base_name.replace(".lora_B.weight", ".weight")
273
+ # models produced by mergekit-extract-lora have token embeddings in the adapter
274
+ base_name = base_name.replace(".lora_embedding_A", ".weight")
275
+ base_name = base_name.replace(".lora_embedding_B", ".weight")
276
+ return base_name
277
+
278
+
279
+ def parse_args() -> argparse.Namespace:
280
+ parser = argparse.ArgumentParser(
281
+ description="Convert a Hugging Face PEFT LoRA adapter to a GGUF file")
282
+ parser.add_argument(
283
+ "--outfile", type=Path,
284
+ help="path to write to; default: based on input. {ftype} will be replaced by the outtype.",
285
+ )
286
+ parser.add_argument(
287
+ "--outtype", type=str, choices=["f32", "f16", "bf16", "q8_0", "auto"], default="f32",
288
+ help="output format - use f32 for float32, f16 for float16, bf16 for bfloat16, q8_0 for Q8_0, auto for the highest-fidelity 16-bit float type depending on the first loaded tensor type",
289
+ )
290
+ parser.add_argument(
291
+ "--bigendian", action="store_true",
292
+ help="model is executed on big endian machine",
293
+ )
294
+ parser.add_argument(
295
+ "--no-lazy", action="store_true",
296
+ help="use more RAM by computing all outputs before writing (use in case lazy evaluation is broken)",
297
+ )
298
+ parser.add_argument(
299
+ "--verbose", action="store_true",
300
+ help="increase output verbosity",
301
+ )
302
+ parser.add_argument(
303
+ "--dry-run", action="store_true",
304
+ help="only print out what will be done, without writing any new files",
305
+ )
306
+ parser.add_argument(
307
+ "--base", type=Path,
308
+ help="directory containing Hugging Face model config files (config.json, tokenizer.json) for the base model that the adapter is based on - only config is needed, actual model weights are not required. If base model is unspecified, it will be loaded from Hugging Face hub based on the adapter config",
309
+ )
310
+ parser.add_argument(
311
+ "--base-model-id", type=str,
312
+ help="the model ID of the base model, if it is not available locally or in the adapter config. If specified, it will ignore --base and load the base model config from the Hugging Face hub (Example: 'meta-llama/Llama-3.2-1B-Instruct')",
313
+ )
314
+ parser.add_argument(
315
+ "--trust-remote-code", default=False, action="store_true",
316
+ help="trust remote code in the model",
317
+ )
318
+ parser.add_argument(
319
+ "lora_path", type=Path,
320
+ help="directory containing Hugging Face PEFT LoRA config (adapter_model.json) and weights (adapter_model.safetensors or adapter_model.bin)",
321
+ )
322
+
323
+ return parser.parse_args()
324
+
325
+
326
+ def load_hparams_from_hf(hf_model_id: str, trust_remote_code: bool) -> tuple[dict[str, Any], Path | None]:
327
+ from huggingface_hub import try_to_load_from_cache
328
+
329
+ # normally, adapter does not come with base model config, we need to load it from AutoConfig
330
+ config = AutoConfig.from_pretrained(hf_model_id, trust_remote_code=trust_remote_code)
331
+ cache_dir = try_to_load_from_cache(hf_model_id, "config.json")
332
+ cache_dir = Path(cache_dir).parent if isinstance(cache_dir, str) else None
333
+
334
+ return config.to_dict(), cache_dir
335
+
336
+
337
+ if __name__ == '__main__':
338
+ args = parse_args()
339
+ logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
340
+
341
+ ftype_map: dict[str, gguf.LlamaFileType] = {
342
+ "f32": gguf.LlamaFileType.ALL_F32,
343
+ "f16": gguf.LlamaFileType.MOSTLY_F16,
344
+ "bf16": gguf.LlamaFileType.MOSTLY_BF16,
345
+ "q8_0": gguf.LlamaFileType.MOSTLY_Q8_0,
346
+ "auto": gguf.LlamaFileType.GUESSED,
347
+ }
348
+
349
+ ftype = ftype_map[args.outtype]
350
+
351
+ dir_base_model: Path | None = args.base
352
+ dir_lora: Path = args.lora_path
353
+ base_model_id: str | None = args.base_model_id
354
+ lora_config = dir_lora / "adapter_config.json"
355
+ input_model = dir_lora / "adapter_model.safetensors"
356
+
357
+ if args.outfile is not None:
358
+ fname_out = args.outfile
359
+ else:
360
+ # output in the same directory as the model by default
361
+ fname_out = dir_lora
362
+
363
+ if os.path.exists(input_model):
364
+ # lazy import load_file only if lora is in safetensors format.
365
+ from safetensors.torch import load_file
366
+
367
+ lora_model = load_file(input_model, device="cpu")
368
+ else:
369
+ input_model = os.path.join(dir_lora, "adapter_model.bin")
370
+ lora_model = torch.load(input_model, map_location="cpu", weights_only=True)
371
+
372
+ # load LoRA config
373
+ with open(lora_config, "r") as f:
374
+ lparams: dict[str, Any] = json.load(f)
375
+
376
+ # load base model
377
+ if base_model_id is not None:
378
+ logger.info(f"Loading base model from Hugging Face: {base_model_id}")
379
+ hparams, dir_base_model = load_hparams_from_hf(base_model_id, args.trust_remote_code)
380
+ elif dir_base_model is None:
381
+ if "base_model_name_or_path" in lparams:
382
+ model_id = lparams["base_model_name_or_path"]
383
+ logger.info(f"Loading base model from Hugging Face: {model_id}")
384
+ try:
385
+ hparams, dir_base_model = load_hparams_from_hf(model_id, args.trust_remote_code)
386
+ except OSError as e:
387
+ logger.error(f"Failed to load base model config: {e}")
388
+ logger.error("Please try downloading the base model and add its path to --base")
389
+ sys.exit(1)
390
+ else:
391
+ logger.error("'base_model_name_or_path' is not found in adapter_config.json")
392
+ logger.error("Base model config is required. Please download the base model and add its path to --base")
393
+ sys.exit(1)
394
+ else:
395
+ logger.info(f"Loading base model: {dir_base_model.name}")
396
+ hparams = ModelBase.load_hparams(dir_base_model, False)
397
+
398
+ with torch.inference_mode():
399
+ model_arch = get_model_architecture(hparams, ModelType.TEXT)
400
+ try:
401
+ model_class = get_model_class(model_arch)
402
+ logger.info("Using model architecture: %s", model_arch)
403
+ except NotImplementedError:
404
+ logger.error(f"Model {model_arch} is not supported")
405
+ sys.exit(1)
406
+
407
+ class LoraModel(model_class): # ty: ignore[unsupported-base]
408
+ model_arch = model_class.model_arch
409
+
410
+ lora_alpha: float
411
+
412
+ def __init__(self, *args, dir_lora_model: Path, lora_alpha: float, **kwargs):
413
+
414
+ super().__init__(*args, **kwargs)
415
+
416
+ self.dir_model_card = dir_lora_model
417
+ self.lora_alpha = float(lora_alpha)
418
+
419
+ def set_vocab(self):
420
+ pass
421
+
422
+ def set_type(self):
423
+ self.gguf_writer.add_type(gguf.GGUFType.ADAPTER)
424
+ self.gguf_writer.add_string(gguf.Keys.Adapter.TYPE, "lora")
425
+
426
+ def set_gguf_parameters(self):
427
+ logger.debug("GGUF KV: %s = %d", gguf.Keys.Adapter.LORA_ALPHA, self.lora_alpha)
428
+ self.gguf_writer.add_float32(gguf.Keys.Adapter.LORA_ALPHA, self.lora_alpha)
429
+ alora_invocation_tokens = lparams.get("alora_invocation_tokens")
430
+ invocation_string = lparams.get("invocation_string")
431
+ if invocation_string and not alora_invocation_tokens:
432
+ logger.debug("Tokenizing invocation_string -> alora_invocation_tokens")
433
+ base_model_path_or_id = hparams.get("_name_or_path")
434
+ try:
435
+ tokenizer = AutoTokenizer.from_pretrained(base_model_path_or_id)
436
+ except ValueError:
437
+ logger.error("Unable to load tokenizer from %s", base_model_path_or_id)
438
+ raise
439
+ # NOTE: There's an off-by-one with the older aLoRAs where
440
+ # the invocation string includes the "<|start_of_turn|>"
441
+ # token, but the adapters themselves were trained to
442
+ # activate _after_ that first token, so we drop it here.
443
+ alora_invocation_tokens = tokenizer(invocation_string)["input_ids"][1:] # ty: ignore[call-non-callable]
444
+ if alora_invocation_tokens:
445
+ logger.debug("GGUF KV: %s = %s", gguf.Keys.Adapter.ALORA_INVOCATION_TOKENS, alora_invocation_tokens)
446
+ self.gguf_writer.add_key_value(
447
+ gguf.Keys.Adapter.ALORA_INVOCATION_TOKENS,
448
+ alora_invocation_tokens,
449
+ GGUFValueType.ARRAY,
450
+ GGUFValueType.UINT32,
451
+ )
452
+
453
+ def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]:
454
+ # Never add extra tensors (e.g. rope_freqs) for LoRA adapters
455
+ return ()
456
+
457
+ def get_tensors(self) -> Iterator[tuple[str, Tensor]]:
458
+ tensor_map: dict[str, PartialLoraTensor] = {}
459
+
460
+ for name, tensor in lora_model.items():
461
+ if self.lazy:
462
+ tensor = LazyTorchTensor.from_eager(tensor)
463
+ base_name = get_base_tensor_name(name)
464
+ # filter base name, ignore tensor transformations for now
465
+ data_gen = lambda g=tensor: g # noqa: E731
466
+ if (titem := self.filter_tensors((base_name, data_gen))) is None:
467
+ continue
468
+ base_name, _ = titem
469
+ # note: mergekit-extract-lora also adds token embeddings to the adapter
470
+ is_lora_a = ".lora_A.weight" in name or ".lora_embedding_A" in name
471
+ is_lora_b = ".lora_B.weight" in name or ".lora_embedding_B" in name
472
+ if not is_lora_a and not is_lora_b:
473
+ if ".base_layer.weight" in name:
474
+ continue
475
+ # mergekit-extract-lora add these layernorm to the adapter, we need to keep them
476
+ if "_layernorm" in name or ".norm" in name:
477
+ yield (base_name, tensor)
478
+ continue
479
+ logger.error(f"Unexpected name '{name}': Not a lora_A or lora_B tensor")
480
+ if ".embed_tokens.weight" in name or ".lm_head.weight" in name:
481
+ logger.error("Embeddings is present in the adapter. This can be due to new tokens added during fine tuning")
482
+ logger.error("Please refer to https://github.com/ggml-org/llama.cpp/pull/9948")
483
+ sys.exit(1)
484
+
485
+ if base_name in tensor_map:
486
+ if is_lora_a:
487
+ tensor_map[base_name].A = tensor
488
+ else:
489
+ tensor_map[base_name].B = tensor
490
+ else:
491
+ if is_lora_a:
492
+ tensor_map[base_name] = PartialLoraTensor(A=tensor)
493
+ else:
494
+ tensor_map[base_name] = PartialLoraTensor(B=tensor)
495
+
496
+ for name, tensor in tensor_map.items():
497
+ assert tensor.A is not None
498
+ assert tensor.B is not None
499
+ yield (name, cast(torch.Tensor, LoraTorchTensor(tensor.A, tensor.B)))
500
+
501
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
502
+ dest = list(super().modify_tensors(data_torch, name, bid))
503
+ # some archs may have the same tensor for lm_head and output (tie word embeddings)
504
+ # in this case, adapters targeting lm_head will fail when using llama-export-lora
505
+ # therefore, we ignore them for now
506
+ # see: https://github.com/ggml-org/llama.cpp/issues/9065
507
+ if name == "lm_head.weight" and len(dest) == 0:
508
+ raise ValueError("lm_head is present in adapter, but is ignored in base model")
509
+ for dest_name, dest_data in dest:
510
+ # mergekit-extract-lora add these layernorm to the adapter
511
+ if "_norm" in dest_name:
512
+ assert dest_data.dim() == 1
513
+ yield (dest_name, dest_data)
514
+ continue
515
+
516
+ # otherwise, we must get the lora_A and lora_B tensors
517
+ assert isinstance(dest_data, LoraTorchTensor)
518
+ lora_a, lora_b = dest_data.get_lora_A_B()
519
+
520
+ # note: mergekit-extract-lora flip and transpose A and B
521
+ # here we only need to transpose token_embd.lora_a, see llm_build_inp_embd()
522
+ if "token_embd.weight" in dest_name:
523
+ lora_a = lora_a.T
524
+
525
+ yield (dest_name + ".lora_a", lora_a)
526
+ yield (dest_name + ".lora_b", lora_b)
527
+
528
+ alpha: float = lparams["lora_alpha"]
529
+
530
+ model_instance = LoraModel(
531
+ dir_base_model,
532
+ ftype,
533
+ fname_out,
534
+ is_big_endian=args.bigendian,
535
+ use_temp_file=False,
536
+ eager=args.no_lazy,
537
+ dry_run=args.dry_run,
538
+ dir_lora_model=dir_lora,
539
+ lora_alpha=alpha,
540
+ hparams=hparams,
541
+ remote_hf_model_id=base_model_id,
542
+ )
543
+
544
+ logger.info("Exporting model...")
545
+ model_instance.write()
546
+ logger.info(f"Model successfully exported to {model_instance.fname_out}")
llama.cpp/docs/android.md ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # Android
3
+
4
+ ## Build GUI binding using Android Studio
5
+
6
+ Import the `examples/llama.android` directory into Android Studio, then perform a Gradle sync and build the project.
7
+ ![Project imported into Android Studio](./android/imported-into-android-studio.jpg)
8
+
9
+ This Android binding supports hardware acceleration up to `SME2` for **Arm** and `AMX` for **x86-64** CPUs on Android and ChromeOS devices.
10
+ It automatically detects the host's hardware to load compatible kernels. As a result, it runs seamlessly on both the latest premium devices and older devices that may lack modern CPU features or have limited RAM, without requiring any manual configuration.
11
+
12
+ A minimal Android app frontend is included to showcase the binding’s core functionalities:
13
+ 1. **Parse GGUF metadata** via `GgufMetadataReader` from either a `ContentResolver` provided `Uri` from shared storage, or a local `File` from your app's private storage.
14
+ 2. **Obtain a `InferenceEngine`** instance through the `AiChat` facade and load your selected model via its app-private file path.
15
+ 3. **Send a raw user prompt** for automatic template formatting, prefill, and batch decoding. Then collect the generated tokens in a Kotlin `Flow`.
16
+
17
+ For a production-ready experience that leverages advanced features such as system prompts and benchmarks, plus friendly UI features such as model management and Arm feature visualizer, check out [Arm AI Chat](https://play.google.com/store/apps/details?id=com.arm.aichat) on Google Play.
18
+ This project is made possible through a collaborative effort by Arm's **CT-ML**, **CE-ML** and **STE** groups:
19
+
20
+ | ![Home screen](https://naco-siren.github.io/ai-chat/policy/index/1-llm-starter-pack.png) | ![System prompt](https://naco-siren.github.io/ai-chat/policy/index/5-system-prompt.png) | !["Haiku"](https://naco-siren.github.io/ai-chat/policy/index/4-metrics.png) |
21
+ |:------------------------------------------------------:|:----------------------------------------------------:|:--------------------------------------------------------:|
22
+ | Home screen | System prompt | "Haiku" |
23
+
24
+ ## Build CLI on Android using Termux
25
+
26
+ [Termux](https://termux.dev/en/) is an Android terminal emulator and Linux environment app (no root required). As of writing, Termux is available experimentally in the Google Play Store; otherwise, it may be obtained directly from the project repo or on F-Droid.
27
+
28
+ With Termux, you can install and run `llama.cpp` as if the environment were Linux. Once in the Termux shell:
29
+
30
+ ```
31
+ $ apt update && apt upgrade -y
32
+ $ apt install git cmake libandroid-spawn
33
+ ```
34
+
35
+ Then, follow the [build instructions](https://github.com/ggml-org/llama.cpp/blob/master/docs/build.md), specifically for CMake.
36
+
37
+ Once the binaries are built, download your model of choice (e.g., from Hugging Face). It's recommended to place it in the `~/` directory for best performance:
38
+
39
+ ```
40
+ $ curl -L {model-url} -o ~/{model}.gguf
41
+ ```
42
+
43
+ Then, if you are not already in the repo directory, `cd` into `llama.cpp` and:
44
+
45
+ ```
46
+ $ ./build/bin/llama-cli -m ~/{model}.gguf -c {context-size} -p "{your-prompt}"
47
+ ```
48
+
49
+ Here, we show `llama-cli`, but any of the executables under `examples` should work, in theory. Be sure to set `context-size` to a reasonable number (say, 4096) to start with; otherwise, memory could spike and kill your terminal.
50
+
51
+ To see what it might look like visually, here's an old demo of an interactive session running on a Pixel 5 phone:
52
+
53
+ https://user-images.githubusercontent.com/271616/225014776-1d567049-ad71-4ef2-b050-55b0b3b9274c.mp4
54
+
55
+ ## Cross-compile CLI using Android NDK
56
+ It's possible to build `llama.cpp` for Android on your host system via CMake and the Android NDK. If you are interested in this path, ensure you already have an environment prepared to cross-compile programs for Android (i.e., install the Android SDK). Note that, unlike desktop environments, the Android environment ships with a limited set of native libraries, and so only those libraries are available to CMake when building with the Android NDK (see: https://developer.android.com/ndk/guides/stable_apis.)
57
+
58
+ Once you're ready and have cloned `llama.cpp`, invoke the following in the project directory:
59
+
60
+ ```
61
+ $ cmake \
62
+ -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
63
+ -DANDROID_ABI=arm64-v8a \
64
+ -DANDROID_PLATFORM=android-28 \
65
+ -DCMAKE_C_FLAGS="-march=armv8.7a" \
66
+ -DCMAKE_CXX_FLAGS="-march=armv8.7a" \
67
+ -DGGML_OPENMP=OFF \
68
+ -DGGML_LLAMAFILE=OFF \
69
+ -B build-android
70
+ ```
71
+
72
+ Notes:
73
+ - While later versions of Android NDK ship with OpenMP, it must still be installed by CMake as a dependency, which is not supported at this time
74
+ - `llamafile` does not appear to support Android devices (see: https://github.com/Mozilla-Ocho/llamafile/issues/325)
75
+
76
+ The above command should configure `llama.cpp` with the most performant options for modern devices. Even if your device is not running `armv8.7a`, `llama.cpp` includes runtime checks for available CPU features it can use.
77
+
78
+ Feel free to adjust the Android ABI for your target. Once the project is configured:
79
+
80
+ ```
81
+ $ cmake --build build-android --config Release -j{n}
82
+ $ cmake --install build-android --prefix {install-dir} --config Release
83
+ ```
84
+
85
+ After installing, go ahead and download the model of your choice to your host system. Then:
86
+
87
+ ```
88
+ $ adb shell "mkdir /data/local/tmp/llama.cpp"
89
+ $ adb push {install-dir} /data/local/tmp/llama.cpp/
90
+ $ adb push {model}.gguf /data/local/tmp/llama.cpp/
91
+ $ adb shell
92
+ ```
93
+
94
+ In the `adb shell`:
95
+
96
+ ```
97
+ $ cd /data/local/tmp/llama.cpp
98
+ $ LD_LIBRARY_PATH=lib ./bin/llama-simple -m {model}.gguf -c {context-size} -p "{your-prompt}"
99
+ ```
100
+
101
+ That's it!
102
+
103
+ Be aware that Android will not find the library path `lib` on its own, so we must specify `LD_LIBRARY_PATH` in order to run the installed executables. Android does support `RPATH` in later API levels, so this could change in the future. Refer to the previous section for information about `context-size` (very important!) and running other `examples`.
llama.cpp/docs/android/imported-into-android-studio.jpg ADDED

Git LFS Details

  • SHA256: 3cc2c2bf6582bf748216ba5d19fc6ff355d054f8f9d3321212407ace06e4b8d9
  • Pointer size: 131 Bytes
  • Size of remote file: 491 kB
llama.cpp/docs/autoparser.md ADDED
@@ -0,0 +1,534 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Auto-Parser Architecture
2
+
3
+ The auto-parser automatically analyzes chat templates to determine how to parse model outputs, including content, reasoning, and tool calls.
4
+
5
+ ## Overview
6
+
7
+ The unified auto-parser uses a pure differential, compositional approach (inspired by the `git diff` algorithm) to analyze chat templates:
8
+
9
+ **Core Philosophy**:
10
+
11
+ - **Minimize Hardcoded Patterns**: All markers extracted through template comparison (the only heuristic is JSON detection to distinguish `JSON_NATIVE` from tag-based formats)
12
+ - **Compositional Architecture**: Separate analyzer structs for reasoning, content, and tools — each responsible for its own analysis and parser construction
13
+
14
+ **Analysis + Parser Building in Two Steps**:
15
+
16
+ 1. `autoparser::autoparser tmpl_analysis(tmpl)` — runs all differential comparisons and populates the analysis structs
17
+ 2. `autoparser::peg_generator::generate_parser(tmpl, generation_params, tmpl_analysis)` — uses the analysis to build a PEG parser and optional GBNF grammar
18
+
19
+ ## Data Structures
20
+
21
+ All structs are defined in [common/chat-auto-parser.h](common/chat-auto-parser.h).
22
+
23
+ ### Top-Level: `autoparser` (main analyzer and generator)
24
+
25
+ [common/chat-auto-parser.h:367-388](common/chat-auto-parser.h#L367-L388) — top-level analysis result aggregating `jinja_caps`, `reasoning`, `content`, and `tools` sub-analyses, plus `preserved_tokens` (union of all non-empty markers).
26
+
27
+ ### `analyze_reasoning`
28
+
29
+ [common/chat-auto-parser.h:254-274](common/chat-auto-parser.h#L254-L274) — reasoning analysis result: `mode` enum, `start` marker (e.g. `<think>`), and `end` marker (e.g. `</think>`).
30
+
31
+ ### `analyze_content`
32
+
33
+ [common/chat-auto-parser.h:280-295](common/chat-auto-parser.h#L280-L295) — content analysis result: `mode` enum, `start`/`end` markers, and `requires_nonnull_content` flag.
34
+
35
+ ### `analyze_tools` and its sub-structs
36
+
37
+ - [common/chat-auto-parser.h:176-194](common/chat-auto-parser.h#L176-L194) — `tool_format_analysis`: `mode` enum, `section_start/end`, `per_call_start/end`, JSON field names (`function_field`, `name_field`, `args_field`, `id_field`, `gen_id_field`), and format flags (`fun_name_is_key`, `tools_array_wrapped`)
38
+ - [common/chat-auto-parser.h:196-200](common/chat-auto-parser.h#L196-L200) — `tool_function_analysis`: `name_prefix`, `name_suffix`, `close` markers around function names
39
+ - [common/chat-auto-parser.h:202-210](common/chat-auto-parser.h#L202-L210) — `tool_arguments_analysis`: `start/end` container markers, `name_prefix/suffix`, `value_prefix/suffix`, `separator`
40
+ - [common/chat-auto-parser.h:212-217](common/chat-auto-parser.h#L212-L217) — `tool_id_analysis`: `pos` enum, `prefix`/`suffix` markers around call ID values
41
+ - [common/chat-auto-parser.h:301-361](common/chat-auto-parser.h#L301-L361) — `analyze_tools`: aggregates the four sub-structs above
42
+
43
+ ### Enums
44
+
45
+ **`reasoning_mode`**: How the template handles reasoning/thinking blocks.
46
+
47
+ | Value | Description |
48
+ |-----------------|-----------------------------------------------------------------------------------|
49
+ | `NONE` | No reasoning markers detected |
50
+ | `TAG_BASED` | Tag-based: `<think>...</think>` (start can be empty for delimiter-style formats) |
51
+ | `TOOLS_ONLY` | Reasoning only appears in tool call responses, not plain content |
52
+
53
+ **Generation Prompt & Reasoning Prefill**: Computed in `common_chat_templates_apply_jinja` before invoking either the specialized handlers or the auto-parser, by rendering the template twice — once with `add_generation_prompt=false` and once with `add_generation_prompt=true` — and storing the diff suffix as `generation_params::generation_prompt`. This string is propagated into `common_chat_params::generation_prompt` and `common_chat_parser_params::generation_prompt`.
54
+
55
+ The generation prompt is prepended to model output before PEG parsing via `wrap_for_generation_prompt()`. The portion *before* the reasoning start marker (if any) is prepended as a literal to ensure any boilerplate added by the template is consumed. The full string is also fed to the grammar sampler via `llama_sampler_accept` (stored in `common_params_sampling::grammar_prefill`), advancing the grammar past tokens already in the prompt. It is used to determine the reasoning budget sampler's initial state — COUNTING if the prefill tokens begin with the reasoning start sequence (but don't also contain the end sequence), IDLE otherwise.
56
+
57
+ **`grammar_prefill`** (`common_params_sampling`): The generation prompt string tokenized and accepted by the grammar sampler at init time. Only applied when `grammar_external` is false (i.e., the grammar was not set explicitly by the user).
58
+
59
+ Three outcomes for reasoning-prefill handling (in `generate_parser()`):
60
+
61
+ 1. **Start+end in generation prompt** (e.g. `<think></think>\n`): the parser sees reasoning as opened and immediately closed; whitespace-only reasoning content is discarded.
62
+ 2. **Only start in generation prompt** (e.g. `<think>\n`): the parser sees reasoning as already open.
63
+ 3. **Start marker present but not at the end** (e.g. Apriel's `<|begin_assistant|>` followed by boilerplate): the marker is a template artifact; the start literal is cleared so reasoning uses delimiter-style (end-only). For templates that ignore `add_generation_prompt` (empty diff), the rendered `data.prompt` is used as fallback — but only for non-TOOLS_ONLY modes, since in TOOLS_ONLY the start tag is model-generated and may appear in prior conversation turns.
64
+
65
+ **`content_mode`**: How the template wraps assistant content.
66
+
67
+ | Value | Description |
68
+ |--------------------------|----------------------------------------------------------------|
69
+ | `PLAIN` | No content markers |
70
+ | `ALWAYS_WRAPPED` | Content always wrapped: `<response>...</response>` |
71
+ | `WRAPPED_WITH_REASONING` | Content wrapped only when reasoning is present |
72
+
73
+ **`tool_format`**: Classification of tool call structure.
74
+
75
+ | Value | Description |
76
+ |------------------|------------------------------------------------------------------|
77
+ | `NONE` | No tool support detected |
78
+ | `JSON_NATIVE` | Pure JSON: `{"name": "X", "arguments": {...}}` |
79
+ | `TAG_WITH_JSON` | Tag-based with JSON args: `<function=X>{...}</function>` |
80
+ | `TAG_WITH_TAGGED`| Tag-based with tagged args: `<param=key>value</param>` |
81
+
82
+ **`call_id_position`**: Where call IDs appear in tag-based formats.
83
+
84
+ | Value | Description |
85
+ |--------------------------|----------------------------------------------|
86
+ | `NONE` | No call ID support detected |
87
+ | `PRE_FUNC_NAME` | Before function name |
88
+ | `BETWEEN_FUNC_AND_ARGS` | Between function name and arguments |
89
+ | `POST_ARGS` | After arguments |
90
+
91
+ ## Tool Calling Formats
92
+
93
+ ### JSON_NATIVE
94
+
95
+ **Structure**: The entire tool call (function name, arguments, values) is in JSON format. Optional enclosing tags around the section.
96
+
97
+ **Detection**: Function name appears inside a JSON structure (quotes preceded by `{` or `:`).
98
+
99
+ **Examples**:
100
+
101
+ Standard OpenAI-style:
102
+
103
+ ```json
104
+ <tool_call>
105
+ {"name": "get_weather", "arguments": {"location": "Paris", "unit": "celsius"}}
106
+ </tool_call>
107
+ ```
108
+
109
+ Mistral Nemo with array wrapper:
110
+
111
+ ```json
112
+ [TOOL_CALLS]
113
+ [{"name": "calculate", "arguments": {"expr": "2+2"}}]
114
+ ```
115
+
116
+ Function name as JSON key (Apertus style):
117
+
118
+ ```json
119
+ {"get_weather": {"location": "Paris"}}
120
+ ```
121
+
122
+ ---
123
+
124
+ ### TAG_WITH_JSON
125
+
126
+ **Structure**: Function name is outside JSON, in tag attributes or XML-style tags. Arguments are a JSON object.
127
+
128
+ **Detection**: Function name not in JSON, but argument names appear in JSON context.
129
+
130
+ **Examples**:
131
+
132
+ Functionary v3.1:
133
+
134
+ ```xml
135
+ <function=get_weather>{"location": "Paris", "unit": "celsius"}</function>
136
+ ```
137
+
138
+ MiniMax:
139
+
140
+ ```xml
141
+ <minimax:tool_call>
142
+ <tool_name>calculate</tool_name>
143
+ <arguments>{"expr": "2+2"}</arguments>
144
+ </minimax:tool_call>
145
+ ```
146
+
147
+ ---
148
+
149
+ ### TAG_WITH_TAGGED
150
+
151
+ **Structure**: Both function name and argument names are in XML-style tags. String values are unquoted; non-string values are JSON-formatted.
152
+
153
+ **Detection**: Neither function name nor argument names appear in a JSON context.
154
+
155
+ **Examples**:
156
+
157
+ Qwen/Hermes XML format:
158
+
159
+ ```xml
160
+ <function=get_weather>
161
+ <param=location>Paris</param>
162
+ <param=unit>celsius</param>
163
+ </function>
164
+ ```
165
+
166
+ Mixed types:
167
+
168
+ ```xml
169
+ <function=calculate>
170
+ <param=expr>2+2</param>
171
+ <param=precision>2</param>
172
+ <param=options>{"round": true}</param>
173
+ </function>
174
+ ```
175
+
176
+ String values (`Paris`, `celsius`, `2+2`) are unquoted; `options` (object type) is JSON-formatted.
177
+
178
+ ---
179
+
180
+ ## Analysis Flow
181
+
182
+ ```text
183
+ autoparser::autoparser(tmpl)
184
+ |
185
+ |-- Phase 1: analyze_reasoning(tmpl, jinja_caps.supports_tool_calls)
186
+ | |-- R1: compare_reasoning_presence() — with/without reasoning_content field
187
+ | |-- R2: compare_thinking_enabled() — enable_thinking=false vs true
188
+ | '-- R3: compare_reasoning_scope() — reasoning+content vs reasoning+tools
189
+ | (only if supports_tool_calls)
190
+ |
191
+ |-- Phase 2: analyze_content(tmpl, reasoning)
192
+ | '-- C1: compares content-only vs tools output and content-only vs reasoning output
193
+ |
194
+ |-- Phase 3: analyze_tools(tmpl, jinja_caps, reasoning)
195
+ | (skipped entirely if !jinja_caps.supports_tool_calls)
196
+ | |
197
+ | |-- T1: analyze_tool_calls() — no tools vs with tools; classifies format
198
+ | | |-- JSON path → analyze_tool_call_format_json_native()
199
+ | | '-- tag path → analyze_tool_call_format_non_json()
200
+ | |
201
+ | (if format != NONE and format != JSON_NATIVE:)
202
+ | |
203
+ | |-- T2: check_per_call_markers() — 1 call vs 2 calls; moves section→per-call if needed
204
+ | | (only if supports_parallel_tool_calls)
205
+ | |
206
+ | |-- T3: extract_function_markers() — func_alpha vs func_beta; extracts name prefix/suffix/close
207
+ | |
208
+ | |-- T4: analyze_arguments() — (TAG_WITH_TAGGED only)
209
+ | | |-- A1: extract_argument_name_markers() — arg_name_A vs arg_name_B
210
+ | | '-- A2: extract_argument_value_markers() — value "XXXX" vs "YYYY"
211
+ | |
212
+ | |-- T5: extract_argument_separator() — 1 arg vs 2 args; finds separator between args
213
+ | |
214
+ | |-- T6: extract_args_markers() — 0 args vs 1 arg; finds args container markers
215
+ | |
216
+ | '-- T7: extract_call_id_markers() — call_id "call00001" vs "call99999"
217
+ |
218
+ '-- collect_preserved_tokens() — union of all non-empty markers
219
+ |
220
+ '-- apply workarounds() — post-hoc patches for edge-case templates
221
+ |
222
+ v
223
+ autoparser (analysis result)
224
+ |
225
+ v
226
+ autoparser::peg_generator::generate_parser(tmpl, inputs, analysis)
227
+ |-- analysis.build_parser(inputs) — builds PEG parser arena
228
+ | |-- reasoning.build_parser(ctx) — reasoning parser (mode-dependent)
229
+ | |-- content.build_parser(ctx) — content parser (mode-dependent)
230
+ | '-- tools.build_parser(ctx) — tool parser (dispatches by tool_format)
231
+ | |-- build_tool_parser_json_native()
232
+ | |-- build_tool_parser_tag_json()
233
+ | '-- build_tool_parser_tag_tagged()
234
+ |
235
+ |-- Build GBNF grammar (if tools present and trigger_marker non-empty)
236
+ '-- Set grammar_triggers from section_start or per_call_start
237
+ |
238
+ v
239
+ common_chat_params (prompt, parser, grammar, triggers, preserved_tokens)
240
+ ```
241
+
242
+ ## Entry Point
243
+
244
+ The auto-parser is invoked in [common/chat.cpp:1280-1310](common/chat.cpp#L1280-L1310) in `common_chat_templates_apply_jinja`. A few specialized templates are handled first (Ministral/Magistral Large 3, GPT-OSS with `<|channel|>`, Functionary v3.2 with `>>>all`), then the auto-parser handles everything else via `autoparser::autoparser` + `peg_generator::generate_parser`.
245
+
246
+ ## Algorithm Details
247
+
248
+ ### Core Mechanism: Differential Comparison
249
+
250
+ All analysis phases use the same factorized comparison function declared in [common/chat-auto-parser-helpers.h:68](common/chat-auto-parser-helpers.h#L68):
251
+
252
+ ```cpp
253
+ compare_variants(tmpl, params_A, params_modifier)
254
+ ```
255
+
256
+ This creates variant B by applying a modifier lambda to a copy of `params_A`, renders both through the template, and computes a `diff_split` ([common/chat-auto-parser.h:28-37](common/chat-auto-parser.h#L28-L37)):
257
+
258
+ - `prefix` — common prefix between A and B
259
+ - `suffix` — common suffix between A and B
260
+ - `left` — unique to variant A
261
+ - `right` — unique to variant B
262
+
263
+ The diff is computed via `calculate_diff_split()`, which finds the longest-common-prefix and longest-common-suffix, then iteratively moves incomplete `<...>` or `[...]` markers from the prefix/suffix into left/right until stable (tag boundary fixing).
264
+
265
+ Text is segmentized into markers and non-marker fragments using `segmentize_markers()`, which splits on `<...>` and `[...]` boundaries.
266
+
267
+ ### Phase 1: Reasoning Analysis
268
+
269
+ **R1 — `compare_reasoning_presence()`**: Compares assistant message with vs without a `reasoning_content` field.
270
+
271
+ - Searches `diff.right` (output with reasoning) for the reasoning content needle
272
+ - Uses PEG parsers to find surrounding markers:
273
+ - If both pre/post markers found in `diff.right` → `TAG_BASED`
274
+ - If both found but post marker only in the full output B → `TAG_BASED` (template forces markers; handled via prefill)
275
+ - If only post marker found → `TAG_BASED` (delimiter-style, empty start)
276
+ - Sets `reasoning.start` and `reasoning.end`
277
+
278
+ **R2 — `compare_thinking_enabled()`**: Compares `enable_thinking=false` vs `true` with a generation prompt.
279
+
280
+ - Detects template-added reasoning markers: `enable_thinking=true` appends a non-empty marker → sets `reasoning.start`, mode = `TAG_BASED`
281
+ - Handles the reverse case (`enable_thinking=false` appends the marker instead): extracts both start (from the preceding segment) and end markers; mode = `TAG_BASED`
282
+ - The reasoning prefill (markers added by the template) is later extracted in `common_chat_templates_apply_jinja` and prepended to model output before parsing
283
+
284
+ **R3 — `compare_reasoning_scope()`**: Compares assistant message with reasoning+text-content vs reasoning+tool-calls.
285
+
286
+ - Only runs if `jinja_caps.supports_tool_calls`
287
+ - Detects `TOOLS_ONLY`: reasoning content present in B (with tools) but not in A (with text content)
288
+ - Extracts reasoning markers from the tool call output using PEG parsers
289
+
290
+ ### Phase 2: Content Analysis
291
+
292
+ **C1**: Two comparisons in the `analyze_content` constructor:
293
+
294
+ - Comparison 1: content-only output vs tool-call output → `diff_tools`
295
+ - Comparison 2: content-only output vs reasoning+empty-content output → `diff_reasoning`
296
+
297
+ Classification logic:
298
+
299
+ - `PLAIN`: `diff_tools.left` equals the response string (content is the entire diff, no wrapper)
300
+ - `ALWAYS_WRAPPED`: markers found surrounding the content text in `pure_content` → extracts `start`/`end`
301
+
302
+ ### Phase 3: Tool Call Analysis
303
+
304
+ **T1 — `analyze_tool_calls()`**: Compares no-tools vs with-tools output.
305
+
306
+ - Extracts the tool call section as `diff.right`
307
+ - Calls `analyze_tool_call_format()` which first strips reasoning markers from the haystack, then:
308
+ - Calls `in_json_haystack()` for both function name and argument name needles
309
+ - `in_json_haystack()` uses a PEG parser to check whether the needle appears in a JSON context (preceded by `{` or `:` with surrounding quotes)
310
+ - If function name is in JSON → `JSON_NATIVE` → `analyze_tool_call_format_json_native()`
311
+ - If function name not in JSON, arg name is in JSON → `TAG_WITH_JSON`
312
+ - If neither in JSON → `TAG_WITH_TAGGED`
313
+ - `analyze_tool_call_format_json_native()`: parses the JSON object, matches field values to needles to populate `name_field`, `args_field`, `id_field`, `gen_id_field`; detects `tools_array_wrapped`; extracts `section_start`/`section_end`
314
+ - `analyze_tool_call_format_non_json()`: uses PEG parsers on the haystack to find up to two opening markers (section + per-call) then up to two closing markers
315
+
316
+ **T2 — `check_per_call_markers()`**: Compares 1 call vs 2 calls.
317
+
318
+ - Computes a secondary diff of the second call portion vs the common suffix
319
+ - If the second call content starts with `section_start` → the section marker is actually per-call → moves `section_start/end` to `per_call_start/end` and clears the section markers
320
+
321
+ **T3 — `extract_function_markers()`**: Compares function name `FUN_FIRST` vs `FUN_SECOND` (two different named functions).
322
+
323
+ - Finds where the function name appears in `diff.left`
324
+ - Extracts `function.name_prefix` from the common prefix up to the function marker, and `function.name_suffix` from after the name up to the next marker
325
+ - Extends `name_suffix` into `diff.suffix` (to the first marker for TAG_WITH_TAGGED; to the first `{` or `[` for TAG_WITH_JSON)
326
+ - Extracts `function.close` from after the last argument value up to the per-call/section end marker
327
+
328
+ **T4 — `analyze_arguments()`** (TAG_WITH_TAGGED only):
329
+
330
+ - **A1 `extract_argument_name_markers()`**: Compares `arg_name_A` vs `arg_name_B` (two different argument names).
331
+ - Finds shared surrounding structure → `arguments.name_prefix`, `arguments.name_suffix`
332
+ - **A2 `extract_argument_value_markers()`**: Compares argument value `"XXXX"` vs `"YYYY"` (same arg, different value).
333
+ - Finds markers surrounding the value → `arguments.value_prefix`, `arguments.value_suffix`
334
+
335
+ **T5 — `extract_argument_separator()`**: Compares 1 argument vs 2 arguments (same function).
336
+
337
+ - Uses `until_common_prefix(diff.right, ARG_FIRST, ARG_SECOND)` to find what separates the two argument blocks
338
+
339
+ **T6 — `extract_args_markers()`**: Compares 0 arguments vs 1 argument.
340
+
341
+ - Uses `until_common_prefix()` and `after_common_suffix()` with the empty and single-arg JSON strings as anchors to find container markers (`arguments.start`, `arguments.end`)
342
+
343
+ **T7 — `extract_call_id_markers()`**: Compares call IDs `"call00001"` vs `"call99999"`.
344
+
345
+ - Determines whether function name appears in `diff.prefix` or `diff.suffix` to classify position:
346
+ - Function name in prefix only → `BETWEEN_FUNC_AND_ARGS` or `POST_ARGS` (further distinguished by where `{` appears)
347
+ - Function name in suffix only → `PRE_FUNC_NAME`
348
+ - Extracts `call_id.prefix` and `call_id.suffix` markers around the call ID value
349
+ - Clears `per_call_end` if it incorrectly incorporated the call ID suffix
350
+
351
+ ### Workarounds
352
+
353
+ A workaround array in `common/chat-diff-analyzer.cpp` applies post-hoc patches after analysis. Each workaround is a lambda that inspects the template source and overrides analysis results. Current workarounds:
354
+
355
+ 1. **Old Qwen/DeepSeek thinking templates** — source contains `content.split('</think>')` but not `<SPECIAL_12>`: sets `reasoning.mode = TAG_BASED` with `<think>`/`</think>` markers if no reasoning was detected
356
+ 2. **Granite 3.3** — source contains specific "Write your thoughts" text: forces `TAG_BASED` reasoning with `<think>`/`</think>` and `WRAPPED_WITH_REASONING` content with `<response>`/`</response>`
357
+ 3. **Cohere Command R+** — source contains `<|CHATBOT_TOKEN|>`: sets `ALWAYS_WRAPPED` content mode if no content start is already set
358
+ 4. **Functionary 3.1** — source contains `set has_code_interpreter`: forces `PLAIN` content, specific `per_call_start/end`, clears preserved tokens to only keep Functionary-specific markers
359
+ 5. **DeepSeek-R1-Distill-Qwen** — source contains `tool▁calls▁begin` markers: overrides tool section/per-call markers with the correct Unicode block characters
360
+
361
+ ### Parser Building
362
+
363
+ Each analyzer struct (`analyze_reasoning`, `analyze_content`, `analyze_tools`) implements `build_parser(parser_build_context&)`. They share a `parser_build_context` that carries the PEG builder, inference inputs, the pre-built reasoning parser, and a pointer to the content analyzer.
364
+
365
+ #### Reasoning Parser (`analyze_reasoning::build_parser`)
366
+
367
+ | Mode | Parser |
368
+ |-----------------------------------------------|---------------------------------------------------------------------------|
369
+ | Not extracting reasoning | `eps()` |
370
+ | `TAG_BASED` or `TOOLS_ONLY` (non-empty start) | `optional(start + reasoning(until(end)) + end + space())` |
371
+ | `TAG_BASED` or `TOOLS_ONLY` (empty start) | `optional(reasoning(until(end)) + end + space())` — delimiter-style |
372
+
373
+ Note: The start marker may be empty either because the analyzer detected delimiter-style reasoning, or because `generate_parser()` cleared a template artifact start marker (see Generation Prompt & Reasoning Prefill above). Whitespace-only reasoning content (e.g. from a `<think></think>` prefill) is discarded by the mapper.
374
+
375
+ #### Content Parser (`analyze_content::build_parser`)
376
+
377
+ | Condition | Parser |
378
+ |----------------------------------------|---------------------------------------------------------------------------------|
379
+ | `json_schema` present | `reasoning + space() + content(schema(json(), "response-format", ...)) + end()` |
380
+ | Tools present | Dispatches to `analyze_tools::build_parser()` |
381
+ | `ALWAYS_WRAPPED` with reasoning | `reasoning + start + content(until(end)) + end + end()` |
382
+ | `ALWAYS_WRAPPED` without reasoning | `content(until(start)) + start + content(until(end)) + end + end()` |
383
+ | Default (PLAIN) | `reasoning + content(rest()) + end()` |
384
+
385
+ #### Tool Parsers (`analyze_tools::build_parser`)
386
+
387
+ Dispatches by `format.mode`:
388
+
389
+ **`build_tool_parser_json_native()`**: Calls `p.standard_json_tools()` which internally dispatches to:
390
+
391
+ - `build_json_tools_function_is_key()` — function name is the JSON key: `{"get_weather": {...}}`
392
+ - `build_json_tools_nested_keys()` — nested: `{"function": {"name": "X", "arguments": {...}}}`
393
+ - `build_json_tools_flat_keys()` — flat: `{"name": "X", "arguments": {...}}`
394
+
395
+ Handles content wrappers, array wrapping (`tools_array_wrapped`), parallel calls, and `parameter_order`.
396
+
397
+ **`build_tool_parser_tag_json()`**: For each tool function:
398
+
399
+ ```text
400
+ tool_open(name_prefix + tool_name(literal(name)) + name_suffix) +
401
+ call_id_section +
402
+ tool_args(schema(json(), tool_schema))
403
+ [+ function.close if non-empty]
404
+ ```
405
+
406
+ Wrapped in per-call markers (with optional parallel call repetition) then optionally in section markers.
407
+
408
+ **`build_tool_parser_tag_tagged()`**: For each tool function, builds one parser per argument:
409
+
410
+ - String types: `tool_arg_string_value(schema(until(value_suffix), ...))`
411
+ - JSON types: `tool_arg_json_value(schema(json(), ...))`
412
+ - Required args are plain; optional args wrapped in `optional()`
413
+ - Arguments joined with `space()` between consecutive parsers
414
+
415
+ For closing: uses `function.close` if present; otherwise uses `peek(per_call_end)` to avoid premature close during partial streaming; falls back to `tool_close(space())` to trigger mapper callbacks.
416
+
417
+ All three tool parsers return:
418
+
419
+ ```text
420
+ reasoning + optional(content(until(trigger_marker))) + tool_calls + end()
421
+ ```
422
+
423
+ Each returned parser is wrapped by `wrap_for_generation_prompt()`, which prepends a literal for any boilerplate prefix of the generation prompt (the portion before the reasoning start marker).
424
+
425
+ ## Mapper
426
+
427
+ `common_chat_peg_mapper` maps PEG parse results (AST nodes) into `common_chat_msg` structures. Key design:
428
+
429
+ - **Buffered arguments**: Before `tool_name` is known, argument text goes to `args_buffer`; once the name is set, the buffer is flushed to `current_tool->arguments`
430
+ - **`args_target()`**: Returns a reference to whichever destination is currently active (buffer or tool args), eliminating branching
431
+ - **`closing_quote_pending`**: Tracks whether a closing `"` needs to be appended when a string argument value is finalized (for schema-declared string types in tagged format)
432
+ - **Whitespace-only reasoning**: Reasoning content that consists entirely of whitespace (e.g. from a `<think></think>` prefill) is cleared so the message shows no reasoning
433
+ - **Brace auto-closing**: At tool close, unclosed `{` braces are closed automatically
434
+
435
+ ## Files
436
+
437
+ | File | Purpose |
438
+ |-------------------------------------------|---------------------------------------------------------------------------------|
439
+ | `common/chat-auto-parser.h` | All analysis structs, enums, `autoparser`, `peg_generator`, `generation_params` |
440
+ | `common/chat-auto-parser-generator.cpp` | Parser generator: `generate_parser()` and `build_parser()` methods |
441
+ | `common/chat-diff-analyzer.cpp` | Differential analysis implementation and workarounds |
442
+ | `common/chat-auto-parser-helpers.h/cpp` | `calculate_diff_split()`, `segmentize_markers()`, `compare_variants()`, |
443
+ | | `wrap_for_generation_prompt()`, string helpers |
444
+ | `common/chat-peg-parser.h/cpp` | `common_chat_peg_builder`, `common_chat_peg_mapper`, and helpers |
445
+ | `common/chat.cpp` | Entry point: `common_chat_templates_apply_jinja()` |
446
+ | `tools/parser/debug-template-parser.cpp` | Debug tool for template analysis |
447
+ | `tools/parser/template-analysis.cpp` | Template analysis tool |
448
+
449
+ ## Testing & Debugging
450
+
451
+ ### Debug Tools
452
+
453
+ **Template Debugger**: `tools/parser/debug-template-parser.cpp`
454
+
455
+ - Usage: `./bin/llama-debug-template-parser path/to/template.jinja`
456
+ - Shows detected format, markers, generated parser, and GBNF grammar
457
+
458
+ **Template Analysis**: `tools/parser/template-analysis.cpp`
459
+
460
+ - Usage: `./bin/llama-template-analysis path/to/template.jinja`
461
+
462
+ **Debug Logging**: Enable with `LLAMA_ARG_LOG_VERBOSITY=2`
463
+
464
+ - Shows detailed analysis steps, pattern extraction results, and generated parser structure
465
+
466
+ **PEG Test Builder**: Fluent API for creating test cases — see [tests/test-chat.cpp:947-1043](tests/test-chat.cpp#L947-L1043). Example usage:
467
+
468
+ ```cpp
469
+ auto tst = peg_tester("models/templates/Template.jinja");
470
+ tst.test("input text")
471
+ .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
472
+ .tools({tool_json})
473
+ .parallel_tool_calls(true)
474
+ .enable_thinking(true)
475
+ .expect(expected_message)
476
+ .run();
477
+ ```
478
+
479
+ ### Tested Templates
480
+
481
+ The following templates have active tests in `tests/test-chat.cpp`:
482
+
483
+ | Template | Format | Notes |
484
+ | -------- | ------ | ----- |
485
+ | Ministral-3-14B-Reasoning | Reasoning | `[THINK]...[/THINK]` tags (specialized handler) |
486
+ | NVIDIA-Nemotron-3-Nano-30B | TAG_WITH_TAGGED | Reasoning + tools |
487
+ | CohereForAI Command-R7B | JSON_NATIVE | `<\|START_THINKING\|>`/`<\|START_RESPONSE\|>` markers |
488
+ | Google Gemma 2 2B | Content only | No tool support |
489
+ | Qwen-QwQ-32B | Reasoning | Forced-open thinking |
490
+ | NousResearch Hermes 2 Pro | JSON_NATIVE | `<tool_call>` wrapper |
491
+ | IBM Granite 3.3 | JSON_NATIVE | `<think></think>` + `<response></response>` |
492
+ | IBM Granite 4.0 | JSON_NATIVE | `<tool_call>` wrapper (same template used by 4.1) |
493
+ | ByteDance Seed-OSS | TAG_WITH_TAGGED | Custom `<seed:think>` and `<seed:tool_call>` tags |
494
+ | Qwen3-Coder | TAG_WITH_TAGGED | XML-style tool format |
495
+ | DeepSeek V3.1 | JSON_NATIVE | Forced thinking mode |
496
+ | GLM-4.6 | TAG_WITH_TAGGED | `<tool_call>name\n<arg_key>...<arg_value>...` format |
497
+ | GLM-4.7-Flash | TAG_WITH_TAGGED | Updated GLM format |
498
+ | Kimi-K2-Thinking | JSON_NATIVE | Reasoning + JSON tools |
499
+ | Apertus-8B-Instruct | JSON_NATIVE | Function name as JSON key |
500
+ | MiniMax-M2 | TAG_WITH_JSON | XML invoke with JSON args |
501
+ | NVIDIA-Nemotron-Nano-v2 | JSON_NATIVE | `<TOOLCALL>` wrapper (nested) |
502
+ | CohereForAI Command-R Plus | JSON_NATIVE | Markdown code block format |
503
+ | Mistral-Nemo-Instruct-2407 | JSON_NATIVE | `[TOOL_CALLS]` wrapper with ID field |
504
+ | Functionary v3.1 | TAG_WITH_JSON | `<function=X>` format |
505
+ | Functionary v3.2 | Specialized | `>>>` recipient delimiter (dedicated handler) |
506
+ | Fireworks Firefunction v2 | TAG_WITH_JSON | Fireworks tool format |
507
+ | DeepSeek R1 Distill (Llama/Qwen) | Reasoning | Forced-open thinking |
508
+ | llama-cpp-deepseek-r1 | Reasoning | Forced-open thinking |
509
+ | Kimi-K2 / Kimi-K2-Instruct | JSON_NATIVE | JSON tools with special markers |
510
+ | Llama 3.1/3.2/3.3 | JSON_NATIVE | Standard Llama tool format |
511
+ | OpenAI GPT-OSS | Specialized | Channel-based (dedicated handler) |
512
+ | Apriel 1.5 | JSON_NATIVE | `<tool_calls>` wrapper with JSON array |
513
+ | Apriel 1.6 Thinker | Reasoning | Implicit reasoning start |
514
+ | Mistral Small 3.2 | JSON_NATIVE | `[TOOL_CALLS]func[ARGS]{...}` with call ID |
515
+ | Devstral | JSON_NATIVE | `[TOOL_CALLS]func[ARGS]{...}` without call ID |
516
+ | StepFun 3.5 Flash | TAG_WITH_TAGGED | `<function=X><parameter=Y>` format |
517
+
518
+ ## Adding Support for New Templates
519
+
520
+ To support a new template format:
521
+
522
+ 1. **If it follows standard patterns** — The auto-parser should detect it automatically. Run `llama-debug-template-parser` to verify markers are correctly extracted.
523
+ 2. **If differential analysis extracts incorrect markers** — Add a workaround lambda to the `workarounds` vector in `common/chat-diff-analyzer.cpp`. Inspect the template source for a unique identifying substring.
524
+ 3. **If it needs fundamentally different handling** — Add a dedicated handler function in `chat.cpp` before the auto-parser block (as done for GPT-OSS, Functionary v3.2, and Ministral).
525
+
526
+ ## Edge Cases and Quirks
527
+
528
+ 1. **Generation Prompt & Reasoning Prefill**: The generation prompt is extracted by diffing `add_generation_prompt=false` vs `true` in `common_chat_templates_apply_jinja`, so it contains exactly what the template appends — avoiding false positives from prior conversation turns.
529
+ 2. **Per-Call vs Per-Section Markers**: Some templates wrap each tool call individually (`per_call_start/end`); others wrap the entire section (`section_start/end`). T2 (`check_per_call_markers()`) disambiguates by checking if the second call in a two-call output starts with the section marker.
530
+ 3. **Tag Boundary Fixing**: `calculate_diff_split()` iteratively adjusts prefix/suffix boundaries to avoid splitting `<tag>` or `[marker]` tokens, ensuring clean extraction.
531
+ 4. **Call ID Side Effects**: When a call ID is detected, `per_call_end` may have been incorrectly set to include the call ID suffix. T7 clears `per_call_end` in this case.
532
+ 5. **Tool Analysis Gating**: `analyze_tools` is only constructed (and all tool analysis phases run) when `jinja_caps.supports_tool_calls` is true. Within tool analysis, `check_per_call_markers()` (T2) only runs if `jinja_caps.supports_parallel_tool_calls`.
533
+ 6. **`analyze_arguments()` Gating**: Within tool analysis, A1 and A2 (argument name/value marker extraction) only run for `TAG_WITH_TAGGED` format. `extract_argument_separator()` and `extract_args_markers()` run for all non-`JSON_NATIVE` formats.
534
+ 7. **Undetected Tool Format**: If `analyze_tools` concludes tool calling is supported but cannot determine the format, `build_parser()` logs an error and returns `eps()` (graceful degradation) rather than aborting.
llama.cpp/docs/backend/BLIS.md ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ BLIS Installation Manual
2
+ ------------------------
3
+
4
+ BLIS is a portable software framework for high-performance BLAS-like dense linear algebra libraries. It has received awards and recognition, including the 2023 James H. Wilkinson Prize for Numerical Software and the 2020 SIAM Activity Group on Supercomputing Best Paper Prize. BLIS provides a new BLAS-like API and a compatibility layer for traditional BLAS routine calls. It offers features such as object-based API, typed API, BLAS and CBLAS compatibility layers.
5
+
6
+ Project URL: https://github.com/flame/blis
7
+
8
+ ### Prepare:
9
+
10
+ Compile BLIS:
11
+
12
+ ```bash
13
+ git clone https://github.com/flame/blis
14
+ cd blis
15
+ ./configure --enable-cblas -t openmp,pthreads auto
16
+ # will install to /usr/local/ by default.
17
+ make -j
18
+ ```
19
+
20
+ Install BLIS:
21
+
22
+ ```bash
23
+ sudo make install
24
+ ```
25
+
26
+ We recommend using openmp since it's easier to modify the cores being used.
27
+
28
+ ### llama.cpp compilation
29
+
30
+ CMake:
31
+
32
+ ```bash
33
+ mkdir build
34
+ cd build
35
+ cmake -DGGML_BLAS=ON -DGGML_BLAS_VENDOR=FLAME ..
36
+ make -j
37
+ ```
38
+
39
+ ### llama.cpp execution
40
+
41
+ According to the BLIS documentation, we could set the following
42
+ environment variables to modify the behavior of openmp:
43
+
44
+ ```bash
45
+ export GOMP_CPU_AFFINITY="0-19"
46
+ export BLIS_NUM_THREADS=14
47
+ ```
48
+
49
+ And then run the binaries as normal.
50
+
51
+
52
+ ### Intel specific issue
53
+
54
+ Some might get the error message saying that `libimf.so` cannot be found.
55
+ Please follow this [stackoverflow page](https://stackoverflow.com/questions/70687930/intel-oneapi-2022-libimf-so-no-such-file-or-directory-during-openmpi-compila).
56
+
57
+ ### Reference:
58
+
59
+ 1. https://github.com/flame/blis#getting-started
60
+ 2. https://github.com/flame/blis/blob/master/docs/Multithreading.md
llama.cpp/docs/backend/CANN.md ADDED
@@ -0,0 +1,357 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # llama.cpp for CANN
2
+
3
+ - [Background](#background)
4
+ - [News](#news)
5
+ - [OS](#os)
6
+ - [Hardware](#hardware)
7
+ - [Model Supports](#model-supports)
8
+ - [DataType Supports](#datatype-supports)
9
+ - [Docker](#docker)
10
+ - [Linux](#linux)
11
+ - [Environment variable setup](#environment-variable-setup)
12
+ - [TODO](#todo)
13
+
14
+
15
+ ## Background
16
+
17
+ **Ascend NPU** is a range of AI processors using Neural Processing Unit. It will efficiently handle matrix-matrix multiplication, dot-product and scalars.
18
+
19
+ **CANN** (Compute Architecture for Neural Networks) is a heterogeneous computing architecture for AI scenarios, providing support for multiple AI frameworks on the top and serving AI processors and programming at the bottom. It plays a crucial role in bridging the gap between upper and lower layers, and is a key platform for improving the computing efficiency of Ascend AI processors. Meanwhile, it offers a highly efficient and easy-to-use programming interface for diverse application scenarios, allowing users to rapidly build AI applications and services based on the Ascend platform.
20
+
21
+ **Llama.cpp + CANN**
22
+
23
+ The llama.cpp CANN backend is designed to support Ascend NPU. It utilize the ability of AscendC and ACLNN which are integrated to CANN Toolkit and kernels to using Ascend NPU directly.
24
+
25
+ ## News
26
+
27
+ - 2024.11
28
+ - Support F16 and F32 data type model for Ascend 310P NPU.
29
+ - 2024.8
30
+ - Support `Q4_0` and `Q8_0` data type for Ascend NPU.
31
+ - 2024.7
32
+ - Create CANN backend for Ascend NPU.
33
+
34
+ ## OS
35
+
36
+ | OS | Status | Verified |
37
+ |:-------:|:-------:|:----------------------------------------------:|
38
+ | Linux | Support | Ubuntu 22.04, OpenEuler22.03 |
39
+
40
+
41
+ ## Hardware
42
+
43
+ ### Ascend NPU
44
+
45
+ You can retrieve your Ascend device IDs using the following command:
46
+
47
+ ```sh
48
+ lspci -n | grep -Eo '19e5:d[0-9a-f]{3}' | cut -d: -f2
49
+ ```
50
+
51
+ **Devices**
52
+
53
+ | Device Id | Product Series | Product Models | Chip Model | Verified Status |
54
+ |:---------:|----------------|----------------|:----------:|:---------------:|
55
+ | d803 | Atlas A3 Train | | 910C | |
56
+ | d803 | Atlas A3 Infer | | 910C | |
57
+ | d802 | Atlas A2 Train | | 910B | |
58
+ | d802 | Atlas A2 Infer | Atlas 300I A2 | 910B | Support |
59
+ | d801 | Atlas Train | | 910 | |
60
+ | d500 | Atlas Infer | Atlas 300I Duo | 310P | Support |
61
+
62
+ *Notes:*
63
+
64
+ - If you have trouble with Ascend NPU device, please create a issue with **[CANN]** prefix/tag.
65
+ - If you run successfully with your Ascend NPU device, please help update the upper table.
66
+
67
+
68
+ ## Model Supports
69
+
70
+ <details>
71
+ <summary>Text-only</summary>
72
+
73
+ | Model Name | FP16 | Q4_0 | Q8_0 |
74
+ |:----------------------------|:-----:|:----:|:----:|
75
+ | Llama-2 | √ | √ | √ |
76
+ | Llama-3 | √ | √ | √ |
77
+ | Mistral-7B | √ | √ | √ |
78
+ | Mistral MOE | √ | √ | √ |
79
+ | DBRX | - | - | - |
80
+ | Falcon | √ | √ | √ |
81
+ | Chinese LLaMA/Alpaca | √ | √ | √ |
82
+ | Vigogne(French) | √ | √ | √ |
83
+ | BERT | x | x | x |
84
+ | Koala | √ | √ | √ |
85
+ | Baichuan | √ | √ | √ |
86
+ | Aquila 1 & 2 | √ | √ | √ |
87
+ | Starcoder models | √ | √ | √ |
88
+ | Refact | √ | √ | √ |
89
+ | MPT | √ | √ | √ |
90
+ | Bloom | √ | √ | √ |
91
+ | Yi models | √ | √ | √ |
92
+ | stablelm models | √ | √ | √ |
93
+ | DeepSeek models | x | x | x |
94
+ | Qwen models | √ | √ | √ |
95
+ | PLaMo-13B | √ | √ | √ |
96
+ | Phi models | √ | √ | √ |
97
+ | PhiMoE | √ | √ | √ |
98
+ | GPT-2 | √ | √ | √ |
99
+ | Orion | √ | √ | √ |
100
+ | InternlLM2 | √ | √ | √ |
101
+ | CodeShell | √ | √ | √ |
102
+ | Gemma | √ | √ | √ |
103
+ | Mamba | √ | √ | √ |
104
+ | Xverse | √ | √ | √ |
105
+ | command-r models | √ | √ | √ |
106
+ | Grok-1 | - | - | - |
107
+ | SEA-LION | √ | √ | √ |
108
+ | GritLM-7B | √ | √ | √ |
109
+ | OLMo | √ | √ | √ |
110
+ | OLMo 2 | √ | �� | √ |
111
+ | OLMoE | √ | √ | √ |
112
+ | Granite models | √ | √ | √ |
113
+ | GPT-NeoX | √ | √ | √ |
114
+ | Pythia | √ | √ | √ |
115
+ | Snowflake-Arctic MoE | - | - | - |
116
+ | Smaug | √ | √ | √ |
117
+ | Poro 34B | √ | √ | √ |
118
+ | Bitnet b1.58 models | √ | x | x |
119
+ | Flan-T5 | √ | √ | √ |
120
+ | Open Elm models | x | √ | √ |
121
+ | chatGLM3-6B + ChatGLM4-9b + GLMEdge-1.5b + GLMEdge-4b | √ | √ | √ |
122
+ | GLM-4-0414 | √ | √ | √ |
123
+ | SmolLM | √ | √ | √ |
124
+ | EXAONE-3.0-7.8B-Instruct | √ | √ | √ |
125
+ | FalconMamba Models | √ | √ | √ |
126
+ | Jais Models | - | x | x |
127
+ | Bielik-11B-v2.3 | √ | √ | √ |
128
+ | RWKV-6 | - | √ | √ |
129
+ | QRWKV-6 | √ | √ | √ |
130
+ | GigaChat-20B-A3B | x | x | x |
131
+ | Trillion-7B-preview | √ | √ | √ |
132
+ | Ling models | √ | √ | √ |
133
+
134
+ </details>
135
+
136
+ <details>
137
+ <summary>Multimodal</summary>
138
+
139
+ | Model Name | FP16 | Q4_0 | Q8_0 |
140
+ |:----------------------------|:-----:|:----:|:----:|
141
+ | LLaVA 1.5 models, LLaVA 1.6 models | x | x | x |
142
+ | BakLLaVA | √ | √ | √ |
143
+ | Obsidian | √ | - | - |
144
+ | ShareGPT4V | x | - | - |
145
+ | MobileVLM 1.7B/3B models | - | - | - |
146
+ | Yi-VL | - | - | - |
147
+ | Mini CPM | √ | √ | √ |
148
+ | Moondream | √ | √ | √ |
149
+ | Bunny | √ | - | - |
150
+ | GLM-EDGE | √ | √ | √ |
151
+ | Qwen2-VL | √ | √ | √ |
152
+
153
+ </details>
154
+
155
+
156
+
157
+ ## DataType Supports
158
+
159
+ | DataType | 910B | 310P |
160
+ |:----------------------:|:-------:|:-------:|
161
+ | FP16 | Support | Support |
162
+ | Q8_0 | Support | Partial |
163
+ | Q4_0 | Support | Partial |
164
+ | BF16 | Support | |
165
+
166
+ > **310P note**
167
+ > - `Q8_0`: data transform / buffer path is implemented, and `GET_ROWS` is supported, but quantized `MUL_MAT` / `MUL_MAT_ID` are not supported.
168
+ > - `Q4_0`: data transform / buffer path is implemented, but quantized `MUL_MAT` / `MUL_MAT_ID` are not supported.
169
+
170
+ ## Docker
171
+
172
+ ### Build Images
173
+ You can get a image with llama.cpp in one command.
174
+ ```sh
175
+ docker build -t llama-cpp-cann -f .devops/llama-cli-cann.Dockerfile .
176
+ ```
177
+
178
+ ### Run container
179
+
180
+ ```sh
181
+ # Find all cards.
182
+ npu-smi info
183
+
184
+ # Select the cards that you want to use, make sure these cards are not used by someone.
185
+ # Following using cards of device0.
186
+ docker run --name llamacpp \
187
+ --device /dev/davinci0 \
188
+ --device /dev/davinci_manager \
189
+ --device /dev/devmm_svm \
190
+ --device /dev/hisi_hdc \
191
+ -v /usr/local/dcmi:/usr/local/dcmi \
192
+ -v /usr/local/bin/npu-smi:/usr/local/bin/npu-smi \
193
+ -v /usr/local/Ascend/driver/lib64/:/usr/local/Ascend/driver/lib64/ \
194
+ -v /usr/local/Ascend/driver/version.info:/usr/local/Ascend/driver/version.info \
195
+ -v /PATH_TO_YOUR_MODELS/:/app/models \
196
+ -it llama-cpp-cann \
197
+ -m /app/models/MODEL_PATH \
198
+ -ngl 32 \
199
+ -p "Building a website can be done in 10 simple steps:"
200
+ ```
201
+
202
+ *Notes:*
203
+
204
+ - You may need to install Ascend Driver and firmware on the **host** machine *(Please refer to the [Linux configuration](#linux) for details)*.
205
+
206
+ ## Linux
207
+
208
+ ### I. Setup Environment
209
+
210
+ 1. **Configure Ascend user and group**
211
+
212
+ ```sh
213
+ sudo groupadd HwHiAiUser
214
+ sudo useradd -g HwHiAiUser -d /home/HwHiAiUser -m HwHiAiUser -s /bin/bash
215
+ sudo usermod -aG HwHiAiUser $USER
216
+ ```
217
+
218
+ 2. **Install dependencies**
219
+
220
+ **Ubuntu/Debian:**
221
+ ```sh
222
+ sudo apt-get update
223
+ sudo apt-get install -y gcc python3 python3-pip linux-headers-$(uname -r)
224
+ ```
225
+
226
+ **RHEL/CentOS:**
227
+ ```sh
228
+ sudo yum makecache
229
+ sudo yum install -y gcc python3 python3-pip kernel-headers-$(uname -r) kernel-devel-$(uname -r)
230
+ ```
231
+
232
+ 3. **Install CANN (driver + toolkit)**
233
+
234
+ > The `Ascend-cann` package includes both the driver and toolkit.
235
+ > `$ARCH` can be `x86_64` or `aarch64`, `$CHIP` can be `910b` or `310p`.
236
+
237
+ ```sh
238
+ wget https://ascend-repo.obs.cn-east-2.myhuaweicloud.com/CANN/CANN%208.5.T63/Ascend-cann_8.5.0_linux-$ARCH.run
239
+ sudo bash ./Ascend-cann_8.5.0_linux-$ARCH.run --install
240
+
241
+ wget https://ascend-repo.obs.cn-east-2.myhuaweicloud.com/CANN/CANN%208.5.T63/Ascend-cann-$CHIP-ops_8.5.0_linux-$ARCH.run
242
+ sudo bash ./Ascend-cann-$CHIP-ops_8.5.0_linux-$ARCH.run --install
243
+ ```
244
+
245
+ 4. **Verify installation**
246
+
247
+ ```sh
248
+ npu-smi info
249
+ ```
250
+
251
+ If device information is displayed correctly, the driver is functioning properly.
252
+
253
+ ```sh
254
+ # Set environment variables (adjust path if needed)
255
+ source /usr/local/Ascend/cann/set_env.sh
256
+
257
+ python3 -c "import acl; print(acl.get_soc_name())"
258
+ ```
259
+
260
+ If the command outputs the chip model, the installation was successful.
261
+
262
+ ### II. Build llama.cpp
263
+
264
+ ```sh
265
+ cmake -B build -DGGML_CANN=on -DCMAKE_BUILD_TYPE=release
266
+ cmake --build build --config release
267
+ ```
268
+
269
+ ### III. Run the inference
270
+
271
+ 1. **Retrieve and prepare model**
272
+
273
+ You can refer to the general [*Obtaining and quantizing models*](../../README.md#obtaining-and-quantizing-models) guide for model prepration.
274
+
275
+ **Notes**:
276
+
277
+ - CANN backend only supports FP16/Q4_0/Q8_0 models currently.
278
+
279
+ 2. **Launch inference**
280
+
281
+ There are two device selection modes:
282
+
283
+ - Single device: Use one device target specified by the user.
284
+ - Multiple devices: Automatically choose the devices with the same backend.
285
+
286
+ | Device selection | Parameter |
287
+ |:----------------:|:--------------------------------------:|
288
+ | Single device | --split-mode none --main-gpu DEVICE_ID |
289
+ | Multiple devices | --split-mode layer (default) |
290
+
291
+ Examples:
292
+
293
+ - Use device 0:
294
+
295
+ ```sh
296
+ ./build/bin/llama-cli -m path_to_model -p "Building a website can be done in 10 simple steps:" -n 400 -e -ngl 33 -sm none -mg 0
297
+ ```
298
+
299
+ - Use multiple devices:
300
+
301
+ ```sh
302
+ ./build/bin/llama-cli -m path_to_model -p "Building a website can be done in 10 simple steps:" -n 400 -e -ngl 33 -sm layer
303
+ ```
304
+
305
+ ### **GitHub contribution**:
306
+ Please add the **[CANN]** prefix/tag in issues/PRs titles to help the CANN-team check/address them without delay.
307
+
308
+ ## Updates
309
+ ### Basic Flash Attention Support
310
+ The basic FA kernel with aclnnops has been added in aclnn_ops.cpp.
311
+ Currently, the FA only supports the cases with FP16 KV tensors and NO logit softcap.
312
+ Since the aclnn interface for flash attention cannot support the logit softcap, we will only update the quantized version in the future.
313
+
314
+ Authors from Peking University: Bizhao Shi (bshi@pku.edu.cn), Yuxin Yang (yxyang@pku.edu.cn), Ruiyang Ma (ruiyang@stu.pku.edu.cn), and Guojie Luo (gluo@pku.edu.cn).
315
+
316
+ We would like to thank Tuo Dai, Shanni Li, and all of the project maintainers from Huawei Technologies Co., Ltd for their help during the code development and pull request.
317
+
318
+ ## Environment variable setup
319
+
320
+ ### GGML_CANN_MEM_POOL
321
+
322
+ Specifies the memory pool management strategy, Default is vmm.
323
+
324
+ - vmm: Utilizes a virtual memory manager pool. If hardware support for VMM is unavailable, falls back to the legacy (leg) memory pool.
325
+
326
+ - prio: Employs a priority queue-based memory pool management.
327
+
328
+ - leg: Uses a fixed-size buffer pool.
329
+
330
+ ### GGML_CANN_DISABLE_BUF_POOL_CLEAN
331
+
332
+ Controls automatic cleanup of the memory pool. This option is only effective when using the prio or leg memory pool strategies.
333
+
334
+ ### GGML_CANN_WEIGHT_NZ
335
+
336
+ Converting the matmul weight format from ND to NZ to improve performance. Enabled by default.
337
+
338
+ ### GGML_CANN_ACL_GRAPH
339
+
340
+ Operators are executed using ACL graph execution, rather than in op-by-op (eager) mode. Enabled by default. This option is only effective if `USE_ACL_GRAPH` was enabled at compilation time. To enable it, recompile using:
341
+
342
+ ```sh
343
+ cmake -B build -DGGML_CANN=on -DCMAKE_BUILD_TYPE=release -DUSE_ACL_GRAPH=ON
344
+ cmake --build build --config release
345
+ ```
346
+
347
+ ### GGML_CANN_GRAPH_CACHE_CAPACITY
348
+
349
+ Maximum number of compiled CANN graphs kept in the LRU cache, default is 12. When the number of cached graphs exceeds this capacity, the least recently used graph will be evicted.
350
+
351
+ ### GGML_CANN_PREFILL_USE_GRAPH
352
+
353
+ Enable ACL graph execution during the prefill stage, default is false. This option is only effective when FA is enabled.
354
+
355
+ ### GGML_CANN_OPERATOR_FUSION
356
+
357
+ Enable operator fusion during computation, default is false. This option fuses compatible operators (e.g., ADD + RMS_NORM) to reduce overhead and improve performance.
llama.cpp/docs/backend/CUDA-FEDORA.md ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Setting Up CUDA on Fedora
2
+
3
+ In this guide we setup [Nvidia CUDA](https://docs.nvidia.com/cuda/) in a toolbox container. This guide is applicable for:
4
+
5
+ - [Fedora Workstation](https://fedoraproject.org/workstation/)
6
+ - [Atomic Desktops for Fedora](https://fedoraproject.org/atomic-desktops/)
7
+ - [Fedora Spins](https://fedoraproject.org/spins)
8
+ - [Other Distributions](https://containertoolbx.org/distros/), including `Red Hat Enterprise Linux >= 8.5`, `Arch Linux`, and `Ubuntu`.
9
+
10
+ ## Table of Contents
11
+
12
+ - [Prerequisites](#prerequisites)
13
+ - [Using the Fedora 41 CUDA Repository](#using-the-fedora-41-cuda-repository)
14
+ - [Creating a Fedora Toolbox Environment](#creating-a-fedora-toolbox-environment)
15
+ - [Installing Essential Development Tools](#installing-essential-development-tools)
16
+ - [Adding the CUDA Repository](#adding-the-cuda-repository)
17
+ - [Installing Nvidia Driver Libraries](#installing-nvidia-driver-libraries)
18
+ - [Installing the CUDA Meta-Package](#installing-the-cuda-meta-package)
19
+ - [Configuring the Environment](#configuring-the-environment)
20
+ - [Verifying the Installation](#verifying-the-installation)
21
+ - [Conclusion](#conclusion)
22
+ - [Troubleshooting](#troubleshooting)
23
+ - [Additional Notes](#additional-notes)
24
+ - [References](#references)
25
+
26
+ ## Prerequisites
27
+
28
+ - **Toolbox Installed on the Host System** `Fedora Silverblue` and `Fedora Workstation` both have toolbox by default, other distributions may need to install the [toolbox package](https://containertoolbx.org/install/).
29
+ - **NVIDIA Drivers and Graphics Card installed on Host System (recommended)** To run CUDA program, such as `llama.cpp`, the host should be setup to access your NVIDIA hardware. Fedora Hosts can use the [RPM Fusion Repository](https://rpmfusion.org/Howto/NVIDIA).
30
+ - **Internet connectivity** to download packages.
31
+
32
+ ### Using the Fedora 41 CUDA Repository
33
+
34
+ The latest release is 41.
35
+
36
+ - [Fedora 41 CUDA Repository](https://developer.download.nvidia.com/compute/cuda/repos/fedora41/x86_64/)
37
+
38
+ **Note:** We recommend using a toolbox environment to prevent system conflicts.
39
+
40
+ ## Creating a Fedora Toolbox Environment
41
+
42
+ This guide focuses on Fedora hosts, but with small adjustments, it can work for other hosts. Using the Fedora Toolbox allows us to install the necessary packages without affecting the host system.
43
+
44
+ **Note:** Toolbox is available for other systems, and even without Toolbox, it is possible to use Podman or Docker.
45
+
46
+ 1. **Create a Fedora 41 Toolbox:**
47
+
48
+ ```bash
49
+ toolbox create --image registry.fedoraproject.org/fedora-toolbox:41 --container fedora-toolbox-41-cuda
50
+ ```
51
+
52
+ 2. **Enter the Toolbox:**
53
+
54
+ ```bash
55
+ toolbox enter --container fedora-toolbox-41-cuda
56
+ ```
57
+
58
+ Inside the toolbox, you have root privileges and can install packages without affecting the host system.
59
+
60
+ ## Installing Essential Development Tools
61
+
62
+ 1. **Synchronize the DNF Package Manager:**
63
+
64
+ ```bash
65
+ sudo dnf distro-sync
66
+ ```
67
+
68
+ 2. **Install **Vim** the default text editor (Optional):**
69
+
70
+ ```bash
71
+ sudo dnf install vim-default-editor --allowerasing
72
+ ```
73
+
74
+ The `--allowerasing` flag will allow the removal of the conflicting `nano-default-editor` package.
75
+
76
+ 3. **Install Development Tools and Libraries:**
77
+
78
+ ```bash
79
+ sudo dnf install @c-development @development-tools cmake
80
+ ```
81
+
82
+ This installs essential packages for compiling software, including `gcc`, `make`, and other development headers.
83
+
84
+ ## Adding the CUDA Repository
85
+
86
+ Add the NVIDIA CUDA repository to your DNF configuration:
87
+
88
+ ```bash
89
+ sudo dnf config-manager addrepo --from-repofile=https://developer.download.nvidia.com/compute/cuda/repos/fedora41/x86_64/cuda-fedora41.repo
90
+ ```
91
+
92
+ After adding the repository, synchronize the package manager again:
93
+
94
+ ```bash
95
+ sudo dnf distro-sync
96
+ ```
97
+
98
+ ## Installing Nvidia Driver Libraries
99
+
100
+ First, we need to detect if the host is supplying the [NVIDIA driver libraries into the toolbox](https://github.com/containers/toolbox/blob/main/src/pkg/nvidia/nvidia.go):
101
+
102
+ ```bash
103
+ ls -la /usr/lib64/libcuda.so.1
104
+ ```
105
+
106
+ ### If *`libcuda.so.1`* is missing:
107
+
108
+ ```
109
+ ls: cannot access '/usr/lib64/libcuda.so.1': No such file or directory
110
+ ```
111
+
112
+ **Explanation:**
113
+ The host dose not supply the CUDA drivers, **install them now:**
114
+
115
+ #### Install the Nvidia Driver Libraries on Guest:
116
+
117
+ ```bash
118
+ sudo dnf install nvidia-driver-cuda nvidia-driver-libs nvidia-driver-cuda-libs nvidia-persistenced
119
+ ```
120
+
121
+ ### If *`libcuda.so.1`* exists:
122
+ ```
123
+ lrwxrwxrwx. 1 root root 21 Mar 24 11:26 /usr/lib64/libcuda.so.1 -> libcuda.so.570.133.07
124
+ ```
125
+
126
+ **Explanation:**
127
+ The host is supply the CUDA drivers, **we need to update the guest RPM Database accordingly:**
128
+
129
+ #### Update the Toolbox RPM Database to include the Host-Supplied Libraries:
130
+
131
+ Note: we do not actually install the libraries, we just update the DB so that the guest system knows they are supplied by the host.
132
+
133
+ ##### 1. Download `nvidia-` parts that are supplied by the host RPM's (with dependencies)
134
+
135
+ ```bash
136
+ sudo dnf download --destdir=/tmp/nvidia-driver-libs --resolve --arch x86_64 nvidia-driver-cuda nvidia-driver-libs nvidia-driver-cuda-libs nvidia-persistenced
137
+ ```
138
+
139
+ ##### 2. Update the RPM database to assume the installation of these packages.
140
+
141
+ ```bash
142
+ sudo rpm --install --verbose --hash --justdb /tmp/nvidia-driver-libs/*
143
+ ```
144
+
145
+ **Note:**
146
+
147
+ - The `--justdb` option only updates the RPM database, without touching the filesystem elsewhere.
148
+
149
+ ##### Check that the RPM Database has been correctly updated:
150
+
151
+ **Note:** This is the same command as in the *"Install the Nvidia Driver Libraries on Guest"* for if *`libcuda.so.1`* was missing.
152
+
153
+
154
+ ```bash
155
+ sudo dnf install nvidia-driver-cuda nvidia-driver-libs nvidia-driver-cuda-libs nvidia-persistenced
156
+ ```
157
+
158
+ *(this time it will not install anything, as the database things that these packages are already installed)*
159
+
160
+ ```
161
+ Updating and loading repositories:
162
+ Repositories loaded.
163
+ Package "nvidia-driver-cuda-3:570.124.06-1.fc41.x86_64" is already installed.
164
+ Package "nvidia-driver-libs-3:570.124.06-1.fc41.x86_64" is already installed.
165
+ Package "nvidia-driver-cuda-libs-3:570.124.06-1.fc41.x86_64" is already installed.
166
+ Package "nvidia-persistenced-3:570.124.06-1.fc41.x86_64" is already installed.
167
+
168
+ Nothing to do.
169
+ ```
170
+
171
+ ## Installing the CUDA Meta-Package
172
+
173
+ Now that the driver libraries are installed, proceed to install CUDA:
174
+
175
+ ```bash
176
+ sudo dnf install cuda
177
+ ```
178
+
179
+ This installs the CUDA toolkit and associated packages.
180
+
181
+ ## Configuring the Environment
182
+
183
+ To use CUDA, add its binary directory to your system's `PATH`.
184
+
185
+ 1. **Create a Profile Script:**
186
+
187
+ ```bash
188
+ sudo sh -c 'echo "export PATH=\$PATH:/usr/local/cuda/bin" >> /etc/profile.d/cuda.sh'
189
+ ```
190
+
191
+ **Explanation:**
192
+
193
+ - We add to `/etc/profile.d/` as the `/etc/` folder is unique to this particular container, and is not shared with other containers or the host system.
194
+ - The backslash `\` before `$PATH` ensures the variable is correctly written into the script.
195
+
196
+ 2. **Make the Script Executable:**
197
+
198
+ ```bash
199
+ sudo chmod +x /etc/profile.d/cuda.sh
200
+ ```
201
+
202
+ 3. **Source the Script to Update Your Environment:**
203
+
204
+ ```bash
205
+ source /etc/profile.d/cuda.sh
206
+ ```
207
+
208
+ **Note:** This command updates your current shell session with the new `PATH`. The `/etc/profile.d/cuda.sh` script ensures that the CUDA binaries are available in your `PATH` for all future sessions.
209
+
210
+ ## Verifying the Installation
211
+
212
+ To confirm that CUDA is correctly installed and configured, check the version of the NVIDIA CUDA Compiler (`nvcc`):
213
+
214
+ ```bash
215
+ nvcc --version
216
+ ```
217
+
218
+ You should see output similar to:
219
+
220
+ ```
221
+ nvcc: NVIDIA (R) Cuda compiler driver
222
+ Copyright (c) 2005-2025 NVIDIA Corporation
223
+ Built on Fri_Feb_21_20:23:50_PST_2025
224
+ Cuda compilation tools, release 12.8, V12.8.93
225
+ Build cuda_12.8.r12.8/compiler.35583870_0
226
+ ```
227
+
228
+ This output confirms that the CUDA compiler is accessible and indicates the installed version.
229
+
230
+ ## Conclusion
231
+
232
+ You have successfully set up CUDA on Fedora within a toolbox environment using the Fedora 41 CUDA repository. By manually updating the RPM db and configuring the environment, you can develop CUDA applications without affecting your host system.
233
+
234
+ ## Troubleshooting
235
+
236
+ - **Installation Failures:**
237
+
238
+ - If you encounter errors during installation, carefully read the error messages. They often indicate conflicting files or missing dependencies.
239
+ - You may use the `--excludepath` option with `rpm` to exclude conflicting files during manual RPM installations.
240
+
241
+ - **Rebooting the Container:**
242
+
243
+ - Sometimes there may be a bug in the NVIDIA driver host passthrough (such as missing a shared library). Rebooting the container may solve this issue:
244
+
245
+ ```bash
246
+ # on the host system
247
+ podman container restart --all
248
+ ```
249
+
250
+ - **Environment Variables Not Set:**
251
+ - If `nvcc` is not found after installation, ensure that `/usr/local/cuda/bin` is in your `PATH`.
252
+ - Run `echo $PATH` to check if the path is included.
253
+ - Re-source the profile script or open a new terminal session.
254
+
255
+ ## Additional Notes
256
+
257
+ - **Updating CUDA in the Future:**
258
+
259
+ - Keep an eye on the official NVIDIA repositories for updates to your Fedora version.
260
+ - When an updated repository becomes available, adjust your `dnf` configuration accordingly.
261
+
262
+ - **Building `llama.cpp`:**
263
+
264
+ - With CUDA installed, you can follow these [build instructions for `llama.cpp`](https://github.com/ggml-org/llama.cpp/blob/master/docs/build.md) to compile it with CUDA support.
265
+ - Ensure that any CUDA-specific build flags or paths are correctly set in your build configuration.
266
+
267
+ - **Using the Toolbox Environment:**
268
+ - The toolbox environment is isolated from your host system, which helps prevent conflicts.
269
+ - Remember that system files and configurations inside the toolbox are separate from the host. By default the home directory of the user is shared between the host and the toolbox.
270
+
271
+ ---
272
+
273
+ **Disclaimer:** Manually installing and modifying system packages can lead to instability of the container. The above steps are provided as a guideline and may need adjustments based on your specific system configuration. Always back up important data before making significant system changes, especially as your home folder is writable and shared with the toolbox.
274
+
275
+ **Acknowledgments:** Special thanks to the Fedora community and NVIDIA documentation for providing resources that assisted in creating this guide.
276
+
277
+ ## References
278
+
279
+ - [Fedora Toolbox Documentation](https://docs.fedoraproject.org/en-US/fedora-silverblue/toolbox/)
280
+ - [NVIDIA CUDA Installation Guide](https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html)
281
+ - [Podman Documentation](https://podman.io/get-started)
282
+
283
+ ---
llama.cpp/docs/backend/OPENCL.md ADDED
@@ -0,0 +1,294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # llama.cpp for OpenCL
2
+
3
+ - [llama.cpp for OpenCL](#llamacpp-for-opencl)
4
+ - [Background](#background)
5
+ - [Llama.cpp + OpenCL](#llamacpp--opencl)
6
+ - [OS](#os)
7
+ - [Hardware](#hardware)
8
+ - [Adreno GPU](#adreno-gpu)
9
+ - [DataType Supports](#datatype-supports)
10
+ - [Model Preparation](#model-preparation)
11
+ - [Binary Kernel Library](#binary-kernel-library)
12
+ - [CMake Options](#cmake-options)
13
+ - [Android](#android)
14
+ - [I. Setup Environment](#i-setup-environment)
15
+ - [II. Build llama.cpp](#ii-build-llamacpp)
16
+ - [Windows 11 Arm64](#windows-11-arm64)
17
+ - [I. Setup Environment](#i-setup-environment-1)
18
+ - [II. Build llama.cpp](#ii-build-llamacpp-1)
19
+ - [Linux](#linux)
20
+ - [I. Setup Environment](#i-setup-environment-2)
21
+ - [II. Build llama.cpp](#ii-build-llamacpp-2)
22
+ - [Known Issues](#known-issues)
23
+ - [TODO](#todo)
24
+
25
+ ## Background
26
+
27
+ OpenCL (Open Computing Language) is an open, royalty-free standard for cross-platform, parallel programming of diverse accelerators found in supercomputers, cloud servers, personal computers, mobile devices and embedded platforms. OpenCL specifies a programming language (based on C99) for programming these devices and application programming interfaces (APIs) to control the platform and execute programs on the compute devices. Similar to CUDA, OpenCL has been widely used to program GPUs and is supported by most GPU vendors.
28
+
29
+ ### Llama.cpp + OpenCL
30
+
31
+ The llama.cpp OpenCL backend is designed to enable llama.cpp on **Qualcomm Adreno GPU** firstly via OpenCL. Thanks to the portabilty of OpenCL, the OpenCL backend can also run on certain Intel GPUs such as those that do not have [SYCL](/docs/backend/SYCL.md) support although the performance is not optimal.
32
+
33
+ ## OS
34
+
35
+ | OS | Status | Verified |
36
+ |---------|---------|------------------------------------------------|
37
+ | Android | Support | Snapdragon 8 Gen 3, Snapdragon 8 Elite |
38
+ | Windows | Support | Windows 11 Arm64 with Snapdragon X Elite |
39
+ | Linux | Support | Ubuntu 22.04 WSL2 with Intel 12700H |
40
+
41
+ ## Hardware
42
+
43
+ ### Adreno GPU
44
+
45
+ **Verified devices**
46
+
47
+ | Adreno GPU | Status |
48
+ |:-------------------------------------:|:-------:|
49
+ | Adreno 750 (Snapdragon 8 Gen 3) | Support |
50
+ | Adreno 830 (Snapdragon 8 Elite) | Support |
51
+ | Adreno 840 (Snapdragon 8 Elite Gen 5) | Support |
52
+ | Adreno X1-85 (Snapdragon X Elite) | Support |
53
+ | Adreno X2-90 (Snapdragon X2 Elite) | Support |
54
+
55
+ > A6x GPUs with a recent driver and compiler are supported; they are usually found in IoT platforms.
56
+ However, A6x GPUs in phones are likely not supported due to the outdated driver and compiler.
57
+
58
+ ## DataType Supports
59
+
60
+ | DataType | Status |
61
+ |:----------------------:|:--------------------------:|
62
+ | Q1_0 | Support |
63
+ | Q4_0 | Support |
64
+ | Q4_1 | Support |
65
+ | Q5_0 | Support |
66
+ | Q5_1 | Support |
67
+ | Q8_0 | Support |
68
+ | Q4_K | Support |
69
+ | Q5_K | Support |
70
+ | Q6_K | Support |
71
+ | MXFP4 | Support |
72
+ | IQ4_NL | Support |
73
+
74
+ ## Model Preparation
75
+
76
+ Since common quantizations are supported now, it is recommanded to download GGUF models directly from Huggingface.
77
+
78
+ ## Binary Kernel Library
79
+
80
+ A prebuilt binary kernel library has been introduced for Adreno GPUs.
81
+ It currently targets X2 GPUs (X2-90, X2-85 and X2-45) found in Snapdragon X2 SoC.
82
+ The library currently contains kernels for MUL_MAT_ID with Q4_0, Q4_1, Q4_K, MXFP4.
83
+ The library must be manually downloaded from https://softwarecenter.qualcomm.com/catalog/item/Adreno_Kernel_Library_GGML.
84
+
85
+ To allow using the kernel library, add `-DGGML_OPENCL_USE_ADRENO_BIN_KERNELS=ON` when configuring with CMake.
86
+ Then, extract `adreno-opencl-kernels.dll` from the zip file downloaded from the above URL and put it alongside the executables.
87
+ If kernels compatible with the current GPU are found in the library, they will be loaded and used.
88
+
89
+
90
+ ## CMake Options
91
+
92
+ The OpenCL backend has the following CMake options that control the behavior of the backend.
93
+
94
+ | CMake options | Default value | Description |
95
+ |:------------------------------------:|:--------------:|:------------------------------------------|
96
+ | `GGML_OPENCL_EMBED_KERNELS` | `ON` | Embed OpenCL kernels into the executable. |
97
+ | `GGML_OPENCL_USE_ADRENO_KERNELS` | `ON` | Use kernels optimized for Adreno. |
98
+ | `GGML_OPENCL_USE_ADRENO_BIN_KERNELS` | `OFF` | Allow using binary kernel lib for Adreno. |
99
+
100
+ ## Android
101
+
102
+ Ubuntu 22.04 is used for targeting Android. Make sure the following tools are accessible from command line,
103
+
104
+ * Git
105
+ * CMake 3.29
106
+ * Ninja
107
+ * Python3
108
+
109
+ ### I. Setup Environment
110
+
111
+ 1. **Install NDK**
112
+
113
+ ```sh
114
+ cd ~
115
+ wget https://dl.google.com/android/repository/commandlinetools-linux-8512546_latest.zip && \
116
+ unzip commandlinetools-linux-8512546_latest.zip && \
117
+ mkdir -p ~/android-sdk/cmdline-tools && \
118
+ mv cmdline-tools latest && \
119
+ mv latest ~/android-sdk/cmdline-tools/ && \
120
+ rm -rf commandlinetools-linux-8512546_latest.zip
121
+
122
+ yes | ~/android-sdk/cmdline-tools/latest/bin/sdkmanager "ndk;26.3.11579264"
123
+ ```
124
+
125
+ 2. **Install OpenCL Headers and Library**
126
+
127
+ ```sh
128
+ mkdir -p ~/dev/llm
129
+ cd ~/dev/llm
130
+
131
+ git clone https://github.com/KhronosGroup/OpenCL-Headers && \
132
+ cd OpenCL-Headers && \
133
+ cp -r CL ~/android-sdk/ndk/26.3.11579264/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include
134
+
135
+ cd ~/dev/llm
136
+
137
+ git clone https://github.com/KhronosGroup/OpenCL-ICD-Loader && \
138
+ cd OpenCL-ICD-Loader && \
139
+ mkdir build_ndk26 && cd build_ndk26 && \
140
+ cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release \
141
+ -DCMAKE_TOOLCHAIN_FILE=$HOME/android-sdk/ndk/26.3.11579264/build/cmake/android.toolchain.cmake \
142
+ -DOPENCL_ICD_LOADER_HEADERS_DIR=$HOME/android-sdk/ndk/26.3.11579264/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include \
143
+ -DANDROID_ABI=arm64-v8a \
144
+ -DANDROID_PLATFORM=24 \
145
+ -DANDROID_STL=c++_shared && \
146
+ ninja && \
147
+ cp libOpenCL.so ~/android-sdk/ndk/26.3.11579264/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android
148
+ ```
149
+
150
+ ### II. Build llama.cpp
151
+
152
+ ```sh
153
+ cd ~/dev/llm
154
+
155
+ git clone https://github.com/ggml-org/llama.cpp && \
156
+ cd llama.cpp && \
157
+ mkdir build-android && cd build-android
158
+
159
+ cmake .. -G Ninja \
160
+ -DCMAKE_TOOLCHAIN_FILE=$HOME/android-sdk/ndk/26.3.11579264/build/cmake/android.toolchain.cmake \
161
+ -DANDROID_ABI=arm64-v8a \
162
+ -DANDROID_PLATFORM=android-28 \
163
+ -DBUILD_SHARED_LIBS=OFF \
164
+ -DGGML_OPENCL=ON
165
+
166
+ ninja
167
+ ```
168
+
169
+ ## Windows 11 Arm64
170
+
171
+ A Snapdragon X Elite device with Windows 11 Arm64 is used. Make sure the following tools are accessible from command line,
172
+
173
+ * Git
174
+ * CMake 3.29
175
+ * Clang 19
176
+ * Ninja
177
+ * Visual Studio 2022
178
+ * Powershell 7
179
+ * Python
180
+
181
+ Visual Studio provides necessary headers and libraries although it is not directly used for building.
182
+ Alternatively, Visual Studio Build Tools can be installed instead of the full Visual Studio.
183
+
184
+ > Note that building using Visual Studio's cl compiler is not supported. Clang must be used. Clang depends on libraries provided by Visual Studio to work. Therefore, Visual Studio must be installed. Alternatively, Visual Studio Build Tools can be installed instead of the full Visual Studio.
185
+
186
+ Powershell 7 is used for the following commands.
187
+ If an older version of Powershell is used, these commands may not work as they are.
188
+
189
+ ### I. Setup Environment
190
+
191
+ 1. **Install OpenCL Headers and Library**
192
+
193
+ ```powershell
194
+ mkdir -p ~/dev/llm
195
+
196
+ cd ~/dev/llm
197
+ git clone https://github.com/KhronosGroup/OpenCL-Headers && cd OpenCL-Headers
198
+ mkdir build && cd build
199
+ cmake .. -G Ninja `
200
+ -DBUILD_TESTING=OFF `
201
+ -DOPENCL_HEADERS_BUILD_TESTING=OFF `
202
+ -DOPENCL_HEADERS_BUILD_CXX_TESTS=OFF `
203
+ -DCMAKE_INSTALL_PREFIX="$HOME/dev/llm/opencl"
204
+ cmake --build . --target install
205
+
206
+ cd ~/dev/llm
207
+ git clone https://github.com/KhronosGroup/OpenCL-ICD-Loader && cd OpenCL-ICD-Loader
208
+ mkdir build && cd build
209
+ cmake .. -G Ninja `
210
+ -DCMAKE_BUILD_TYPE=Release `
211
+ -DCMAKE_PREFIX_PATH="$HOME/dev/llm/opencl" `
212
+ -DCMAKE_INSTALL_PREFIX="$HOME/dev/llm/opencl"
213
+ cmake --build . --target install
214
+ ```
215
+
216
+ ### II. Build llama.cpp
217
+
218
+ ```powershell
219
+
220
+ mkdir -p ~/dev/llm
221
+ cd ~/dev/llm
222
+
223
+ git clone https://github.com/ggml-org/llama.cpp && cd llama.cpp
224
+ mkdir build && cd build
225
+
226
+ cmake .. -G Ninja `
227
+ -DCMAKE_TOOLCHAIN_FILE="$HOME/dev/llm/llama.cpp/cmake/arm64-windows-llvm.cmake" `
228
+ -DCMAKE_BUILD_TYPE=Release `
229
+ -DCMAKE_PREFIX_PATH="$HOME/dev/llm/opencl" `
230
+ -DBUILD_SHARED_LIBS=OFF `
231
+ -DGGML_OPENCL=ON
232
+ ninja
233
+ ```
234
+
235
+ ## Linux
236
+
237
+ The two steps just above also apply to Linux. When building for linux, the commands are mostly the same as those for PowerShell on Windows, but in the second step they do not have the `-DCMAKE_TOOLCHAIN_FILE` parameter, and then in both steps the backticks are replaced with back slashes.
238
+
239
+ If not installed already, install Git, CMake, Clang, Ninja and Python, then run in the terminal the following:
240
+
241
+ ### I. Setup Environment
242
+
243
+ 1. **Install OpenCL Headers and Library**
244
+
245
+ ```bash
246
+ mkdir -p ~/dev/llm
247
+
248
+ cd ~/dev/llm
249
+ git clone https://github.com/KhronosGroup/OpenCL-Headers && cd OpenCL-Headers
250
+ mkdir build && cd build
251
+ cmake .. -G Ninja \
252
+ -DBUILD_TESTING=OFF \
253
+ -DOPENCL_HEADERS_BUILD_TESTING=OFF \
254
+ -DOPENCL_HEADERS_BUILD_CXX_TESTS=OFF \
255
+ -DCMAKE_INSTALL_PREFIX="$HOME/dev/llm/opencl"
256
+ cmake --build . --target install
257
+
258
+ cd ~/dev/llm
259
+ git clone https://github.com/KhronosGroup/OpenCL-ICD-Loader && cd OpenCL-ICD-Loader
260
+ mkdir build && cd build
261
+ cmake .. -G Ninja \
262
+ -DCMAKE_BUILD_TYPE=Release \
263
+ -DCMAKE_PREFIX_PATH="$HOME/dev/llm/opencl" \
264
+ -DCMAKE_INSTALL_PREFIX="$HOME/dev/llm/opencl"
265
+ cmake --build . --target install
266
+ ```
267
+
268
+ ### II. Build llama.cpp
269
+
270
+ ```bash
271
+ mkdir -p ~/dev/llm
272
+ cd ~/dev/llm
273
+
274
+ git clone https://github.com/ggml-org/llama.cpp && cd llama.cpp
275
+ mkdir build && cd build
276
+
277
+ cmake .. -G Ninja \
278
+ -DCMAKE_BUILD_TYPE=Release \
279
+ -DCMAKE_PREFIX_PATH="$HOME/dev/llm/opencl" \
280
+ -DBUILD_SHARED_LIBS=OFF \
281
+ -DGGML_OPENCL=ON
282
+ ninja
283
+ ```
284
+
285
+ ## Known Issues
286
+
287
+ - Flash attention does not always improve performance.
288
+ - Currently OpenCL backend works on A6xx GPUs with recent drivers and compilers (usually found in IoT platforms).
289
+ However, it does not work on A6xx GPUs found in phones with old drivers and compilers.
290
+
291
+ ## TODO
292
+
293
+ - Improve flash attention
294
+ - Improve OpenCL C kernels performance