char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

PikaPikachuconvert : add `--fuse-qkv` flag to fuse Q/K/V into QKV during HF-to-GGUF conversion (#22780)465e49b9c

char/aliceai-t5-moe
141.4 KiB2822 linesraw
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3
4from __future__ import annotations
5
6import ast
7import logging
8import contextlib
9import json
10import os
11import re
12import sys
13from enum import IntEnum
14from pathlib import Path
15from hashlib import sha256
16from typing import TYPE_CHECKING, Any, Callable, ContextManager, Iterable, Iterator, Literal, Sequence, TypeVar, cast
17from itertools import chain
18from transformers import AutoConfig
19
20import numpy as np
21import torch
22
23if TYPE_CHECKING:
24    from torch import Tensor
25
26if 'NO_LOCAL_GGUF' not in os.environ:
27    sys.path.insert(1, str(Path(__file__).parent.parent / 'gguf-py'))
28import gguf
29from gguf.vocab import MistralTokenizerType, MistralVocab
30
31try:
32    from mistral_common.tokens.tokenizers.base import TokenizerVersion # type: ignore[import-not-found, ty:unresolved-import]
33    from mistral_common.tokens.tokenizers.multimodal import DATASET_MEAN as _MISTRAL_COMMON_DATASET_MEAN, DATASET_STD as _MISTRAL_COMMON_DATASET_STD # type: ignore[import-not-found, ty:unresolved-import]
34    from mistral_common.tokens.tokenizers.tekken import Tekkenizer # type: ignore[import-not-found, ty:unresolved-import]
35    from mistral_common.tokens.tokenizers.sentencepiece import ( # type: ignore[import-not-found, ty:unresolved-import]
36        SentencePieceTokenizer,
37    )
38
39    _mistral_common_installed = True
40    _mistral_import_error_msg = ""
41except ImportError:
42    _MISTRAL_COMMON_DATASET_MEAN = (0.48145466, 0.4578275, 0.40821073)
43    _MISTRAL_COMMON_DATASET_STD = (0.26862954, 0.26130258, 0.27577711)
44
45    _mistral_common_installed = False
46    TokenizerVersion: Any = None
47    Tekkenizer: Any = None
48    SentencePieceTokenizer: Any = None
49    _mistral_import_error_msg = (
50        "Mistral format requires `mistral-common` to be installed. Please run "
51        "`pip install mistral-common[image,audio]` to install it."
52    )
53
54
55logger = logging.getLogger("hf-to-gguf")
56
57
58AnyModel = TypeVar("AnyModel", bound="type[ModelBase]")
59
60
61# for checkpoints that ship no config.json, we will try to provide a synthetic one
62HparamsMatcher = Callable[[Path], bool]
63HparamsLoader = Callable[[Path], dict[str, Any]]
64
65
66class SentencePieceTokenTypes(IntEnum):
67    NORMAL = 1
68    UNKNOWN = 2
69    CONTROL = 3
70    USER_DEFINED = 4
71    UNUSED = 5
72    BYTE = 6
73
74
75class ModelType(IntEnum):
76    TEXT = 1
77    MMPROJ = 2
78
79
80class ModelBase:
81    _model_classes: dict[ModelType, dict[str, type[ModelBase]]] = {
82        ModelType.TEXT: {},
83        ModelType.MMPROJ: {},
84    }
85    _hparams_loaders: list[tuple[HparamsMatcher, HparamsLoader]] = []
86
87    dir_model: Path
88    ftype: gguf.LlamaFileType
89    fname_out: Path
90    is_big_endian: bool
91    endianess: gguf.GGUFEndian
92    use_temp_file: bool
93    lazy: bool
94    dry_run: bool
95    hparams: dict[str, Any]
96    model_tensors: dict[str, Callable[[], Tensor]]
97    gguf_writer: gguf.GGUFWriter
98    model_name: str | None
99    metadata_override: Path | None
100    metadata: gguf.Metadata
101    dir_model_card: Path
102    remote_hf_model_id: str | None
103    target_model_dir: Path | None
104
105    # subclasses should define this!
106    model_arch: gguf.MODEL_ARCH
107
108    # subclasses should initialize this!
109    block_count: int
110    tensor_map: gguf.TensorNameMap
111
112    # Mistral format specifics
113    is_mistral_format: bool = False
114    disable_mistral_community_chat_template: bool = False
115    sentence_transformers_dense_modules: bool = False
116
117    # MTP (multi-token prediction) export modes; set by main() before instantiation.
118    # Architectures that implement the filtering/export behavior opt in by
119    # setting supports_mtp_export = True on their model class or a mixin.
120    supports_mtp_export: bool = False
121    mtp_only: bool = False
122    no_mtp: bool = False
123
124    def __init__(self, dir_model: Path, ftype: gguf.LlamaFileType, fname_out: Path, *, is_big_endian: bool = False,
125                 use_temp_file: bool = False, eager: bool = False,
126                 metadata_override: Path | None = None, model_name: str | None = None,
127                 split_max_tensors: int = 0, split_max_size: int = 0, dry_run: bool = False,
128                 small_first_shard: bool = False, hparams: dict[str, Any] | None = None, remote_hf_model_id: str | None = None,
129                 disable_mistral_community_chat_template: bool = False,
130                 sentence_transformers_dense_modules: bool = False,
131                 target_model_dir: Path | None = None,
132                 fuse_gate_up_exps: bool = False,
133                 fp8_as_q8: bool = False,
134                 fuse_qkv: bool = False):
135        if type(self) is ModelBase or \
136                type(self) is TextModel or \
137                type(self) is MmprojModel:
138            raise TypeError(f"{type(self).__name__!r} should not be directly instantiated")
139
140        if self.is_mistral_format and not _mistral_common_installed:
141            raise ImportError(_mistral_import_error_msg)
142
143        self.dir_model = dir_model
144        self.ftype = ftype
145        self.fname_out = fname_out
146        self.is_big_endian = is_big_endian
147        self.endianess = gguf.GGUFEndian.BIG if is_big_endian else gguf.GGUFEndian.LITTLE
148        self.use_temp_file = use_temp_file
149        self.lazy = not eager or (remote_hf_model_id is not None)
150        self.dry_run = dry_run
151        self.remote_hf_model_id = remote_hf_model_id
152        self.sentence_transformers_dense_modules = sentence_transformers_dense_modules
153        self.target_model_dir = target_model_dir
154        self.fuse_gate_up_exps = fuse_gate_up_exps
155        self._gate_exp_buffer: dict[int, Tensor] = {}
156        self._up_exp_buffer: dict[int, Tensor] = {}
157        self.fuse_qkv = fuse_qkv
158        self._q_buffer: dict[int, Tensor] = {}
159        self._k_buffer: dict[int, Tensor] = {}
160        self._v_buffer: dict[int, Tensor] = {}
161        self._q_bias_buffer: dict[int, Tensor] = {}
162        self._k_bias_buffer: dict[int, Tensor] = {}
163        self._v_bias_buffer: dict[int, Tensor] = {}
164        self._fusable_qkv_weight_layers: set[int] = set()
165        self._fusable_qkv_bias_layers: set[int] = set()
166        self.hparams = ModelBase.load_hparams(self.dir_model, self.is_mistral_format) if hparams is None else hparams
167        self.model_tensors = self.index_tensors(remote_hf_model_id=remote_hf_model_id)
168        self.metadata_override = metadata_override
169        self.model_name = model_name
170        self.dir_model_card = dir_model  # overridden in convert_lora_to_gguf.py
171        self._is_nvfp4 = False
172        self._is_mxfp4 = False
173        self._fp8_as_q8 = fp8_as_q8
174        self._fp8_dequantized: set[str] = set()
175
176        # Apply heuristics to figure out typical tensor encoding based on first tensor's dtype
177        # NOTE: can't use field "torch_dtype" in config.json, because some finetunes lie.
178        if self.ftype == gguf.LlamaFileType.GUESSED:
179            for _, tensor in self.get_tensors():
180                if tensor.dim() < 2:
181                    continue
182
183                if tensor.dtype == torch.bfloat16:
184                    self.ftype = gguf.LlamaFileType.MOSTLY_BF16
185                    logger.info("heuristics detected bfloat16 tensor dtype, setting --outtype bf16")
186                    break
187                elif tensor.dtype == torch.float16:
188                    self.ftype = gguf.LlamaFileType.MOSTLY_F16
189                    logger.info("heuristics detected float16 tensor dtype, setting --outtype f16")
190                    break
191            else:
192                self.ftype = gguf.LlamaFileType.MOSTLY_F16
193                logger.info("heuristics unable to detect tensor dtype, defaulting to --outtype f16")
194
195        # Configure GGUF Writer
196        self.gguf_writer = gguf.GGUFWriter(path=None, arch=gguf.MODEL_ARCH_NAMES[self.model_arch], endianess=self.endianess, use_temp_file=self.use_temp_file,
197                                           split_max_tensors=split_max_tensors, split_max_size=split_max_size, dry_run=dry_run, small_first_shard=small_first_shard)
198
199        # Mistral specific
200        self.disable_mistral_community_chat_template = disable_mistral_community_chat_template
201
202    @classmethod
203    def add_prefix_to_filename(cls, path: Path, prefix: str) -> Path:
204        stem, suffix = path.stem, path.suffix
205        new_name = f"{prefix}{stem}{suffix}"
206        return path.with_name(new_name)
207
208    def find_hparam(self, keys: Iterable[str], optional: bool = False) -> Any:
209        key = next((k for k in keys if k in self.hparams), None)
210        if key is not None:
211            return self.hparams[key]
212        if optional:
213            return None
214        raise KeyError(f"could not find any of: {keys}")
215
216    def index_tensors(self, remote_hf_model_id: str | None = None) -> dict[str, Callable[[], Tensor]]:
217        tensors: dict[str, Callable[[], Tensor]] = {}
218
219        if remote_hf_model_id is not None:
220            is_safetensors = True
221
222            logger.info(f"Using remote model with HuggingFace id: {remote_hf_model_id}")
223            remote_tensors = gguf.utility.SafetensorRemote.get_list_tensors_hf_model(remote_hf_model_id)
224            for name, remote_tensor in remote_tensors.items():
225                data_gen = lambda r=remote_tensor: LazyTorchTensor.from_remote_tensor(r)  # noqa: E731
226                if titem := self.filter_tensors((name, data_gen)):
227                    tname, tgen = titem
228                    tensors[tname] = tgen
229
230            return tensors
231
232        prefix = "model" if not self.is_mistral_format else "consolidated"
233        part_names: list[str] = ModelBase.get_model_part_names(self.dir_model, prefix, ".safetensors")
234        is_safetensors: bool = len(part_names) > 0
235        if not is_safetensors:
236            part_names = ModelBase.get_model_part_names(self.dir_model, "pytorch_model", ".bin")
237
238        tensor_names_from_index: set[str] = set()
239        tensor_names_from_parts: set[str] = set()
240
241        if not self.is_mistral_format:
242            index_name = "model.safetensors" if is_safetensors else "pytorch_model.bin"
243            index_name += ".index.json"
244            index_file = self.dir_model / index_name
245
246            if index_file.is_file():
247                logger.info(f"gguf: loading model weight map from '{index_name}'")
248                with open(index_file, "r", encoding="utf-8") as f:
249                    index: dict[str, Any] = json.load(f)
250                    weight_map = index.get("weight_map")
251                    if weight_map is None or not isinstance(weight_map, dict):
252                        raise ValueError(f"Can't load 'weight_map' from {index_name!r}")
253                    tensor_names_from_index.update(weight_map.keys())
254                    part_dict: dict[str, None] = dict.fromkeys(weight_map.values(), None) # ty: ignore[invalid-assignment]
255                    part_names = sorted(part_dict.keys())
256            else:
257                weight_map = {}
258        else:
259            weight_map = {}
260
261        for part_name in part_names:
262            logger.info(f"gguf: indexing model part '{part_name}'")
263            ctx: ContextManager[Any]
264            if is_safetensors:
265                ctx = cast(ContextManager[Any], gguf.utility.SafetensorsLocal(self.dir_model / part_name))
266            else:
267                ctx = contextlib.nullcontext(torch.load(str(self.dir_model / part_name), map_location="cpu", mmap=True, weights_only=True))
268
269            with ctx as model_part:
270                assert model_part is not None
271
272                for name in model_part.keys():
273                    tensor_names_from_parts.add(name)
274                    if is_safetensors:
275                        data: gguf.utility.LocalTensor = model_part[name]
276                        if self.lazy:
277                            data_gen = lambda data=data: LazyTorchTensor.from_local_tensor(data)  # noqa: E731
278                        else:
279                            dtype = LazyTorchTensor._dtype_str_map[data.dtype]
280                            data_gen = lambda data=data, dtype=dtype: torch.from_numpy(data.mmap_bytes()).view(dtype).reshape(data.shape)  # noqa: E731
281                    else:
282                        data_torch: Tensor = model_part[name]
283                        if self.lazy:
284                            data_gen = lambda data=data_torch: LazyTorchTensor.from_eager(data)  # noqa: E731
285                        else:
286                            data_gen = lambda data=data_torch: data  # noqa: E731
287                    if titem := self.filter_tensors((name, data_gen)):
288                        tname, tgen = titem
289                        tensors[tname] = tgen
290
291        # verify tensor name presence and identify potentially missing files
292        if len(tensor_names_from_index) > 0:
293            if len(tensor_names_from_parts.symmetric_difference(tensor_names_from_index)) > 0:
294                missing = sorted(tensor_names_from_index.difference(tensor_names_from_parts))
295                extra = sorted(tensor_names_from_parts.difference(tensor_names_from_index))
296                missing_files = sorted(set(weight_map[n] for n in missing if n in weight_map))
297                if len(extra) == 0 and len(missing_files) > 0:
298                    raise ValueError(f"Missing or incomplete model files: {missing_files}\n"
299                                     f"Missing tensors: {missing}")
300                else:
301                    raise ValueError("Mismatch between weight map and model parts for tensor names:\n"
302                                     f"Missing tensors: {missing}\n"
303                                     f"Extra tensors: {extra}")
304
305        return tensors
306
307    @staticmethod
308    def _scale_is_trivial(scale: Tensor) -> bool:
309        return scale.numel() <= 1 and abs(float(scale.float().sum()) - 1.0) < 1e-6
310
311    def _write_scale_tensor(self, scale_name: str, scale: Tensor):
312        if not self._scale_is_trivial(scale):
313            scale_f32 = scale.float().numpy().flatten()
314            logger.info(f"  + {scale_name} (per-tensor scale, shape [{scale_f32.size}])")
315            self.gguf_writer.add_tensor(scale_name, scale_f32)
316
317    def _write_scales_tensor(self, scale_name: str, scales: list[float]):
318        if not np.allclose(scales, 1.0, atol=1e-6):
319            scale_vals = np.array(scales, dtype=np.float32)
320            logger.info(f"  + {scale_name} (per-expert scale, shape [{len(scales)}])")
321            self.gguf_writer.add_tensor(scale_name, scale_vals)
322
323    def dequant_model(self):
324        # If all quantized tensors were already handled (e.g. pure NVFP4), skip
325        if self._is_nvfp4 and not any(k.endswith((".weight_scale", ".weight_scale_inv")) for k in self.model_tensors):
326            return
327
328        tensors_to_remove: list[str] = []
329        new_tensors: dict[str, Callable[[], Tensor]] = {}
330
331        if (quant_config := self.hparams.get("quantization_config")) and isinstance(quant_config, dict):
332            quant_method = quant_config.get("quant_method")
333
334            def dequant_bitnet(weight: Tensor, scale: Tensor) -> Tensor:
335                weight = weight.view(torch.uint8)
336                orig_shape = weight.shape
337
338                shift = torch.tensor([0, 2, 4, 6], dtype=torch.uint8).reshape((4, *(1 for _ in range(len(orig_shape)))))
339                data = weight.unsqueeze(0).expand((4, *orig_shape)) >> shift
340                data = data & 3
341                data = (data.float() - 1).reshape((orig_shape[0] * 4, *orig_shape[1:]))
342
343                # The scale is inverted
344                return data / scale.float()
345
346            def dequant_simple(weight: Tensor, scale: Tensor, block_size: Sequence[int] | None = None) -> Tensor:
347                scale = scale.float()
348
349                if block_size is not None:
350                    dim_offset = scale.ndim - len(block_size)
351                    for i, size in enumerate(block_size):
352                        scale = scale.repeat_interleave(size, dim_offset + i)
353                    # unpad the scale (e.g. when the tensor size isn't a multiple of the block size)
354                    scale = scale[tuple(slice(0, size) for size in weight.shape)]
355
356                # align scale dims to weight for correct broadcasting (e.g. [128] -> [128, 1, 1])
357                while scale.ndim < weight.ndim:
358                    scale = scale.unsqueeze(-1)
359
360                return weight.float() * scale
361
362            # ref: https://github.com/ModelCloud/GPTQModel/blob/037c5c0f6c9e33c500d975b038d02e7ca437546d/gptqmodel/nn_modules/qlinear/__init__.py#L437-L476
363            def dequant_gptq(g_idx: Tensor, qweight: Tensor, qzeros: Tensor, scales: Tensor) -> Tensor:
364                bits = quant_config["bits"]
365                assert bits in (2, 3, 4, 8)
366                assert qweight.dtype == qzeros.dtype
367                maxq = (2 ** bits) - 1
368                weight = None
369                zeros = None
370                pack_dtype_bits = qweight.dtype.itemsize * 8
371
372                if bits in [2, 4, 8]:
373                    pack_factor = pack_dtype_bits // bits
374                    wf = torch.tensor(list(range(0, pack_dtype_bits, bits)), dtype=torch.int32).unsqueeze(0)
375                    if self.lazy:
376                        wf = LazyTorchTensor.from_eager(wf)
377
378                    zeros = torch.bitwise_right_shift(
379                        qzeros.unsqueeze(2).expand(-1, -1, pack_factor),
380                        wf.unsqueeze(0)
381                    ).to(torch.int16 if bits == 8 else torch.int8)
382                    zeros = torch.bitwise_and(zeros, maxq).reshape(scales.shape)
383
384                    weight = torch.bitwise_and(
385                        torch.bitwise_right_shift(
386                            qweight.unsqueeze(1).expand(-1, pack_factor, -1),
387                            wf.unsqueeze(-1)
388                        ).to(torch.int16 if bits == 8 else torch.int8),
389                        maxq
390                    )
391                elif bits == 3:
392                    raise NotImplementedError("3-bit gptq dequantization is not yet implemented")
393
394                assert weight is not None
395                assert zeros is not None
396
397                weight = weight.reshape(weight.shape[0] * weight.shape[1], weight.shape[2])
398
399                # gptq_v2 doesn't need to offset zeros
400                if quant_config.get("checkpoint_format", "gptq") == "gptq":
401                    zeros += 1
402
403                return (scales[g_idx].float() * (weight - zeros[g_idx]).float()).T
404
405            def dequant_packed(w: Tensor, scale: Tensor, shape_tensor: Tensor, zero_point: Tensor | None, num_bits: int, group_size: int):
406                assert w.dtype == torch.int32
407                shape = tuple(shape_tensor.tolist())
408                assert len(shape) == 2
409                mask = (1 << num_bits) - 1
410
411                shifts = torch.arange(0, 32 - (num_bits - 1), num_bits, dtype=torch.int32)
412                if self.lazy:
413                    shifts = LazyTorchTensor.from_eager(shifts)
414
415                if zero_point is None:
416                    offset = 1 << (num_bits - 1)
417                else:
418                    assert len(zero_point.shape) == 2
419                    offset = (zero_point.unsqueeze(1) >> shifts.reshape(1, -1, 1)) & mask
420                    offset = offset.reshape(-1, zero_point.shape[1])
421                    # trim padding, and prepare for broadcast
422                    # NOTE: the zero-point is packed along dim 0
423                    offset = offset[:shape[0], :].unsqueeze(-1)
424
425                # extract values
426                # NOTE: the weights are packed along dim 1
427                unpacked = (w.unsqueeze(-1) >> shifts.reshape(1, 1, -1)) & mask
428                unpacked = unpacked.reshape(shape[0], -1)
429
430                # trim padding
431                unpacked = unpacked[:, :shape[1]]
432
433                # prepare for broadcast of the scale
434                unpacked = unpacked.reshape(shape[0], (unpacked.shape[-1] + group_size - 1) // group_size, group_size)
435                unpacked = unpacked - offset
436
437                return (unpacked * scale.unsqueeze(-1).float()).reshape(shape)
438
439            if quant_method == "bitnet":
440                for name in self.model_tensors.keys():
441                    if name.endswith(".weight_scale"):
442                        weight_name = name.removesuffix("_scale")
443                        w = self.model_tensors[weight_name]
444                        s = self.model_tensors[name]
445                        self.model_tensors[weight_name] = lambda w=w, s=s: dequant_bitnet(w(), s())
446                        tensors_to_remove.append(name)
447            elif quant_method == "fp8":
448                block_size = quant_config.get("weight_block_size")
449                for name in self.model_tensors.keys():
450                    if name.endswith("_scale_inv"):
451                        weight_name = name.removesuffix("_scale_inv")
452                        w = self.model_tensors[weight_name]
453                        s = self.model_tensors[name]
454                        self.model_tensors[weight_name] = lambda w=w, s=s, bs=block_size: dequant_simple(w(), s(), bs)
455                        tensors_to_remove.append(name)
456                        if self._fp8_as_q8:
457                            self._fp8_dequantized.add(weight_name)
458                    if name.endswith(".activation_scale"):  # unused
459                        tensors_to_remove.append(name)
460                    if name.endswith("_activation_scale"):  # Mistral-Small-4-119B-2602, unused
461                        tensors_to_remove.append(name)
462                    # mistral format
463                    if name.endswith(".qscale_weight"):
464                        weight_name = name.removesuffix("qscale_weight") + "weight"
465                        w = self.model_tensors[weight_name]
466                        s = self.model_tensors[name]
467                        self.model_tensors[weight_name] = lambda w=w, s=s, bs=block_size: dequant_simple(w(), s(), bs)
468                        tensors_to_remove.append(name)
469                        if self._fp8_as_q8:
470                            self._fp8_dequantized.add(weight_name)
471                    if name.endswith(".qscale_act"):
472                        tensors_to_remove.append(name)
473            elif quant_method == "gptq":
474                for name in self.model_tensors.keys():
475                    if name.endswith(".qweight"):
476                        base_name = name.removesuffix(".qweight")
477                        g_idx = self.model_tensors[base_name + ".g_idx"]
478                        qweight = self.model_tensors[base_name + ".qweight"]
479                        qzeros = self.model_tensors[base_name + ".qzeros"]
480                        scales = self.model_tensors[base_name + ".scales"]
481                        new_tensors[base_name + ".weight"] = (
482                            lambda g=g_idx, z=qzeros, w=qweight, s=scales: dequant_gptq(
483                                g(), w(), z(), s()
484                            )
485                        )
486                        tensors_to_remove += [
487                            base_name + n
488                            for n in (
489                                ".g_idx",
490                                ".qzeros",
491                                ".qweight",
492                                ".scales",
493                            )
494                        ]
495            elif quant_method == "compressed-tensors":
496                quant_format = quant_config["format"]
497                groups = quant_config["config_groups"]
498                nvfp4_compressed_tensors = (
499                    quant_format == "nvfp4-pack-quantized"
500                    or quant_format == "mixed-precision"
501                    and bool(groups)
502                    and all(g.get("format") == "nvfp4-pack-quantized" for g in groups.values() if isinstance(g, dict))
503                )
504
505                if len(groups) > 1 and not nvfp4_compressed_tensors:
506                    raise NotImplementedError("Can't handle multiple config groups for compressed-tensors yet")
507                weight_config = tuple(groups.values())[0]["weights"]
508
509                if quant_format == "float-quantized" or quant_format == "int-quantized" or quant_format == "naive-quantized":
510                    block_size = weight_config.get("block_structure", None)
511                    strategy = weight_config.get("strategy")
512                    assert strategy == "channel" or strategy == "block"
513                    assert weight_config.get("group_size") is None  # didn't find a model using this yet
514                    is_fp8 = (
515                        quant_format == "float-quantized"
516                        and weight_config.get("type") == "float"
517                        and weight_config.get("num_bits") == 8
518                    )
519                    for name in self.model_tensors.keys():
520                        if name.endswith(".weight_scale"):
521                            weight_name = name.removesuffix("_scale")
522                            w = self.model_tensors[weight_name]
523                            s = self.model_tensors[name]
524                            self.model_tensors[weight_name] = lambda w=w, s=s: dequant_simple(w(), s(), block_size)
525                            tensors_to_remove.append(name)
526                            if self._fp8_as_q8 and is_fp8:
527                                self._fp8_dequantized.add(weight_name)
528                elif quant_format == "pack-quantized":
529                    assert weight_config.get("strategy") == "group"
530                    assert weight_config.get("type", "int") == "int"
531                    num_bits = weight_config.get("num_bits")
532                    group_size = weight_config.get("group_size")
533                    assert isinstance(num_bits, int)
534                    assert isinstance(group_size, int)
535                    for name in self.model_tensors.keys():
536                        if name.endswith(".weight_packed"):
537                            base_name = name.removesuffix("_packed")
538                            w = self.model_tensors[name]
539                            scale = self.model_tensors[base_name + "_scale"]
540                            shape = self.model_tensors[base_name + "_shape"]
541                            zero_point = self.model_tensors.get(base_name + "_zero_point", lambda: None)
542                            new_tensors[base_name] = (
543                                lambda w=w, scale=scale, shape=shape, zero_point=zero_point: dequant_packed(
544                                    w(), scale(), shape(), zero_point(), num_bits, group_size,
545                                )
546                            )
547                            tensors_to_remove += [base_name + n for n in ("_packed", "_shape", "_scale")]
548                            if (base_name + "_zero_point") in self.model_tensors:
549                                tensors_to_remove.append(base_name + "_zero_point")
550                elif nvfp4_compressed_tensors:
551                    # Don't error from compressed-tensors, we'll handle them in _generate_nvfp4_tensors
552                    pass
553                else:
554                    raise NotImplementedError(f"Quant format {quant_format!r} for method {quant_method!r} is not yet supported")
555            elif quant_method == "modelopt":
556                # Mixed-precision ModelOpt models: NVFP4 tensors are handled by
557                # _generate_nvfp4_tensors; FP8 tensors have 1D weight_scale and
558                # are dequantized here. k/v scale tensors are unused.
559                for name in self.model_tensors.keys():
560                    if name.endswith(".weight_scale"):
561                        weight_name = name.removesuffix("_scale")
562                        if weight_name not in self.model_tensors:
563                            tensors_to_remove.append(name)
564                            continue
565                        w = self.model_tensors[weight_name]
566                        s = self.model_tensors[name]
567                        is_fp8_weight = False
568                        if self._fp8_as_q8:
569                            is_fp8_weight = w().dtype in (torch.float8_e4m3fn, torch.float8_e5m2)
570                        self.model_tensors[weight_name] = lambda w=w, s=s: dequant_simple(w(), s(), None)
571                        tensors_to_remove.append(name)
572                        if is_fp8_weight:
573                            self._fp8_dequantized.add(weight_name)
574                    if name.endswith((".input_scale", ".k_scale", ".v_scale")):
575                        tensors_to_remove.append(name)
576            elif quant_method is not None:
577                raise NotImplementedError(f"Quant method is not yet supported: {quant_method!r}")
578
579        for name in tensors_to_remove:
580            if name in self.model_tensors:
581                del self.model_tensors[name]
582
583        for name, value in new_tensors.items():
584            self.model_tensors[name] = value
585
586    @classmethod
587    def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
588        name, gen = item
589
590        if name.endswith("e_score_correction_bias"):
591            name = name.replace("e_score_correction_bias", "e_score_correction.bias")
592
593        if "language_model." in name:
594            name = name.replace("language_model.", "")
595
596        return name, gen
597
598    def get_tensors(self) -> Iterator[tuple[str, Tensor]]:
599        for name, gen in self.model_tensors.items():
600            yield name, gen()
601
602    def format_tensor_name(self, key: gguf.MODEL_TENSOR, bid: int | None = None, suffix: str = ".weight") -> str:
603        if key not in gguf.MODEL_TENSORS[self.model_arch]:
604            raise ValueError(f"Missing {key!r} for MODEL_TENSORS of {self.model_arch!r}")
605        name: str = gguf.TENSOR_NAMES[key]
606        if "{bid}" in name:
607            assert bid is not None
608            name = name.format(bid=bid)
609        return name + suffix
610
611    def match_model_tensor_name(self, name: str, key: gguf.MODEL_TENSOR, bid: int | None, suffix: str = ".weight") -> bool:
612        if key not in gguf.MODEL_TENSORS[self.model_arch]:
613            return False
614        key_name: str = gguf.TENSOR_NAMES[key]
615        if "{bid}" in key_name:
616            if bid is None:
617                return False
618            key_name = key_name.format(bid=bid)
619        else:
620            if bid is not None:
621                return False
622        return name == (key_name + suffix)
623
624    def map_tensor_name(self, name: str, try_suffixes: Sequence[str] = (".weight", ".bias")) -> str:
625        new_name = self.tensor_map.get_name(key=name, try_suffixes=try_suffixes)
626        if new_name is None:
627            raise ValueError(f"Can not map tensor {name!r}")
628        return new_name
629
630    def prepare_qkv_fusion(self) -> None:
631        self._fusable_qkv_weight_layers.clear()
632        self._fusable_qkv_bias_layers.clear()
633        if not self.fuse_qkv or gguf.MODEL_TENSOR.ATTN_QKV not in gguf.MODEL_TENSORS[self.model_arch]:
634            return
635
636        qkv_types = {
637            gguf.MODEL_TENSOR.ATTN_Q,
638            gguf.MODEL_TENSOR.ATTN_K,
639            gguf.MODEL_TENSOR.ATTN_V,
640        }
641        weights: dict[int, set[gguf.MODEL_TENSOR]] = {}
642        biases: dict[int, set[gguf.MODEL_TENSOR]] = {}
643
644        for name in self.model_tensors:
645            mapped = self.tensor_map.get_type_and_name(name, try_suffixes=(".weight", ".bias"))
646            if mapped is None:
647                continue
648            tensor_type, new_name = mapped
649            if tensor_type not in qkv_types:
650                continue
651
652            bid = next((int(part) for part in new_name.split(".") if part.isdecimal()), None)
653            if bid is None:
654                continue
655            if new_name.endswith(".weight"):
656                weights.setdefault(bid, set()).add(tensor_type)
657            elif new_name.endswith(".bias"):
658                biases.setdefault(bid, set()).add(tensor_type)
659
660        for bid, weight_types in weights.items():
661            bias_types = biases.get(bid, set())
662            if weight_types == qkv_types and (not bias_types or bias_types == qkv_types):
663                self._fusable_qkv_weight_layers.add(bid)
664                if bias_types:
665                    self._fusable_qkv_bias_layers.add(bid)
666
667    def set_gguf_parameters(self):
668        raise NotImplementedError("set_gguf_parameters() must be implemented in subclasses")
669
670    def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
671        new_name = self.map_tensor_name(name)
672
673        # Handle gate/up expert tensor fusion if enabled
674        if self.fuse_gate_up_exps and bid is not None:
675            if self.match_model_tensor_name(new_name, gguf.MODEL_TENSOR.FFN_GATE_EXP, bid):
676                self._gate_exp_buffer[bid] = data_torch
677            elif self.match_model_tensor_name(new_name, gguf.MODEL_TENSOR.FFN_UP_EXP, bid):
678                self._up_exp_buffer[bid] = data_torch
679
680            # Check if both gate and up are buffered for this layer
681            if bid in self._gate_exp_buffer and bid in self._up_exp_buffer:
682                gate_data = self._gate_exp_buffer.pop(bid)
683                up_data = self._up_exp_buffer.pop(bid)
684                # gate/up shape: (n_expert, n_ff, n_embd), concatenate to (n_expert, n_ff*2, n_embd)
685                fused_data = torch.cat([gate_data, up_data], dim=1)
686                fused_name = self.format_tensor_name(gguf.MODEL_TENSOR.FFN_GATE_UP_EXP, bid)
687                logger.info(f"Fused gate_exps and up_exps for layer {bid}")
688                return [(fused_name, fused_data)]
689
690            # If we buffered a gate/up tensor, wait for the other
691            if self.match_model_tensor_name(new_name, gguf.MODEL_TENSOR.FFN_GATE_EXP, bid) or \
692               self.match_model_tensor_name(new_name, gguf.MODEL_TENSOR.FFN_UP_EXP, bid):
693                return []
694
695        # Handle Q/K/V tensor fusion if enabled
696        qkv_bid = next((int(part) for part in new_name.split(".") if part.isdecimal()), None) if self.fuse_qkv else None
697        if qkv_bid is not None:
698            is_bias = new_name.endswith('.bias')
699            suffix = '.bias' if is_bias else '.weight'
700            fusable_layers = self._fusable_qkv_bias_layers if is_bias else self._fusable_qkv_weight_layers
701            if qkv_bid not in fusable_layers:
702                return [(new_name, data_torch)]
703
704            buf_q = self._q_bias_buffer if is_bias else self._q_buffer
705            buf_k = self._k_bias_buffer if is_bias else self._k_buffer
706            buf_v = self._v_bias_buffer if is_bias else self._v_buffer
707
708            if self.match_model_tensor_name(new_name, gguf.MODEL_TENSOR.ATTN_Q, qkv_bid, suffix):
709                buf_q[qkv_bid] = data_torch
710            elif self.match_model_tensor_name(new_name, gguf.MODEL_TENSOR.ATTN_K, qkv_bid, suffix):
711                buf_k[qkv_bid] = data_torch
712            elif self.match_model_tensor_name(new_name, gguf.MODEL_TENSOR.ATTN_V, qkv_bid, suffix):
713                buf_v[qkv_bid] = data_torch
714
715            if qkv_bid in buf_q and qkv_bid in buf_k and qkv_bid in buf_v:
716                q_data = buf_q.pop(qkv_bid)
717                k_data = buf_k.pop(qkv_bid)
718                v_data = buf_v.pop(qkv_bid)
719                fused_data = torch.cat([q_data, k_data, v_data], dim=0)
720                fused_name = self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_QKV, qkv_bid, suffix=suffix)
721                logger.info(f"Fused Q, K, V {suffix[1:]} into QKV for layer {qkv_bid}")
722                return [(fused_name, fused_data)]
723
724            if self.match_model_tensor_name(new_name, gguf.MODEL_TENSOR.ATTN_Q, qkv_bid, suffix) or \
725               self.match_model_tensor_name(new_name, gguf.MODEL_TENSOR.ATTN_K, qkv_bid, suffix) or \
726               self.match_model_tensor_name(new_name, gguf.MODEL_TENSOR.ATTN_V, qkv_bid, suffix):
727                return []
728
729        return [(new_name, data_torch)]
730
731    def tensor_force_quant(self, name: str, new_name: str, bid: int | None, n_dims: int) -> gguf.GGMLQuantizationType | bool:
732        del new_name, bid  # unused
733        # Force FP8-original tensors to Q8_0 when requested; Q8_0 is faster than F16/BF16.
734        if self._fp8_as_q8 and name in self._fp8_dequantized and n_dims >= 2:
735            return gguf.GGMLQuantizationType.Q8_0
736        return False
737
738    # some models need extra generated tensors (like rope_freqs)
739    def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]:
740        return ()
741
742    @staticmethod
743    def repack_mxfp4_blocks(packed: Tensor, scale: Tensor) -> np.ndarray:
744        """
745        Repack 4-bit MX weights into ggml `block_mxfp4`. Lossless - only moves bits.
746
747        Source (compressed-tensors "mxfp4-pack-quantized", also used by DeepSeek-V4):
748          packed  uint8 [rows, cols/2]  element 2i in the low nibble, 2i+1 in the high one
749          scale   uint8 [rows, cols/32] one E8M0 biased exponent per 32-element group
750
751        Destination, per group: one scale byte then 16 code bytes, where byte j holds
752        element j in the low nibble and element j+16 in the high one.
753
754        The 4-bit codes need no remapping: both sides index into ggml's kvalues_mxfp4
755        order. ggml doubles the kvalues and halves the scale, so the value is the same.
756        """
757        p = packed.contiguous().view(torch.uint8)
758        s = scale.contiguous().view(torch.uint8)
759
760        rows, packed_cols = p.shape
761        cols = packed_cols * 2
762        if cols % 32 != 0:
763            raise ValueError(f"MXFP4 source row has {cols} values, expected a multiple of 32")
764
765        n_blocks = cols // 32
766        if tuple(s.shape) != (rows, n_blocks):
767            raise ValueError(f"MXFP4 scale shape {tuple(s.shape)} does not match {(rows, n_blocks)}")
768
769        src = p.reshape(rows, n_blocks, 16)
770        lo = src & 0x0F           # elements 0, 2, 4, ...
771        hi = (src >> 4) & 0x0F    # elements 1, 3, 5, ...
772
773        vals = torch.stack((lo, hi), dim=-1).reshape(rows, n_blocks, 32)
774        qs = vals[:, :, :16] | (vals[:, :, 16:] << 4)
775
776        raw = torch.cat((s.unsqueeze(-1), qs.to(torch.uint8)), dim=-1)
777        return raw.reshape(rows, n_blocks * 17).cpu().numpy()
778
779    @staticmethod
780    def _nvfp4_pack(weight: Tensor, scale: Tensor) -> tuple[np.ndarray, list[int]]:
781        """Repack NVFP4 ModelOpt tensors into ggml super-block layout.
782        Preserves original E4M3 scale bits as UE4M3 (strip sign bit).
783        The per-tensor scale2 factor is stored as a separate tensor and applied at inference time via ggml_mul().
784        Returns (raw_data, logical_shape)."""
785
786        out_features = weight.shape[0]
787        n_blocks = scale.shape[1]
788
789        # Unpack ModelOpt nibble-packed weights
790        w = weight.reshape(out_features, n_blocks, 8)
791        vals = torch.stack([w & 0x0F, w >> 4], dim=-1).reshape(out_features, n_blocks, 16)
792
793        # Preserve original E4M3 scale bits as UE4M3 (strip sign bit)
794        d_ue = scale.view(torch.uint8).numpy().reshape(out_features, n_blocks) & 0x7F
795        qs = (vals[:, :, :8] | (vals[:, :, 8:] << 4)).to(torch.uint8).numpy()
796
797        # Pack into super-blocks: [4 UE4M3 scales, 32 qs bytes] = 36 bytes per 64 elements
798        n_super = n_blocks // 4
799        d_grouped = d_ue.reshape(out_features, n_super, 4)
800        qs_grouped = qs.reshape(out_features, n_super, 4, 8).reshape(out_features, n_super, 32)
801        raw = np.concatenate([d_grouped, qs_grouped], axis=-1).reshape(out_features, n_super * 36)
802        return raw, [out_features, n_super * 64]
803
804    def _repack_nvfp4(self, name: str, weight: Tensor, scale: Tensor, scale2: Tensor, input_scale: Tensor):
805        new_name = self.map_tensor_name(name)
806
807        raw, shape = self._nvfp4_pack(weight, scale)
808        logger.info(f"Repacked {new_name} with shape {shape} and quantization NVFP4")
809        self.gguf_writer.add_tensor(new_name, raw, raw_dtype=gguf.GGMLQuantizationType.NVFP4)
810
811        self._write_scale_tensor(new_name.replace(".weight", ".scale"), scale2)
812        self._write_scale_tensor(new_name.replace(".weight", ".input_scale"), input_scale)
813
814    def _generate_nvfp4_tensors(self):
815        # Per-layer expert merging to avoid holding all experts in memory
816        expert_blocks: dict[tuple[int, str], list[tuple[int, np.ndarray]]] = {}
817        expert_scales: dict[tuple[int, str], list[tuple[int, float]]] = {}
818        expert_input_scales: dict[tuple[int, str], list[tuple[int, float]]] = {}
819        expert_shapes: dict[tuple[int, str], list[int]] = {}
820        n_experts = self.find_hparam(["num_local_experts", "num_experts"], optional=True) or 0
821        consumed: list[str] = []
822
823        for name in self.model_tensors.keys():
824            if not name.endswith(".weight"):
825                continue
826            scale_name = name.replace(".weight", ".weight_scale")
827            scale2_name = name.replace(".weight", ".weight_scale_2")
828            input_scale_name = name.replace(".weight", ".input_scale")
829            if scale_name not in self.model_tensors:
830                continue
831            # Force eager materialization of lazy tensors
832            weight = LazyTorchTensor.to_eager(self.model_tensors[name]())
833            scale = LazyTorchTensor.to_eager(self.model_tensors[scale_name]())
834
835            # Skip non-NVFP4 tensors (e.g. FP8 with per-channel 1D scales)
836            if scale.ndim < 2:
837                continue
838
839            scale2 = LazyTorchTensor.to_eager(self.model_tensors.get(scale2_name, lambda: torch.tensor(1.0))())
840            input_scale = LazyTorchTensor.to_eager(self.model_tensors.get(input_scale_name, lambda: torch.tensor(1.0))())
841
842            # Mark tensors for removal from model_tensors (already written to gguf)
843            consumed.extend([name, scale_name])
844            if scale2_name in self.model_tensors:
845                consumed.append(scale2_name)
846            if input_scale_name in self.model_tensors:
847                consumed.append(input_scale_name)
848
849            # Check if this is a per-expert tensor
850            m = re.search(r'\.experts\.(\d+)\.(gate_proj|up_proj|down_proj)\.weight$', name)
851            if m:
852                expert_id = int(m.group(1))
853                proj_type = m.group(2)
854                bid_m = re.search(r'\.layers\.(\d+)\.', name)
855                bid = int(bid_m.group(1)) if bid_m else 0
856                key = (bid, proj_type)
857
858                raw, shape = self._nvfp4_pack(weight, scale)
859
860                if key not in expert_blocks:
861                    expert_blocks[key] = []
862                    expert_scales[key] = []
863                    expert_input_scales[key] = []
864                    expert_shapes[key] = shape
865                expert_blocks[key].append((expert_id, raw.copy()))
866                # Collect per-expert scale2 (scalar per expert)
867                expert_scales[key].append((expert_id, float(scale2.float().sum())))
868                # Collect per-expert input_scale (scalar per expert)
869                expert_input_scales[key].append((expert_id, float(input_scale.float().sum())))
870
871                # Flush when all experts for this (layer, proj) are collected
872                if n_experts > 0 and len(expert_blocks[key]) >= n_experts:
873                    self._flush_nvfp4_experts(key, expert_blocks, expert_scales, expert_input_scales, expert_shapes, bid, proj_type)
874            else:
875                self._repack_nvfp4(name, weight, scale, scale2, input_scale)
876
877        # Flush any remaining experts (fallback if n_experts was unknown)
878        for bid, proj_type in list(expert_blocks.keys()):
879            self._flush_nvfp4_experts((bid, proj_type), expert_blocks, expert_scales, expert_input_scales, expert_shapes, bid, proj_type)
880
881        # Remove consumed tensors so get_tensors/modify_tensors won't see them
882        for name in consumed:
883            self.model_tensors.pop(name, None)
884
885        # Remove any remaining unused auxiliary tensors
886        for name in list(self.model_tensors.keys()):
887            if name.endswith((".k_scale", ".v_scale")):
888                del self.model_tensors[name]
889
890    def _flush_nvfp4_experts(self, key, expert_blocks, expert_scales, expert_input_scales, expert_shapes, bid, proj_type):
891        experts = expert_blocks.pop(key)
892        scales = expert_scales.pop(key)
893        input_scales = expert_input_scales.pop(key)
894        shape = expert_shapes.pop(key)
895
896        experts.sort(key=lambda x: x[0])
897        merged = np.stack([e[1] for e in experts], axis=0)
898        merged_name = f"model.layers.{bid}.mlp.experts.{proj_type}.weight"
899        new_name = self.map_tensor_name(merged_name)
900        logger.info(f"Repacked {new_name} with shape [{len(experts)}, {shape[0]}, {shape[1]}] and quantization NVFP4")
901        self.gguf_writer.add_tensor(new_name, merged, raw_dtype=gguf.GGMLQuantizationType.NVFP4)
902
903        scales.sort(key=lambda x: x[0])
904        self._write_scales_tensor(new_name.replace(".weight", ".scale"), [s[1] for s in scales])
905
906        input_scales.sort(key=lambda x: x[0])
907        self._write_scales_tensor(new_name.replace(".weight", ".input_scale"), [s[1] for s in input_scales])
908
909        del experts, merged
910
911    def prepare_tensors(self):
912        # detect NVFP4 quantization (ModelOpt and Compressed-tensors formats)
913        quantization_config = self.hparams.get("quantization_config") or {}
914        quant_algo = quantization_config.get("quant_algo")
915        quant_method = quantization_config.get("quant_method")
916        quant_format = quantization_config.get("format")
917        quant_groups = quantization_config.get("config_groups") or {}
918        quant_layers = quantization_config.get("quantized_layers") or {}
919        quant_config_file = self.dir_model / "hf_quant_config.json"
920
921        if (not quant_algo or not quant_layers) and quant_config_file.is_file():
922            with open(quant_config_file, "r", encoding="utf-8") as f:
923                hf_quant_config = json.load(f)
924                quant_config = hf_quant_config.get("quantization") or {}
925                producer = hf_quant_config.get("producer") or {}
926                producer_name = (producer.get("name") or "").lower()
927                if quant_method is None:
928                    self.hparams.setdefault("quantization_config", {})["quant_method"] = producer_name
929                    quant_method = producer_name
930                quant_algo = quant_config.get("quant_algo", quant_algo)
931                quant_method = quant_config.get("quant_method", quant_method)
932                quant_format = quant_config.get("format", quant_format)
933                quant_groups = quant_config.get("config_groups", quant_groups) or {}
934                quant_layers = quant_config.get("quantized_layers", quant_layers) or {}
935
936        # Some models use per-tensor quant_algo (e.g. "MIXED_PRECISION" with
937        # per-layer NVFP4/FP8) instead of a single global "NVFP4" value.
938        nvfp4_compressed_tensors = quant_method == "compressed-tensors" and (
939            quant_format == "nvfp4-pack-quantized"
940            or quant_format == "mixed-precision"
941            and bool(quant_groups)
942            and all(g.get("format") == "nvfp4-pack-quantized" for g in quant_groups.values() if isinstance(g, dict))
943        )
944        if quant_algo != "NVFP4":
945            if nvfp4_compressed_tensors:
946                quant_algo = "NVFP4"
947            elif any(str(v.get("quant_algo")).endswith("NVFP4") for v in quant_layers.values() if isinstance(v, dict)):
948                quant_algo = "NVFP4"
949
950        self._is_nvfp4 = quant_algo in ("NVFP4", "W4A16_NVFP4")
951        self._is_mxfp4 = quant_method == "mxfp4"
952
953        # NVFP4 weights are repacked and written directly to gguf_writer.
954        # This must run before dequant_model so NVFP4 tensors are removed
955        # from model_tensors, leaving only non-NVFP4 (e.g. FP8) for dequant.
956        if self._is_nvfp4:
957            if nvfp4_compressed_tensors:
958                # Convert compressed-tensors 'global' scales into the reciprocal
959                def inverse_scale(gen):
960                    def load():
961                        scale = LazyTorchTensor.to_eager(gen()).float()
962                        return 1.0 / scale
963                    return load
964
965                # Change the compressed-tensors names to the ModelOpt names for handling consistently later
966                for name in list(self.model_tensors.keys()):
967                    if name.endswith(".weight_packed"):
968                        weight_name = name.removesuffix("_packed")
969                        if weight_name not in self.model_tensors:
970                            self.model_tensors[weight_name] = self.model_tensors.pop(name)
971                    elif name.endswith(".weight_global_scale"):
972                        scale2_name = name.replace(".weight_global_scale", ".weight_scale_2")
973                        if scale2_name not in self.model_tensors:
974                            self.model_tensors[scale2_name] = inverse_scale(self.model_tensors.pop(name))
975                    elif name.endswith(".input_global_scale"):
976                        input_scale_name = name.replace(".input_global_scale", ".input_scale")
977                        if input_scale_name not in self.model_tensors:
978                            self.model_tensors[input_scale_name] = inverse_scale(self.model_tensors.pop(name))
979            self._generate_nvfp4_tensors()
980
981        self.dequant_model()
982
983        self.prepare_qkv_fusion()
984
985        # Handle empty tensor_map for models with block_count=0 (like MobileNetV5)
986        if self.tensor_map.mapping:
987            max_name_len = max(len(s) for _, s in self.tensor_map.mapping.values()) + len(".weight,")
988        else:
989            max_name_len = len("vision_encoder.weight,")  # Default reasonable length
990
991        for name, data_torch in chain(self.generate_extra_tensors(), self.get_tensors()):
992            # we don't need these
993            if name.endswith((".attention.masked_bias", ".attention.bias", ".rotary_emb.inv_freq")):
994                continue
995
996            old_dtype = data_torch.dtype
997
998            # convert any unsupported data types to float32
999            if data_torch.dtype not in (torch.float16, torch.float32):
1000                data_torch = data_torch.to(torch.float32)
1001
1002            # use the first number-like part of the tensor name as the block id
1003            bid = None
1004            for part in name.split("."):
1005                if part.isdecimal():
1006                    bid = int(part)
1007                    break
1008
1009            for new_name, data_torch in (self.modify_tensors(data_torch, name, bid)):
1010                # TODO: why do we squeeze here?
1011                # data = data_torch.squeeze().numpy()
1012                data = data_torch.numpy()
1013
1014                n_dims = len(data.shape)
1015                data_qtype: gguf.GGMLQuantizationType | bool = self.tensor_force_quant(name, new_name, bid, n_dims)
1016
1017                # Most of the codebase that takes in 1D tensors or norms only handles F32 tensors
1018                if n_dims <= 1 or new_name.endswith("_norm.weight"):
1019                    data_qtype = gguf.GGMLQuantizationType.F32
1020
1021                # Conditions should closely match those in llama_model_quantize_internal in llama.cpp
1022                # Some tensor types are always in float32
1023                if data_qtype is False and (
1024                    any(
1025                        self.match_model_tensor_name(new_name, key, bid)
1026                        for key in (
1027                            gguf.MODEL_TENSOR.FFN_GATE_INP,
1028                            gguf.MODEL_TENSOR.FFN_GATE_INP_SHEXP,
1029                            gguf.MODEL_TENSOR.POS_EMBD,
1030                            gguf.MODEL_TENSOR.TOKEN_TYPES,
1031                            gguf.MODEL_TENSOR.SSM_CONV1D,
1032                            gguf.MODEL_TENSOR.SHORTCONV_CONV,
1033                            gguf.MODEL_TENSOR.TIME_MIX_FIRST,
1034                            gguf.MODEL_TENSOR.TIME_MIX_W1,
1035                            gguf.MODEL_TENSOR.TIME_MIX_W2,
1036                            gguf.MODEL_TENSOR.TIME_MIX_DECAY_W1,
1037                            gguf.MODEL_TENSOR.TIME_MIX_DECAY_W2,
1038                            gguf.MODEL_TENSOR.TIME_MIX_LERP_FUSED,
1039                            gguf.MODEL_TENSOR.POSNET_NORM1,
1040                            gguf.MODEL_TENSOR.POSNET_NORM2,
1041                            gguf.MODEL_TENSOR.V_ENC_EMBD_POS,
1042                            gguf.MODEL_TENSOR.A_ENC_EMBD_POS,
1043                            gguf.MODEL_TENSOR.ALTUP_CORRECT_COEF,
1044                            gguf.MODEL_TENSOR.ALTUP_PREDICT_COEF,
1045                            # Kimi KDA conv weights should be F32
1046                            gguf.MODEL_TENSOR.SSM_CONV1D_Q,
1047                            gguf.MODEL_TENSOR.SSM_CONV1D_K,
1048                            gguf.MODEL_TENSOR.SSM_CONV1D_V,
1049                            # DSA indexer weights should be F32
1050                            gguf.MODEL_TENSOR.INDEXER_PROJ,
1051                        )
1052                    )
1053                    or new_name[-7:] not in (".weight", ".lora_a", ".lora_b")
1054                ):
1055                    data_qtype = gguf.GGMLQuantizationType.F32
1056
1057                if data_qtype is False and any(
1058                    self.match_model_tensor_name(new_name, key, bid)
1059                    for key in (
1060                        gguf.MODEL_TENSOR.TOKEN_EMBD,
1061                        gguf.MODEL_TENSOR.PER_LAYER_TOKEN_EMBD,
1062                        gguf.MODEL_TENSOR.OUTPUT,
1063                        gguf.MODEL_TENSOR.ALTUP_ROUTER,
1064                        gguf.MODEL_TENSOR.LAUREL_L,
1065                        gguf.MODEL_TENSOR.LAUREL_R,
1066                    )
1067                ):
1068                    if self.ftype in (
1069                        gguf.LlamaFileType.MOSTLY_TQ1_0,
1070                        gguf.LlamaFileType.MOSTLY_TQ2_0,
1071                    ):
1072                        # TODO: use Q4_K and Q6_K
1073                        data_qtype = gguf.GGMLQuantizationType.F16
1074
1075                # No override (data_qtype is False), or wants to be quantized (data_qtype is True)
1076                if isinstance(data_qtype, bool):
1077                    if self.ftype == gguf.LlamaFileType.ALL_F32:
1078                        data_qtype = gguf.GGMLQuantizationType.F32
1079                    elif self.ftype == gguf.LlamaFileType.MOSTLY_F16:
1080                        data_qtype = gguf.GGMLQuantizationType.F16
1081                    elif self.ftype == gguf.LlamaFileType.MOSTLY_BF16:
1082                        data_qtype = gguf.GGMLQuantizationType.BF16
1083                    elif self.ftype == gguf.LlamaFileType.MOSTLY_Q8_0:
1084                        data_qtype = gguf.GGMLQuantizationType.Q8_0
1085                    elif self.ftype == gguf.LlamaFileType.MOSTLY_TQ1_0:
1086                        data_qtype = gguf.GGMLQuantizationType.TQ1_0
1087                    elif self.ftype == gguf.LlamaFileType.MOSTLY_TQ2_0:
1088                        data_qtype = gguf.GGMLQuantizationType.TQ2_0
1089                    else:
1090                        raise ValueError(f"Unknown file type: {self.ftype.name}")
1091
1092                # a chunked tensor quantizes as one chunk at a time, while it is written
1093                quantize = data.quantize if isinstance(data, gguf.LazyChunkedTensor) else (
1094                    lambda qtype, d=data: gguf.quants.quantize(d, qtype))
1095
1096                try:
1097                    data = quantize(data_qtype)
1098                except gguf.QuantError as e:
1099                    logger.warning("%s, %s", e, "falling back to F16")
1100                    data_qtype = gguf.GGMLQuantizationType.F16
1101                    data = quantize(data_qtype)
1102
1103                shape = gguf.quant_shape_from_byte_shape(data.shape, data_qtype) if data.dtype == np.uint8 else data.shape
1104
1105                # reverse shape to make it similar to the internal ggml dimension order
1106                shape_str = f"{{{', '.join(str(n) for n in reversed(shape))}}}"
1107
1108                # n_dims is implicit in the shape
1109                logger.info(f"{f'%-{max_name_len}s' % f'{new_name},'} {old_dtype} --> {data_qtype.name}, shape = {shape_str}")
1110
1111                self.gguf_writer.add_tensor(new_name, data, raw_dtype=data_qtype)
1112
1113        qkv_buffers = (
1114            self._q_buffer, self._k_buffer, self._v_buffer,
1115            self._q_bias_buffer, self._k_bias_buffer, self._v_bias_buffer,
1116        )
1117        if any(qkv_buffers):
1118            raise ValueError("QKV fusion did not consume all buffered tensors")
1119
1120    def set_type(self):
1121        self.gguf_writer.add_type(gguf.GGUFType.MODEL)
1122
1123    def prepare_metadata(self, vocab_only: bool):
1124
1125        total_params, shared_params, expert_params, expert_count = self.gguf_writer.get_total_parameter_count()
1126
1127        self.metadata = gguf.Metadata.load(self.metadata_override, self.dir_model_card, self.model_name, total_params)
1128
1129        # If we are using HF model id, set the metadata name to the model id
1130        if self.remote_hf_model_id:
1131            self.metadata.name = self.remote_hf_model_id
1132
1133        # Fallback to model directory name if metadata name is still missing
1134        if self.metadata.name is None:
1135            self.metadata.name = self.dir_model.name
1136
1137        if self.ftype in (gguf.LlamaFileType.ALL_F32, gguf.LlamaFileType.MOSTLY_F16, gguf.LlamaFileType.MOSTLY_BF16):
1138            if self._is_nvfp4:
1139                self.ftype = gguf.LlamaFileType.MOSTLY_NVFP4
1140            elif self._is_mxfp4:
1141                self.ftype = gguf.LlamaFileType.MOSTLY_MXFP4_MOE
1142
1143        # Generate parameter weight class (useful for leader boards) if not yet determined
1144        if self.metadata.size_label is None and total_params > 0:
1145            self.metadata.size_label = gguf.size_label(total_params, shared_params, expert_params, expert_count)
1146
1147        self.set_type()
1148
1149        logger.info("Set meta model")
1150        self.metadata.set_gguf_meta_model(self.gguf_writer)
1151
1152        logger.info("Set model parameters")
1153        self.set_gguf_parameters()
1154
1155        logger.info("Set model quantization version")
1156        self.gguf_writer.add_quantization_version(gguf.GGML_QUANT_VERSION)
1157
1158    def write_vocab(self):
1159        raise NotImplementedError("write_vocab() must be implemented in subclasses")
1160
1161    def write(self):
1162        self.prepare_tensors()
1163        self.prepare_metadata(vocab_only=False)
1164        self.gguf_writer.write_header_to_file(path=self.fname_out)
1165        self.gguf_writer.write_kv_data_to_file()
1166        self.gguf_writer.write_tensors_to_file(progress=True)
1167        self.gguf_writer.close()
1168
1169    @staticmethod
1170    def get_model_part_names(dir_model: Path, prefix: str, suffix: str) -> list[str]:
1171        part_names: list[str] = []
1172        for filename in os.listdir(dir_model):
1173            if filename.startswith(prefix) and filename.endswith(suffix):
1174                part_names.append(filename)
1175
1176        part_names.sort()
1177
1178        return part_names
1179
1180    @staticmethod
1181    def load_hparams_guess(dir_model: Path) -> dict[str, Any] | None:
1182        # some models ship no config.json, will try to guess them
1183        from conversion import load_all_models
1184        load_all_models()
1185
1186        for matcher, loader in ModelBase._hparams_loaders:
1187            if matcher(dir_model):
1188                return loader(dir_model)
1189        return None
1190
1191    @classmethod
1192    def register_hparams_loader(cls, matcher: HparamsMatcher) -> Callable[[HparamsLoader], HparamsLoader]:
1193        def inner(loader: HparamsLoader) -> HparamsLoader:
1194            cls._hparams_loaders.append((matcher, loader))
1195            return loader
1196        return inner
1197
1198    @staticmethod
1199    def load_hparams(dir_model: Path, is_mistral_format: bool):
1200        if is_mistral_format:
1201            with open(dir_model / "params.json", "r", encoding="utf-8") as f:
1202                config = json.load(f)
1203            return config
1204
1205        try:
1206            # for security reason, we don't allow loading remote code by default
1207            # if a model need remote code, we will fallback to config.json
1208            config = AutoConfig.from_pretrained(dir_model, trust_remote_code=False).to_dict()
1209        except Exception as e:
1210            logger.warning(f"Failed to load model config from {dir_model}: {e}")
1211            if not (dir_model / "config.json").is_file():
1212                config = ModelBase.load_hparams_guess(dir_model)
1213                if config is not None:
1214                    return config
1215            logger.warning("Trying to load config.json instead")
1216            with open(dir_model / "config.json", "r", encoding="utf-8") as f:
1217                config = json.load(f)
1218        if "llm_config" in config:
1219            # rename for InternVL
1220            config["text_config"] = config["llm_config"]
1221        if "lm_config" in config:
1222            # rename for GlmASR
1223            config["text_config"] = config["lm_config"]
1224        if "thinker_config" in config:
1225            # rename for Qwen2.5-Omni
1226            config["text_config"] = config["thinker_config"]["text_config"]
1227        if "language_config" in config:
1228            # rename for DeepSeekOCR
1229            config["text_config"] = config["language_config"]
1230        if "lfm" in config:
1231            # rename for LFM2-Audio
1232            config["text_config"] = config["lfm"]
1233        return config
1234
1235    @classmethod
1236    def register(cls, *names: str) -> Callable[[AnyModel], AnyModel]:
1237        assert names
1238
1239        def func(modelcls: AnyModel) -> AnyModel:
1240            model_type = ModelType.MMPROJ if modelcls.model_arch == gguf.MODEL_ARCH.MMPROJ else ModelType.TEXT
1241            for name in names:
1242                cls._model_classes[model_type][name] = modelcls
1243            return modelcls
1244        return func
1245
1246    @classmethod
1247    def example(cls, *hf_repos: str) -> Callable[[AnyModel], AnyModel]:
1248        del hf_repos  # unused
1249
1250        def func(modelcls: AnyModel) -> AnyModel:
1251            return modelcls
1252        return func
1253
1254    @classmethod
1255    def print_registered_models(cls):
1256        for model_type, model_classes in cls._model_classes.items():
1257            logger.error(f"{model_type.name} models:")
1258            for name in sorted(model_classes.keys()):
1259                logger.error(f"  - {name}")
1260
1261    @classmethod
1262    def from_model_architecture(cls, arch: str, model_type = ModelType.TEXT) -> type[ModelBase]:
1263        try:
1264            return cls._model_classes[model_type][arch]
1265        except KeyError:
1266            raise NotImplementedError(f'Architecture {arch!r} not supported!') from None
1267
1268
1269class TextModel(ModelBase):
1270    model_type = ModelType.TEXT
1271    hf_arch: str
1272
1273    def __init__(self, *args, **kwargs):
1274        super().__init__(*args, **kwargs)
1275        if not self.is_mistral_format:
1276            self.hf_arch = get_model_architecture(self.hparams, self.model_type)
1277        else:
1278            self.hf_arch = ""
1279
1280        if "text_config" in self.hparams:
1281            # move the text_config to the root level
1282            self.hparams = {**self.hparams, **self.hparams["text_config"]}
1283
1284        self.block_count = self.find_hparam(["n_layers", "num_hidden_layers", "n_layer", "num_layers"])
1285        self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count)
1286
1287        self.rope_parameters = self.hparams.get("rope_parameters", self.hparams.get("rope_scaling")) or {}
1288
1289        rope_theta = self.find_hparam(["global_rope_theta", "rope_global_theta", "rope_theta_global", "rope_theta", "rotary_emb_base"], optional=True)
1290        local_rope_theta = self.find_hparam(["local_rope_theta", "rope_local_theta", "rope_theta_local", "swa_rope_theta", "rope_local_base_freq"], optional=True)
1291        partial_rotary_factor = self.find_hparam(["partial_rotary_factor", "rope_pct", "rope_percent"], optional=True)
1292        original_max_position_embeddings = self.find_hparam(["original_max_position_embeddings"], optional=True)
1293
1294        # Ensure global params are mirrored in rope_parameters
1295        if "full_attention" not in self.rope_parameters and "sliding_attention" not in self.rope_parameters:
1296            if local_rope_theta is not None:
1297                self.rope_parameters["sliding_attention"] = {"rope_theta": local_rope_theta}
1298            if "rope_theta" not in self.rope_parameters and rope_theta is not None:
1299                self.rope_parameters["rope_theta"] = rope_theta
1300            if "rope_type" not in self.rope_parameters and (rope_type := self.rope_parameters.get("type")) is not None:
1301                self.rope_parameters["rope_type"] = rope_type
1302            if "partial_rotary_factor" not in self.rope_parameters and partial_rotary_factor is not None:
1303                self.rope_parameters["partial_rotary_factor"] = partial_rotary_factor
1304            if "original_max_position_embeddings" not in self.rope_parameters and original_max_position_embeddings is not None:
1305                self.rope_parameters["original_max_position_embeddings"] = original_max_position_embeddings
1306
1307    @classmethod
1308    def __init_subclass__(cls):
1309        # can't use an abstract property, because overriding it without type errors
1310        # would require using decorated functions instead of simply defining the property
1311        if "model_arch" not in cls.__dict__:
1312            raise TypeError(f"Missing property 'model_arch' for {cls.__name__!r}")
1313
1314    @classmethod
1315    def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
1316        name, gen = item
1317
1318        # Skip multimodal tensors
1319        if name.startswith(("mlp", "vit.", "vpm.", "siglip2.", "conformer.", "merger.", "resampler.", "sound_encoder.", "sound_projection.", "speech_embeddings.")) \
1320                or "visual." in name or "vision." in name or "audio." in name or "talker." in name \
1321                or "vision_" in name or "audio_" in name \
1322                or "token2wav." in name or "code2wav." in name \
1323                or "projector." in name or "pre_mm_projector_norm" in name \
1324                or "image_newline" in name or "view_seperator" in name \
1325                or "patch_embed" in name or "patch_embedding" in name \
1326                or "patch_merger." in name or "patch_merge_mlp." in name or "model.connector." in name:
1327            return None
1328
1329        return super().filter_tensors(item)
1330
1331    def set_vocab(self):
1332        self._set_vocab_gpt2()
1333
1334    def prepare_metadata(self, vocab_only: bool):
1335        super().prepare_metadata(vocab_only=vocab_only)
1336
1337        total_params = self.gguf_writer.get_total_parameter_count()[0]
1338        # Extract the encoding scheme from the file type name. e.g. 'gguf.LlamaFileType.MOSTLY_Q8_0' --> 'Q8_0'
1339        output_type: str = self.ftype.name.partition("_")[2]
1340
1341        # Filename Output
1342        if self.fname_out.is_dir():
1343            # Generate default filename based on model specification and available metadata
1344            if not vocab_only:
1345                fname_default: str = gguf.naming_convention(self.metadata.name, self.metadata.basename, self.metadata.finetune, self.metadata.version, self.metadata.size_label, output_type, model_type="LoRA" if total_params < 0 else None)
1346            else:
1347                fname_default: str = gguf.naming_convention(self.metadata.name, self.metadata.basename, self.metadata.finetune, self.metadata.version, size_label=None, output_type=None, model_type="vocab")
1348
1349            # Use the default filename
1350            self.fname_out = self.fname_out / f"{fname_default}.gguf"
1351        else:
1352            # Output path is a custom defined templated filename
1353            # Note: `not is_dir()` is used because `.is_file()` will not detect
1354            #       file template strings as it doesn't actually exist as a file
1355
1356            # Process templated file name with the output ftype, useful with the "auto" ftype
1357            self.fname_out = self.fname_out.parent / gguf.fill_templated_filename(self.fname_out.name, output_type)
1358
1359        logger.info("Set model tokenizer")
1360        self.set_vocab()
1361
1362    def set_gguf_parameters(self):
1363        self.gguf_writer.add_block_count(self.block_count)
1364
1365        if (n_ctx := self.find_hparam(["max_position_embeddings", "n_ctx", "n_positions", "max_length", "max_sequence_length", "model_max_length"], optional=True)) is not None:
1366            self.gguf_writer.add_context_length(n_ctx)
1367            logger.info(f"gguf: context length = {n_ctx}")
1368
1369        if (n_embd := self.find_hparam(["hidden_size", "n_embd", "dim"], optional=True)) is not None:
1370            self.gguf_writer.add_embedding_length(n_embd)
1371            logger.info(f"gguf: embedding length = {n_embd}")
1372
1373        if (n_ff := self.find_hparam(["prefix_dense_intermediate_size", "dense_intermediate_size", "intermediate_size", "n_inner", "hidden_dim"], optional=True)) is not None:
1374            self.gguf_writer.add_feed_forward_length(n_ff)
1375            logger.info(f"gguf: feed forward length = {n_ff}")
1376
1377        if (n_head := self.find_hparam(["num_attention_heads", "n_head", "n_heads"], optional=True)) is not None:
1378            self.gguf_writer.add_head_count(n_head)
1379            logger.info(f"gguf: head count = {n_head}")
1380
1381        if (n_head_kv := self.find_hparam(["num_key_value_heads", "n_kv_heads"], optional=True)) is not None:
1382            self.gguf_writer.add_head_count_kv(n_head_kv)
1383            logger.info(f"gguf: key-value head count = {n_head_kv}")
1384
1385        if self.hparams.get("is_causal") is False:
1386            self.gguf_writer.add_causal_attention(False)
1387            logger.info("gguf: causal attention = False")
1388
1389        # TODO: Handle "sliding_attention" similarly when models start implementing it
1390        rope_params = self.rope_parameters.get("full_attention", self.rope_parameters)
1391        if (rope_type := rope_params.get("rope_type")) is not None:
1392            rope_factor = rope_params.get("factor")
1393            rope_gguf_type = gguf.RopeScalingType.NONE
1394            if rope_type == "linear" and rope_factor is not None:
1395                rope_gguf_type = gguf.RopeScalingType.LINEAR
1396                self.gguf_writer.add_rope_scaling_type(rope_gguf_type)
1397                self.gguf_writer.add_rope_scaling_factor(rope_factor)
1398            elif rope_type == "yarn" and rope_factor is not None:
1399                rope_gguf_type = gguf.RopeScalingType.YARN
1400                self.gguf_writer.add_rope_scaling_type(rope_gguf_type)
1401                self.gguf_writer.add_rope_scaling_factor(rope_factor)
1402                self.gguf_writer.add_rope_scaling_orig_ctx_len(rope_params["original_max_position_embeddings"])
1403                if (yarn_ext_factor := rope_params.get("extrapolation_factor")) is not None:
1404                    self.gguf_writer.add_rope_scaling_yarn_ext_factor(yarn_ext_factor)
1405                if (yarn_attn_factor := rope_params.get("attention_factor", rope_params.get("attn_factor"))) is not None:
1406                    self.gguf_writer.add_rope_scaling_yarn_attn_factor(yarn_attn_factor)
1407                if (yarn_beta_fast := rope_params.get("beta_fast")) is not None:
1408                    self.gguf_writer.add_rope_scaling_yarn_beta_fast(yarn_beta_fast)
1409                if (yarn_beta_slow := rope_params.get("beta_slow")) is not None:
1410                    self.gguf_writer.add_rope_scaling_yarn_beta_slow(yarn_beta_slow)
1411                # self.gguf_writer.add_rope_scaling_yarn_log_mul(rope_params["mscale_all_dim"])
1412            elif rope_type == "su" or rope_type == "longrope":
1413                rope_gguf_type = gguf.RopeScalingType.LONGROPE
1414                self.gguf_writer.add_rope_scaling_type(rope_gguf_type)
1415            elif rope_type == "dynamic":
1416                # HunYuan, handled in model class
1417                pass
1418            elif rope_type.lower() == "llama3":
1419                # Handled in generate_extra_tensors
1420                pass
1421            else:
1422                logger.warning(f"Unknown RoPE type: {rope_type}")
1423            logger.info(f"gguf: rope scaling type = {rope_gguf_type.name}")
1424
1425        if "mrope_section" in self.rope_parameters:
1426            mrope_section = self.rope_parameters["mrope_section"]
1427            # Pad to 4 dimensions [time, height, width, extra]
1428            while len(mrope_section) < 4:
1429                mrope_section.append(0)
1430            self.gguf_writer.add_rope_dimension_sections(mrope_section[:4])
1431            logger.info(f"gguf: mrope sections: {mrope_section[:4]}")
1432
1433        if (rope_theta := rope_params.get("rope_theta")) is not None:
1434            self.gguf_writer.add_rope_freq_base(rope_theta)
1435            logger.info(f"gguf: rope theta = {rope_theta}")
1436        if (local_rope_theta := self.rope_parameters.get("sliding_attention", {}).get("rope_theta")) is not None:
1437            self.gguf_writer.add_rope_freq_base_swa(local_rope_theta)
1438            logger.info(f"gguf: rope theta swa = {local_rope_theta}")
1439        if (f_rms_eps := self.find_hparam(["rms_norm_eps", "norm_eps"], optional=True)) is not None:
1440            self.gguf_writer.add_layer_norm_rms_eps(f_rms_eps)
1441            logger.info(f"gguf: rms norm epsilon = {f_rms_eps}")
1442        if (f_norm_eps := self.find_hparam(["layer_norm_eps", "layer_norm_epsilon", "norm_epsilon"], optional=True)) is not None:
1443            self.gguf_writer.add_layer_norm_eps(f_norm_eps)
1444            logger.info(f"gguf: layer norm epsilon = {f_norm_eps}")
1445        if (n_experts := self.find_hparam(["num_local_experts", "num_experts", "n_routed_experts"], optional=True)) is not None:
1446            self.gguf_writer.add_expert_count(n_experts)
1447            logger.info(f"gguf: expert count = {n_experts}")
1448        if (n_experts_used := self.find_hparam(["num_experts_per_tok", "num_experts_per_token", "top_k_experts"], optional=True)) is not None:
1449            self.gguf_writer.add_expert_used_count(n_experts_used)
1450            logger.info(f"gguf: experts used count = {n_experts_used}")
1451        if (n_expert_groups := self.hparams.get("n_group")) is not None:
1452            self.gguf_writer.add_expert_group_count(n_expert_groups)
1453            logger.info(f"gguf: expert groups count = {n_expert_groups}")
1454        if (n_group_used := self.hparams.get("topk_group")) is not None:
1455            self.gguf_writer.add_expert_group_used_count(n_group_used)
1456            logger.info(f"gguf: expert groups used count = {n_group_used}")
1457
1458        if (score_func := self.find_hparam(["score_function", "scoring_func", "score_func", "moe_router_activation", "moe_router_activation_func", "expert_selection_fn"], optional=True)) is not None:
1459            if score_func == "sigmoid":
1460                self.gguf_writer.add_expert_gating_func(gguf.ExpertGatingFuncType.SIGMOID)
1461            elif score_func == "softmax":
1462                self.gguf_writer.add_expert_gating_func(gguf.ExpertGatingFuncType.SOFTMAX)
1463            elif score_func == "sqrtsoftplus":
1464                self.gguf_writer.add_expert_gating_func(gguf.ExpertGatingFuncType.SQRTSOFTPLUS)
1465            else:
1466                raise ValueError(f"Unsupported expert score gating function value: {score_func}")
1467            logger.info(f"gguf: expert score gating function = {score_func}")
1468
1469        if (head_dim := self.hparams.get("head_dim")) is not None:
1470            self.gguf_writer.add_key_length(head_dim)
1471            self.gguf_writer.add_value_length(head_dim)
1472
1473        self.gguf_writer.add_file_type(self.ftype)
1474        logger.info(f"gguf: file type = {self.ftype}")
1475
1476    def write_vocab(self):
1477        if len(self.gguf_writer.tensors) != 1:
1478            raise ValueError('Splitting the vocabulary is not supported')
1479
1480        self.prepare_metadata(vocab_only=True)
1481        self.gguf_writer.write_header_to_file(path=self.fname_out)
1482        self.gguf_writer.write_kv_data_to_file()
1483        self.gguf_writer.close()
1484
1485    def does_token_look_special(self, token: str | bytes) -> bool:
1486        if isinstance(token, (bytes, bytearray)):
1487            token_text = token.decode(encoding="utf-8")
1488        elif isinstance(token, memoryview):
1489            token_text = token.tobytes().decode(encoding="utf-8")
1490        else:
1491            token_text = token
1492
1493        # Some models mark some added tokens which ought to be control tokens as not special.
1494        # (e.g. command-r, command-r-plus, deepseek-coder, gemma{,-2})
1495        seems_special = token_text in (
1496            "<pad>",  # deepseek-coder
1497            "<mask>", "<2mass>", "[@BOS@]",  # gemma{,-2}
1498        )
1499
1500        seems_special = seems_special or (token_text.startswith("<|") and token_text.endswith("|>"))
1501        seems_special = seems_special or (token_text.startswith("<|") and token_text.endswith("|>"))  # deepseek-coder
1502
1503        # TODO: should these be marked as UNUSED instead? (maybe not)
1504        seems_special = seems_special or (token_text.startswith("<unused") and token_text.endswith(">"))  # gemma{,-2}
1505
1506        return seems_special
1507
1508    # used for GPT-2 BPE and WordPiece vocabs
1509    def get_vocab_base(self) -> tuple[list[str], list[int], str]:
1510        tokens: list[str] = []
1511        toktypes: list[int] = []
1512
1513        from transformers import AutoTokenizer
1514        tokenizer = AutoTokenizer.from_pretrained(self.dir_model)
1515        vocab_size = self.hparams.get("vocab_size", len(tokenizer.vocab))  # ty: ignore[unresolved-attribute]
1516        assert max(tokenizer.vocab.values()) < vocab_size  # ty: ignore[unresolved-attribute]
1517
1518        tokpre = self.get_vocab_base_pre(tokenizer)
1519
1520        reverse_vocab = {id_: encoded_tok for encoded_tok, id_ in tokenizer.vocab.items()}  # ty: ignore[unresolved-attribute]
1521        added_vocab = tokenizer.get_added_vocab()  # ty: ignore[unresolved-attribute]
1522
1523        added_tokens_decoder = tokenizer.added_tokens_decoder  # ty: ignore[unresolved-attribute]
1524
1525        for i in range(vocab_size):
1526            if i not in reverse_vocab:
1527                tokens.append(f"[PAD{i}]")
1528                toktypes.append(gguf.TokenType.UNUSED)
1529            else:
1530                token: str = reverse_vocab[i]
1531                if token in added_vocab:
1532                    # The tokenizer in llama.cpp assumes the CONTROL and USER_DEFINED tokens are pre-normalized.
1533                    # To avoid unexpected issues - we make sure to normalize non-normalized tokens
1534                    if not added_tokens_decoder[i].normalized:
1535                        previous_token = token
1536                        token = tokenizer.decode(tokenizer.encode(token, add_special_tokens=False))  # ty: ignore[unresolved-attribute, invalid-assignment]
1537                        if previous_token != token:
1538                            logger.info(f"{repr(previous_token)} is encoded and decoded back to {repr(token)} using AutoTokenizer")
1539
1540                    if added_tokens_decoder[i].special or self.does_token_look_special(token):
1541                        toktypes.append(gguf.TokenType.CONTROL)
1542                    else:
1543                        # NOTE: this was added for Gemma.
1544                        # Encoding and decoding the tokens above isn't sufficient for this case.
1545                        token = token.replace(b"\xe2\x96\x81".decode("utf-8"), " ")  # pre-normalize user-defined spaces
1546                        toktypes.append(gguf.TokenType.USER_DEFINED)
1547                else:
1548                    toktypes.append(gguf.TokenType.NORMAL)
1549                tokens.append(token)
1550
1551        return tokens, toktypes, tokpre
1552
1553    # NOTE: this function is generated by convert_hf_to_gguf_update.py
1554    #       do not modify it manually!
1555    # ref:  https://github.com/ggml-org/llama.cpp/pull/6920
1556    # Marker: Start get_vocab_base_pre
1557    def get_vocab_base_pre(self, tokenizer) -> str:
1558        # encoding this string and hashing the resulting tokens would (hopefully) give us a unique identifier that
1559        # is specific for the BPE pre-tokenizer used by the model
1560        # we will use this unique identifier to write a "tokenizer.ggml.pre" entry in the GGUF file which we can
1561        # use in llama.cpp to implement the same pre-tokenizer
1562
1563        chktxt = '\n \n\n \n\n\n \t \t\t \t\n  \n   \n    \n     \n🚀 (normal) 😶\u200d🌫️ (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'
1564
1565        chktok = tokenizer.encode(chktxt)
1566        chkhsh = sha256(str(chktok).encode()).hexdigest()
1567
1568        logger.debug(f"chktok: {chktok}")
1569        logger.debug(f"chkhsh: {chkhsh}")
1570
1571        res = None
1572
1573        # NOTE: if you get an error here, you need to update the convert_hf_to_gguf_update.py script
1574        #       or pull the latest version of the model from Huggingface
1575        #       don't edit the hashes manually!
1576        if chkhsh == "b6e8e1518dc4305be2fe39c313ed643381c4da5db34a98f6a04c093f8afbe99b":
1577            # ref: https://huggingface.co/THUDM/glm-4-9b-chat
1578            res = "chatglm-bpe"
1579        if chkhsh == "81d72c7348a9f0ebe86f23298d37debe0a5e71149e29bd283904c02262b27516":
1580            # ref: https://huggingface.co/THUDM/glm-4-9b-chat
1581            res = "chatglm-bpe"
1582        if chkhsh == "a1336059768a55c99a734006ffb02203cd450fed003e9a71886c88acf24fdbc2":
1583            # ref: https://huggingface.co/THUDM/glm-4-9b-hf
1584            res = "glm4"
1585        if chkhsh == "9ca2dd618e8afaf09731a7cf6e2105b373ba6a1821559f258b272fe83e6eb902":
1586            # ref: https://huggingface.co/zai-org/GLM-4.5-Air
1587            res = "glm4"
1588        if chkhsh == "cdf5f35325780597efd76153d4d1c16778f766173908894c04afc20108536267":
1589            # ref: https://huggingface.co/zai-org/GLM-4.7-Flash
1590            res = "glm4"
1591        if chkhsh == "1431a23e583c97432bc230bff598d103ddb5a1f89960c8f1d1051aaa944d0b35":
1592            # ref: https://huggingface.co/sapienzanlp/Minerva-7B-base-v1.0
1593            res = "minerva-7b"
1594        if chkhsh == "7e57df22b1fe23a7b1e1c7f3dc4e3f96d43a4eb0836d0c6bdc3436d7b2f1c664":
1595            # ref: https://huggingface.co/tencent/Hunyuan-A13B-Instruct
1596            res = "hunyuan"
1597        if chkhsh == "bba3b3366b646dbdded5dbc42d59598b849371afc42f7beafa914afaa5b70aa6":
1598            # ref: https://huggingface.co/tencent/Hunyuan-4B-Instruct
1599            res = "hunyuan-dense"
1600        if chkhsh == "e6ddf9c6686791c12d698d34c31ab9be1fea9af5a3d9a6909783ab382198ae1c":
1601            # ref: https://huggingface.co/tencent/Hy4-preview
1602            res = "hy_v4"
1603        if chkhsh == "a6b57017d60e6edb4d88ecc2845188e0eb333a70357e45dcc9b53964a73bbae6":
1604            # ref: https://huggingface.co/tiiuae/Falcon-H1-0.5B-Base
1605            res = "falcon-h1"
1606        if chkhsh == "60476e1243776c4fb1b993dbd7a5f15ac22f83c80afdf425fa5ae01c8d44ef86":
1607            # ref: https://huggingface.co/tiiuae/Falcon-H1-1B-Base
1608            res = "falcon-h1"
1609        if chkhsh == "3eda48b4c4dc7de733d1a8b3e3b4a85243dbbf704da2ee9d42c6beced8897896":
1610            # ref: https://huggingface.co/tiiuae/Falcon-H1-7B-Base
1611            res = "falcon-h1"
1612        if chkhsh == "48f8e02c0359c0bbdd82f26909171fac1c18a457bb47573ed1fe3bbb2c1cfd4b":
1613            # ref: https://huggingface.co/tiiuae/Falcon-H1-34B-Base
1614            res = "falcon-h1"
1615        if chkhsh == "81212dc7cdb7e0c1074ca62c5aeab0d43c9f52b8a737be7b12a777c953027890":
1616            # ref: https://huggingface.co/moonshotai/Kimi-K2-Base
1617            res = "kimi-k2"
1618        if chkhsh == "d4540891389ea895b53b399da6ac824becc30f2fba0e9ddbb98f92e55ca0e97c":
1619            # ref: https://huggingface.co/Qwen/Qwen3-Embedding-0.6B
1620            res = "qwen2"
1621        if chkhsh == "1444df51289cfa8063b96f0e62b1125440111bc79a52003ea14b6eac7016fd5f":
1622            # ref: https://huggingface.co/openbmb/MiniCPM-V-4_6
1623            res = "qwen35"
1624        if chkhsh == "66b8d4e19ab16c3bfd89bce5d785fb7e0155e8648708a1f42077cb9fe002c273":
1625            # ref: https://huggingface.co/alvarobartt/grok-2-tokenizer
1626            res = "grok-2"
1627        if chkhsh == "b3d1dd861f1d4c5c0d2569ce36baf3f90fe8a102db3de50dd71ff860d91be3df":
1628            # ref: https://huggingface.co/aari1995/German_Semantic_V3
1629            res = "jina-v2-de"
1630        if chkhsh == "0fe1cf6eda062318a1af7270f3331a85c539a01778ff948e24388e949c5282f4":
1631            # ref: https://huggingface.co/evilfreelancer/ruGPT3XL
1632            res = "gpt-2"
1633        if chkhsh == "9e454714343b69b99b71795c1d27a68c2a1d15dab111f4d353109f966af29da7":
1634            # ref: https://huggingface.co/LiquidAI/LFM2.5-8B-A1B
1635            res = "lfm2"
1636        if chkhsh == "0a766d034107bc736a3f2dc4968fd62e54a3570f1454443e0c5a4cc6bd7941ed":
1637            # ref: https://huggingface.co/XHToken/Spark-X2.5-1.7B
1638            res = "spark2_5"
1639        if chkhsh == "0ef9807a4087ebef797fc749390439009c3b9eda9ad1a097abbe738f486c01e5":
1640            # ref: https://huggingface.co/meta-llama/Meta-Llama-3-8B
1641            res = "llama-bpe"
1642        if chkhsh == "049ecf7629871e3041641907f3de7c733e4dbfdc736f57d882ba0b0845599754":
1643            # ref: https://huggingface.co/deepseek-ai/deepseek-llm-7b-base
1644            res = "deepseek-llm"
1645        if chkhsh == "347715f544604f9118bb75ed199f68779f423cabb20db6de6f31b908d04d7821":
1646            # ref: https://huggingface.co/deepseek-ai/deepseek-coder-6.7b-base
1647            res = "deepseek-coder"
1648        if chkhsh == "8aeee3860c56296a157a1fe2fad249ec40aa59b1bb5709f4ade11c4e6fe652ed":
1649            # ref: https://huggingface.co/tiiuae/falcon-7b
1650            res = "falcon"
1651        if chkhsh == "0876d13b50744004aa9aeae05e7b0647eac9d801b5ba4668afc01e709c15e19f":
1652            # ref: https://huggingface.co/BAAI/bge-small-en-v1.5
1653            res = "bert-bge"
1654        if chkhsh == "9d032fcbd5501f4a38150912590928bfb36091efb5df11b8e2124b0390e3fb1e":
1655            # ref: https://huggingface.co/tiiuae/Falcon3-7B-Base
1656            res = "falcon3"
1657        if chkhsh == "8e62295832751ca1e8f92f2226f403dea30dc5165e448b5bfa05af5340c64ec7":
1658            # ref: https://huggingface.co/BAAI/bge-large-zh-v1.5
1659            res = "bert-bge-large"
1660        if chkhsh == "b6dc8df998e1cfbdc4eac8243701a65afe638679230920b50d6f17d81c098166":
1661            # ref: https://huggingface.co/mosaicml/mpt-7b
1662            res = "mpt"
1663        if chkhsh == "35d91631860c815f952d711435f48d356ebac988362536bed955d43bfa436e34":
1664            # ref: https://huggingface.co/bigcode/starcoder2-3b
1665            res = "starcoder"
1666        if chkhsh == "3ce83efda5659b07b1ad37ca97ca5797ea4285d9b9ab0dc679e4a720c9da7454":
1667            # ref: https://huggingface.co/openai-community/gpt2
1668            res = "gpt-2"
1669        if chkhsh == "32d85c31273f8019248f2559fed492d929ea28b17e51d81d3bb36fff23ca72b3":
1670            # ref: https://huggingface.co/stabilityai/stablelm-2-zephyr-1_6b
1671            res = "stablelm2"
1672        if chkhsh == "6221ad2852e85ce96f791f476e0b390cf9b474c9e3d1362f53a24a06dc8220ff":
1673            # ref: https://huggingface.co/smallcloudai/Refact-1_6-base
1674            res = "refact"
1675        if chkhsh == "9c2227e4dd922002fb81bde4fc02b0483ca4f12911410dee2255e4987644e3f8":
1676            # ref: https://huggingface.co/CohereForAI/c4ai-command-r-v01
1677            res = "command-r"
1678        if chkhsh == "d772b220ace2baec124bed8cfafce0ead7d6c38a4b65ef11261cf9d5d62246d1":
1679            # ref: https://huggingface.co/CohereLabs/tiny-aya-base
1680            res = "tiny_aya"
1681        if chkhsh == "52df12b4c8d4176e7481aab4b6e8454d1fd0a210a04a574f6d4e067d10e23c3e":
1682            # ref: https://huggingface.co/CohereLabs/North-Mini-Code-1.0
1683            res = "cohere2moe"
1684        if chkhsh == "e636dc30a262dcc0d8c323492e32ae2b70728f4df7dfe9737d9f920a282b8aea":
1685            # ref: https://huggingface.co/Qwen/Qwen1.5-7B
1686            res = "qwen2"
1687        if chkhsh == "b6dc8df998e1cfbdc4eac8243701a65afe638679230920b50d6f17d81c098166":
1688            # ref: https://huggingface.co/allenai/OLMo-1.7-7B-hf
1689            res = "olmo"
1690        if chkhsh == "a8594e3edff7c29c003940395316294b2c623e09894deebbc65f33f1515df79e":
1691            # ref: https://huggingface.co/databricks/dbrx-base
1692            res = "dbrx"
1693        if chkhsh == "c7699093ba4255a91e702aa38a596aa81669f3525dae06c2953267dde580f448":
1694            # ref: https://huggingface.co/jinaai/jina-reranker-v1-tiny-en
1695            res = "jina-v1-en"
1696        if chkhsh == "0876d13b50744004aa9aeae05e7b0647eac9d801b5ba4668afc01e709c15e19f":
1697            # ref: https://huggingface.co/jinaai/jina-embeddings-v2-base-en
1698            res = "jina-v2-en"
1699        if chkhsh == "171aeeedd6fb548d418a7461d053f11b6f1f1fc9b387bd66640d28a4b9f5c643":
1700            # ref: https://huggingface.co/jinaai/jina-embeddings-v2-base-es
1701            res = "jina-v2-es"
1702        if chkhsh == "27949a2493fc4a9f53f5b9b029c82689cfbe5d3a1929bb25e043089e28466de6":
1703            # ref: https://huggingface.co/jinaai/jina-embeddings-v2-base-de
1704            res = "jina-v2-de"
1705        if chkhsh == "a023e9fdc5a11f034d3ef515b92350e56fb2af1f66c6b6811a4444ea9bf8763d":
1706            # ref: https://huggingface.co/jinaai/jina-embeddings-v5-text-nano
1707            res = "jina-v5-nano"
1708        if chkhsh == "c136ed14d01c2745d4f60a9596ae66800e2b61fa45643e72436041855ad4089d":
1709            # ref: https://huggingface.co/abacusai/Smaug-Llama-3-70B-Instruct
1710            res = "smaug-bpe"
1711        if chkhsh == "c7ea5862a53e4272c035c8238367063e2b270d51faa48c0f09e9d5b54746c360":
1712            # ref: https://huggingface.co/LumiOpen/Poro-34B-chat
1713            res = "poro-chat"
1714        if chkhsh == "7967bfa498ade6b757b064f31e964dddbb80f8f9a4d68d4ba7998fcf281c531a":
1715            # ref: https://huggingface.co/jinaai/jina-embeddings-v2-base-code
1716            res = "jina-v2-code"
1717        if chkhsh == "7fc505bd3104ca1083b150b17d088b59534ede9bde81f0dd2090967d7fe52cee":
1718            # ref: https://huggingface.co/LumiOpen/Viking-7B
1719            res = "viking"
1720        if chkhsh == "b53802fb28e26d645c3a310b34bfe07da813026ec7c7716883404d5e0f8b1901":
1721            # ref: https://huggingface.co/core42/jais-13b
1722            res = "jais"
1723        if chkhsh == "bc5108ee1eb6a3d600cadd065f63190fbd0554dbc9e4bbd6a0d977970afc8d2a":
1724            # ref: https://huggingface.co/inceptionai/Jais-2-8B-Chat
1725            res = "jais-2"
1726        if chkhsh == "7b3e7548e4308f52a76e8229e4e6cc831195d0d1df43aed21ac6c93da05fec5f":
1727            # ref: https://huggingface.co/WisdomShell/CodeShell-7B
1728            res = "codeshell"
1729        if chkhsh == "63b97e4253352e6f357cc59ea5b583e3a680eaeaf2632188c2b952de2588485e":
1730            # ref: https://huggingface.co/mistralai/Mistral-Nemo-Base-2407
1731            res = "tekken"
1732        if chkhsh == "855059429035d75a914d1eda9f10a876752e281a054a7a3d421ef0533e5b6249":
1733            # ref: https://huggingface.co/HuggingFaceTB/SmolLM-135M
1734            res = "smollm"
1735        if chkhsh == "3c30d3ad1d6b64202cd222813e7736c2db6e1bd6d67197090fc1211fbc612ae7":
1736            # ref: https://huggingface.co/bigscience/bloom
1737            res = "bloom"
1738        if chkhsh == "bc01ce58980e1db43859146dc51b1758b3b88729b217a74792e9f8d43e479d21":
1739            # ref: https://huggingface.co/TurkuNLP/gpt3-finnish-small
1740            res = "gpt3-finnish"
1741        if chkhsh == "4e2b24cc4770243d65a2c9ec19770a72f08cffc161adbb73fcbb6b7dd45a0aae":
1742            # ref: https://huggingface.co/LGAI-EXAONE/EXAONE-3.0-7.8B-Instruct
1743            res = "exaone"
1744        if chkhsh == "fcace8b9cac38ce847670c970cd5892031a753a1ef381abd1d9af00f713da085":
1745            # ref: https://huggingface.co/microsoft/phi-2
1746            res = "phi-2"
1747        if chkhsh == "60824e3c0d9401f89943cbb2fff727f0e2d4c545ba4df2d6e4f09a6db0f5b450":
1748            # ref: https://huggingface.co/facebook/chameleon-7b
1749            res = "chameleon"
1750        if chkhsh == "8b5a93ed704057481f240da0be7e7dca721d7f8f4755263b6807227a2cbeae65":
1751            # ref: https://huggingface.co/sentence-transformers/stsb-roberta-base
1752            res = "roberta-bpe"
1753        if chkhsh == "ad851be1dba641f2e3711822f816db2c265f788b37c63b4e1aeacb9ee92de8eb":
1754            # ref: https://huggingface.co/ai-sage/GigaChat-20B-A3B-instruct
1755            res = "gigachat"
1756        if chkhsh == "d4c8f286ea6b520b3d495c4455483cfa2302c0cfcd4be05d781b6a8a0a7cdaf1":
1757            # ref: https://huggingface.co/Infinigence/Megrez-3B-Instruct
1758            res = "megrez"
1759        if chkhsh == "877081d19cf6996e2c4ff0e1236341e9b7bde288f5311a56a937f0afbbb3aeb5":
1760            # ref: https://huggingface.co/deepseek-ai/DeepSeek-V3
1761            res = "deepseek-v3"
1762        if chkhsh == "b3f499bb4255f8ca19fccd664443283318f2fd2414d5e0b040fbdd0cc195d6c5":
1763            # ref: https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B
1764            res = "deepseek-r1-qwen"
1765        if chkhsh == "ccc2ef013c104be7bae2965776d611e1d7a8a2a9c547dd93a682c9a9fc80352e":
1766            # ref: https://huggingface.co/Xenova/gpt-4o
1767            res = "gpt-4o"
1768        if chkhsh == "7dec86086fcc38b66b7bc1575a160ae21cf705be7718b9d5598190d7c12db76f":
1769            # ref: https://huggingface.co/UW/OLMo2-8B-SuperBPE-t180k
1770            res = "superbpe"
1771        if chkhsh == "1994ffd01900cfb37395608534236ecd63f2bd5995d6cb1004dda1af50240f15":
1772            # ref: https://huggingface.co/trillionlabs/Trillion-7B-preview
1773            res = "trillion"
1774        if chkhsh == "96a5f08be6259352137b512d4157e333e21df7edd3fcd152990608735a65b224":
1775            # ref: https://huggingface.co/inclusionAI/Ling-lite
1776            res = "bailingmoe"
1777        if chkhsh == "d353350c764d8c3b39c763113960e4fb4919bea5fbf208a0e3b22e8469dc7406":
1778            # ref: https://huggingface.co/meta-llama/Llama-4-Scout-17B-16E-Instruct
1779            res = "llama4"
1780        if chkhsh == "0e9433cbbb161f89e264eb32e8e64bfe69e834973ffca5d41d3948a604a3e2a3":
1781            # ref: https://huggingface.co/mistral-community/pixtral-12b
1782            res = "pixtral"
1783        if chkhsh == "d5f1dd6f980fec569fb218a81a7658ac45fc56b38c5a0adeb1c232fbe04ef5ec":
1784            # ref: https://huggingface.co/ByteDance-Seed/Seed-Coder-8B-Base
1785            res = "seed-coder"
1786        if chkhsh == "b0a6b1c0bd5998ebd9df08611efde34a4ff03faed45ae09c43e6b31ebd4b94cf":
1787            # ref: https://huggingface.co/skt/A.X-4.0
1788            res = "a.x-4.0"
1789        if chkhsh == "f6791d196f87ce6b56a7d234be618e0d58f8cda3549416635b2bebcd22cd95c4":
1790            # ref: https://huggingface.co/K-intelligence/Midm-2.0-Base-Instruct
1791            res = "midm-2.0"
1792        if chkhsh == "169bf0296a13c4d9b7672313f749eb36501d931022de052aad6e36f2bf34dd51":
1793            # ref: https://huggingface.co/LiquidAI/LFM2.5-350M
1794            res = "lfm2"
1795        if chkhsh == "2085e1638f6c377a0aa4ead21b27bb4cb941bf800df86ed391011769c1758dfb":
1796            # ref: https://huggingface.co/LGAI-EXAONE/EXAONE-4.0-32B
1797            res = "exaone4"
1798        if chkhsh == "a1e163ecab2e718a4c829d1148b6e86824ec36163bb71941c3dca9cd5ac25756":
1799            # ref: https://huggingface.co/JetBrains/Mellum-4b-base
1800            res = "mellum"
1801        if chkhsh == "a0b64b4385f123663873756336c085744376d015ff328bb1d901598f63c44152":
1802            # ref: https://huggingface.co/answerdotai/ModernBERT-base
1803            res = "modern-bert"
1804        if chkhsh == "49fc0303c9e0d2c2c565c510f64b2d9b271276acdcdadff733249eda9f7d59df":
1805            # ref: https://huggingface.co/arcee-ai/Trinity-Tokenizer
1806            res = "afmoe"
1807        if chkhsh == "9b1be57e70d20d9501b2b3186e792d81181ae36ada3903c26f9fea418cf87206":
1808            # ref: https://huggingface.co/inclusionAI/Ling-mini-base-2.0
1809            res = "bailingmoe2"
1810        if chkhsh == "53e325976a6e142379c19b09afcae354f2f496f147afa8f9e189a33fe4e3024e":
1811            # ref: https://huggingface.co/ibm-granite/granite-docling-258M
1812            res = "granite-docling"
1813        if chkhsh == "f4f37b6c8eb9ea29b3eac6bb8c8487c5ab7885f8d8022e67edc1c68ce8403e95":
1814            # ref: https://huggingface.co/MiniMaxAI/MiniMax-M2
1815            res = "minimax-m2"
1816        if chkhsh == "4a2e2abae11ca2b86d570fc5b44be4d5eb5e72cc8f22dd136a94b37da83ab665":
1817            # ref: https://huggingface.co/KORMo-Team/KORMo-tokenizer
1818            res = "kormo"
1819        if chkhsh == "9d70134b369a70e5735009b6de918f7581b5211f7c074d1f89f753aea8248af1":
1820            # ref: https://huggingface.co/tencent/Youtu-LLM-2B
1821            res = "youtu"
1822        if chkhsh == "16389f0a1f51ee53e562ffd51c371dc508639ab0e4261502071836e50e223e91":
1823            # ref: https://huggingface.co/upstage/Solar-Open-100B
1824            res = "solar-open"
1825        if chkhsh == "6c81ce329e0802883b22eabab0d3fa48357337ef1ecb45443828bf1f6254833f":
1826            # ref: https://huggingface.co/LGAI-EXAONE/K-EXAONE-236B-A23B
1827            res = "exaone-moe"
1828        if chkhsh == "d30d75d9059f1aa2c19359de71047b3ae408c70875e8a3ccf8c5fba56c9d8af4":
1829            # ref: https://huggingface.co/Qwen/Qwen3.5-9B-Instruct
1830            res = "qwen35"
1831        if chkhsh == "b4b8ca1f9769494fbd956ebc4c249de6131fb277a4a3345a7a92c7dd7a55808d":
1832            # ref: https://huggingface.co/jdopensource/JoyAI-LLM-Flash
1833            res = "joyai-llm"
1834        if chkhsh == "e4d54df1ebc1f2b91acd986c5b51aa50837d5faf7c7398e73c1f9e9ee5d19869":
1835            # ref: https://huggingface.co/kakaocorp/kanana-2-30b-a3b-instruct-2601
1836            res = "kanana2"
1837        if chkhsh == "862f827721df956049dff5ca81a57f29e575280bc622e290d3bf4e35eca29015":
1838            # ref: https://huggingface.co/codefuse-ai/F2LLM-v2-4B
1839            res = "f2llmv2"
1840        if chkhsh == "62f6fb0a6fd5098caeabb19b07a5c1099cafc8b9c40eab6ea89ece4ec02fbc57":
1841            # ref: https://huggingface.co/sarvamai/sarvam-30b
1842            res = "sarvam-moe"
1843        if chkhsh == "f728162c1315c26e40249849799b4ba3fe584c32084b4795b03eb295e63cb5af":
1844            # ref: https://huggingface.co/lewtun/talkie-1930-13b-it-hf
1845            res = "talkie"
1846        if chkhsh == "36f3066e97b7f3994b379aaacde306c1444c6ae84e81a5ae3cd2b7ed3b8c42d4":
1847            # ref: https://huggingface.co/openbmb/MiniCPM5-1B
1848            res = "minicpm5"
1849        if chkhsh == "f241072145675bf8322086f115aebad05e9f869557a238bf2150a2a417d1bf60":
1850            # ref: https://huggingface.co/ibm-granite/granite-embedding-97m-multilingual-r2
1851            res = "granite-embed-multi-97m"
1852        if chkhsh == "789696f5946cc0fc59371f39f6097cafed196b3acded6140432f26bbb1ae1669":
1853            # ref: https://huggingface.co/ibm-granite/granite-embedding-311m-multilingual-r2
1854            res = "granite-embed-multi-311m"
1855        if chkhsh == "9dcf830ee9990cdbf78cc523a5f7bd9ad8f3f9890c2d3581d2785ad10f07049d":
1856            # ref: https://huggingface.co/JetBrains/Mellum2-12B-A2.5B-Base
1857            res = "mellum2"
1858        if chkhsh == "972da7b59cec44d1f0a490a86c96df53859e486e481563e5dddac155013d87ac":
1859            # ref: https://huggingface.co/poolside/Laguna-XS.2
1860            res = "laguna"
1861
1862        if res is None:
1863            logger.warning("\n")
1864            logger.warning("**************************************************************************************")
1865            logger.warning("** WARNING: The BPE pre-tokenizer was not recognized!")
1866            logger.warning("**          There are 2 possible reasons for this:")
1867            logger.warning("**          - the model has not been added to convert_hf_to_gguf_update.py yet")
1868            logger.warning("**          - the pre-tokenization config has changed upstream")
1869            logger.warning("**          Check your model files and convert_hf_to_gguf_update.py and update them accordingly.")
1870            logger.warning("** ref:     https://github.com/ggml-org/llama.cpp/pull/6920")
1871            logger.warning("**")
1872            logger.warning(f"** chkhsh:  {chkhsh}")
1873            logger.warning("**************************************************************************************")
1874            logger.warning("\n")
1875            raise NotImplementedError("BPE pre-tokenizer was not recognized - update get_vocab_base_pre()")
1876
1877        logger.debug(f"tokenizer.ggml.pre: {repr(res)}")
1878        logger.debug(f"chkhsh: {chkhsh}")
1879
1880        return res
1881        # Marker: End get_vocab_base_pre
1882
1883    def _set_vocab_none(self) -> None:
1884        self.gguf_writer.add_tokenizer_model("none")
1885
1886    def _set_vocab_gpt2(self) -> None:
1887        tokens, toktypes, tokpre = self.get_vocab_base()
1888        self.gguf_writer.add_tokenizer_model("gpt2")
1889        self.gguf_writer.add_tokenizer_pre(tokpre)
1890        self.gguf_writer.add_token_list(tokens)
1891        self.gguf_writer.add_token_types(toktypes)
1892
1893        special_vocab = gguf.SpecialVocab(self.dir_model, load_merges=True)
1894        special_vocab.add_to_gguf(self.gguf_writer)
1895
1896    def _set_vocab_whitespace(self) -> None:
1897        tokens, toktypes, _ = self.get_vocab_base()
1898        self.gguf_writer.add_tokenizer_model("whitespace")
1899        self.gguf_writer.add_tokenizer_pre("whitespace") # pinned, not hash-detected: chktxt hash collides with jina-v1-en
1900        self.gguf_writer.add_token_list(tokens)
1901        self.gguf_writer.add_token_types(toktypes)
1902
1903        special_vocab = gguf.SpecialVocab(self.dir_model, load_merges=True)
1904        special_vocab.add_to_gguf(self.gguf_writer)
1905
1906    def _set_vocab_hybriddna(self):
1907        from transformers import AutoTokenizer
1908        tokenizer = AutoTokenizer.from_pretrained(self.dir_model, trust_remote_code=True)
1909        vocab_size = self.hparams.get("vocab_size", len(tokenizer.vocab))  # ty: ignore[unresolved-attribute]
1910        assert max(tokenizer.vocab.values()) < vocab_size  # ty: ignore[unresolved-attribute]
1911
1912        reverse_vocab = {id_: encoded_tok for encoded_tok, id_ in tokenizer.vocab.items()}  # ty: ignore[unresolved-attribute]
1913        # k-mers can share text with a base-vocab BPE token (e.g. CCCCCC) and get
1914        # dropped by get_vocab(); a reserved marker suffix (U+E000) keeps each
1915        # k-mer's own id (llama.cpp strips it on detokenization)
1916        for kmer in tokenizer.kmers:  # ty: ignore[unresolved-attribute]
1917            reverse_vocab[tokenizer.dna_token_to_id[kmer]] = kmer + "\ue000"  # ty: ignore[unresolved-attribute]
1918        added_vocab = tokenizer.get_added_vocab()  # ty: ignore[unresolved-attribute]
1919        added_tokens_decoder = tokenizer.added_tokens_decoder  # ty: ignore[unresolved-attribute]
1920
1921        tokens: list[str] = []
1922        toktypes: list[int] = []
1923        for i in range(vocab_size):
1924            if i not in reverse_vocab:
1925                tokens.append(f"[PAD{i}]")
1926                toktypes.append(gguf.TokenType.UNUSED)
1927            else:
1928                token: str = reverse_vocab[i]
1929                if token in added_vocab:
1930                    if added_tokens_decoder[i].special or self.does_token_look_special(token):
1931                        toktypes.append(gguf.TokenType.CONTROL)
1932                    else:
1933                        toktypes.append(gguf.TokenType.USER_DEFINED)
1934                else:
1935                    toktypes.append(gguf.TokenType.NORMAL)
1936                tokens.append(token)
1937
1938        tokpre = self.get_vocab_base_pre(tokenizer)
1939        self.gguf_writer.add_tokenizer_model("hybriddna")
1940        self.gguf_writer.add_tokenizer_pre(tokpre)
1941        self.gguf_writer.add_token_list(tokens)
1942        self.gguf_writer.add_token_types(toktypes)
1943
1944        special_vocab = gguf.SpecialVocab(self.dir_model, load_merges=True)
1945        special_vocab.add_to_gguf(self.gguf_writer)
1946
1947    def _set_vocab_qwen(self):
1948        from .qwen import QwenModel
1949
1950        dir_model = self.dir_model
1951        hparams = self.hparams
1952        tokens: list[str] = []
1953        toktypes: list[int] = []
1954
1955        from transformers import AutoTokenizer
1956        tokenizer = AutoTokenizer.from_pretrained(dir_model, trust_remote_code=True)
1957        vocab_size = hparams["vocab_size"]
1958        assert max(tokenizer.get_vocab().values()) < vocab_size  # ty: ignore[unresolved-attribute]
1959
1960        tokpre = self.get_vocab_base_pre(tokenizer)
1961
1962        merges = []
1963        vocab = {}
1964        mergeable_ranks = tokenizer.mergeable_ranks  # ty: ignore[unresolved-attribute]
1965        for token, rank in mergeable_ranks.items():
1966            vocab[QwenModel.token_bytes_to_string(token)] = rank
1967            if len(token) == 1:
1968                continue
1969            merged = QwenModel.bpe(mergeable_ranks, token, max_rank=rank)
1970            assert len(merged) == 2
1971            merges.append(' '.join(map(QwenModel.token_bytes_to_string, merged)))
1972
1973        # for this kind of tokenizer, added_vocab is not a subset of vocab, so they need to be combined
1974        added_vocab = tokenizer.special_tokens  # ty: ignore[unresolved-attribute]
1975        reverse_vocab = {id_ : encoded_tok for encoded_tok, id_ in {**vocab, **added_vocab}.items()}
1976
1977        for i in range(vocab_size):
1978            if i not in reverse_vocab:
1979                tokens.append(f"[PAD{i}]")
1980                toktypes.append(gguf.TokenType.UNUSED)
1981            elif reverse_vocab[i] in added_vocab:
1982                tokens.append(reverse_vocab[i])
1983                toktypes.append(gguf.TokenType.CONTROL)
1984            else:
1985                tokens.append(reverse_vocab[i])
1986                toktypes.append(gguf.TokenType.NORMAL)
1987
1988        self.gguf_writer.add_tokenizer_model("gpt2")
1989        self.gguf_writer.add_tokenizer_pre(tokpre)
1990        self.gguf_writer.add_token_list(tokens)
1991        self.gguf_writer.add_token_types(toktypes)
1992
1993        special_vocab = gguf.SpecialVocab(dir_model, load_merges=False)
1994        special_vocab.merges = merges
1995        # only add special tokens when they were not already loaded from config.json
1996        if len(special_vocab.special_token_ids) == 0:
1997            special_vocab._set_special_token("bos", tokenizer.special_tokens["<|endoftext|>"])  # ty: ignore[unresolved-attribute]
1998            special_vocab._set_special_token("eos", tokenizer.special_tokens["<|endoftext|>"])  # ty: ignore[unresolved-attribute]
1999        # this one is usually not in config.json anyway
2000        special_vocab._set_special_token("unk", tokenizer.special_tokens["<|endoftext|>"])  # ty: ignore[unresolved-attribute]
2001        special_vocab.add_to_gguf(self.gguf_writer)
2002
2003    def _set_vocab_sentencepiece(self, add_to_gguf=True):
2004        tokens, scores, toktypes = self._create_vocab_sentencepiece()
2005
2006        self.gguf_writer.add_tokenizer_model("llama")
2007        self.gguf_writer.add_tokenizer_pre("default")
2008        self.gguf_writer.add_token_list(tokens)
2009        self.gguf_writer.add_token_scores(scores)
2010        self.gguf_writer.add_token_types(toktypes)
2011
2012        special_vocab = gguf.SpecialVocab(self.dir_model, n_vocab=len(tokens))
2013        special_vocab.add_to_gguf(self.gguf_writer)
2014
2015    def _create_vocab_sentencepiece(self):
2016        from sentencepiece import SentencePieceProcessor
2017
2018        tokenizer_path = self.dir_model / 'tokenizer.model'
2019
2020        if not tokenizer_path.is_file():
2021            raise FileNotFoundError(f"File not found: {tokenizer_path}")
2022
2023        tokenizer = SentencePieceProcessor()
2024        tokenizer.LoadFromFile(str(tokenizer_path))
2025
2026        vocab_size = self.find_hparam([
2027            "vocab_size_per_layer_input", # gemma3n
2028            "vocab_size",
2029        ], optional=True) or tokenizer.vocab_size()
2030
2031        tokens: list[bytes] = [f"[PAD{i}]".encode("utf-8") for i in range(vocab_size)]
2032        scores: list[float] = [-10000.0] * vocab_size
2033        toktypes: list[int] = [SentencePieceTokenTypes.UNUSED] * vocab_size
2034
2035        for token_id in range(tokenizer.vocab_size()):
2036            if token_id >= vocab_size:
2037                logger.warning(f'ignore tokens from {token_id}: id is out of range, max={vocab_size - 1}')
2038                break
2039
2040            piece = tokenizer.IdToPiece(token_id)
2041            text = piece.encode("utf-8")
2042            score = tokenizer.GetScore(token_id)
2043
2044            toktype = SentencePieceTokenTypes.NORMAL
2045            if tokenizer.IsUnknown(token_id):
2046                toktype = SentencePieceTokenTypes.UNKNOWN
2047            elif tokenizer.IsControl(token_id):
2048                toktype = SentencePieceTokenTypes.CONTROL
2049            elif tokenizer.IsUnused(token_id):
2050                toktype = SentencePieceTokenTypes.UNUSED
2051            elif tokenizer.IsByte(token_id):
2052                toktype = SentencePieceTokenTypes.BYTE
2053
2054            tokens[token_id] = text
2055            scores[token_id] = score
2056            toktypes[token_id] = toktype
2057
2058        added_tokens_file = self.dir_model / 'added_tokens.json'
2059        if added_tokens_file.is_file():
2060            with open(added_tokens_file, "r", encoding="utf-8") as f:
2061                added_tokens_json = json.load(f)
2062                for key in added_tokens_json:
2063                    token_id = added_tokens_json[key]
2064                    if token_id >= vocab_size:
2065                        logger.warning(f'ignore token {token_id}: id is out of range, max={vocab_size - 1}')
2066                        continue
2067
2068                    tokens[token_id] = key.encode("utf-8")
2069                    scores[token_id] = -1000.0
2070                    toktypes[token_id] = SentencePieceTokenTypes.USER_DEFINED
2071
2072        tokenizer_config_file = self.dir_model / 'tokenizer_config.json'
2073        if tokenizer_config_file.is_file():
2074            with open(tokenizer_config_file, "r", encoding="utf-8") as f:
2075                tokenizer_config_json = json.load(f)
2076                added_tokens_decoder = tokenizer_config_json.get("added_tokens_decoder", {})
2077                for token_id, token_data in added_tokens_decoder.items():
2078                    token_id = int(token_id)
2079                    token: str = token_data["content"]
2080                    if token_id >= vocab_size:
2081                        logger.warning(f'ignore token {token_id}: id is out of range, max={vocab_size - 1}')
2082                        continue
2083                    if toktypes[token_id] != SentencePieceTokenTypes.UNUSED:
2084                        if tokens[token_id] != token.encode("utf-8"):
2085                            logger.warning(f'replacing token {token_id}: {tokens[token_id].decode("utf-8")!r} -> {token!r}')
2086                    if token_data.get("special") or self.does_token_look_special(token):
2087                        toktypes[token_id] = SentencePieceTokenTypes.CONTROL
2088                    else:
2089                        token = token.replace(b"\xe2\x96\x81".decode("utf-8"), " ")  # pre-normalize user-defined spaces
2090                        toktypes[token_id] = SentencePieceTokenTypes.USER_DEFINED
2091
2092                    scores[token_id] = -1000.0
2093                    tokens[token_id] = token.encode("utf-8")
2094
2095        if vocab_size > len(tokens):
2096            pad_count = vocab_size - len(tokens)
2097            logger.debug(f"Padding vocab with {pad_count} token(s) - [PAD1] through [PAD{pad_count}]")
2098            for i in range(1, pad_count + 1):
2099                tokens.append(bytes(f"[PAD{i}]", encoding="utf-8"))
2100                scores.append(-1000.0)
2101                toktypes.append(SentencePieceTokenTypes.UNUSED)
2102
2103        return tokens, scores, toktypes
2104
2105    def _set_vocab_llama_hf(self):
2106        vocab = gguf.LlamaHfVocab(self.dir_model)
2107        tokens = []
2108        scores = []
2109        toktypes = []
2110
2111        for text, score, toktype in vocab.all_tokens():
2112            tokens.append(text)
2113            scores.append(score)
2114            toktypes.append(toktype)
2115
2116        assert len(tokens) == vocab.vocab_size
2117
2118        self.gguf_writer.add_tokenizer_model("llama")
2119        self.gguf_writer.add_tokenizer_pre("default")
2120        self.gguf_writer.add_token_list(tokens)
2121        self.gguf_writer.add_token_scores(scores)
2122        self.gguf_writer.add_token_types(toktypes)
2123
2124        special_vocab = gguf.SpecialVocab(self.dir_model, n_vocab=len(tokens))
2125        special_vocab.add_to_gguf(self.gguf_writer)
2126
2127    def _set_vocab_rwkv_world(self):
2128        assert (self.dir_model / "rwkv_vocab_v20230424.txt").is_file()
2129        vocab_size = self.hparams.get("vocab_size", 65536)
2130
2131        tokens: list[bytes] = ['<s>'.encode("utf-8")]
2132        toktypes: list[int] = [gguf.TokenType.CONTROL]
2133
2134        with open(self.dir_model / "rwkv_vocab_v20230424.txt", "r", encoding="utf-8") as f:
2135            lines = f.readlines()
2136            for line in lines:
2137                parts = line.split(' ')
2138                assert len(parts) >= 3
2139                token, token_len = ast.literal_eval(' '.join(parts[1:-1])), int(parts[-1])
2140                token = token.encode("utf-8") if isinstance(token, str) else token
2141                assert isinstance(token, bytes)
2142                assert len(token) == token_len
2143                token_text: str = repr(token)[2:-1]  # "b'\xff'" -> "\xff"
2144                tokens.append(token_text.encode("utf-8"))
2145                toktypes.append(gguf.TokenType.NORMAL)
2146        remainder = vocab_size - len(tokens)
2147        assert remainder >= 0
2148        for i in range(len(tokens), vocab_size):
2149            tokens.append(f"[PAD{i}]".encode("utf-8"))
2150            toktypes.append(gguf.TokenType.UNUSED)
2151
2152        self.gguf_writer.add_tokenizer_model("rwkv")
2153        self.gguf_writer.add_token_list(tokens)
2154        self.gguf_writer.add_token_types(toktypes)
2155        special_vocab = gguf.SpecialVocab(self.dir_model, load_merges=False)
2156        if special_vocab.chat_template is None:
2157            template_path = Path(__file__).parent.parent / "models" / "templates" / "llama-cpp-rwkv-world.jinja"
2158            if template_path.is_file():
2159                with open(template_path, "r", encoding="utf-8") as f:
2160                    template = f.read()
2161            else:
2162                template = "rwkv-world"
2163            special_vocab.chat_template = template
2164        # hack: Add '\n\n' as the EOT token to make it chat normally
2165        special_vocab._set_special_token("eot", 261)
2166        # hack: Override these as they have already been set (incorrectly)
2167        special_vocab.special_token_ids["bos"] = 0
2168        special_vocab.special_token_ids["eos"] = 0
2169
2170        special_vocab.add_to_gguf(self.gguf_writer)
2171
2172    def _set_vocab_builtin(self, model_name: Literal["gpt-neox", "llama-spm"], vocab_size: int):
2173        tokenizer_path = Path(sys.path[0]) / "models" / f"ggml-vocab-{model_name}.gguf"
2174        logger.warning(f"Using tokenizer from '{os.path.relpath(tokenizer_path, os.getcwd())}'")
2175        vocab_reader = gguf.GGUFReader(tokenizer_path, "r")
2176
2177        default_pre = "mpt" if model_name == "gpt-neox" else "default"
2178
2179        field = vocab_reader.get_field(gguf.Keys.Tokenizer.MODEL)
2180        assert field  # tokenizer model
2181        self.gguf_writer.add_tokenizer_model(bytes(field.parts[-1]).decode("utf-8"))
2182
2183        field = vocab_reader.get_field(gguf.Keys.Tokenizer.PRE)
2184        self.gguf_writer.add_tokenizer_pre(bytes(field.parts[-1]).decode("utf-8") if field else default_pre)
2185
2186        field = vocab_reader.get_field(gguf.Keys.Tokenizer.LIST)
2187        assert field  # token list
2188        self.gguf_writer.add_token_list([bytes(field.parts[i]) for i in field.data][:vocab_size])
2189
2190        if model_name == "llama-spm":
2191            field = vocab_reader.get_field(gguf.Keys.Tokenizer.SCORES)
2192            assert field  # token scores
2193            self.gguf_writer.add_token_scores([field.parts[i].tolist()[0] for i in field.data][:vocab_size])
2194
2195        field = vocab_reader.get_field(gguf.Keys.Tokenizer.TOKEN_TYPE)
2196        assert field  # token types
2197        self.gguf_writer.add_token_types([field.parts[i].tolist()[0] for i in field.data][:vocab_size])
2198
2199        if model_name != "llama-spm":
2200            field = vocab_reader.get_field(gguf.Keys.Tokenizer.MERGES)
2201            assert field  # token merges
2202            self.gguf_writer.add_token_merges([bytes(field.parts[i]) for i in field.data])
2203
2204        if (field := vocab_reader.get_field(gguf.Keys.Tokenizer.BOS_ID)) is not None:
2205            self.gguf_writer.add_bos_token_id(field.parts[-1].tolist()[0])
2206        if (field := vocab_reader.get_field(gguf.Keys.Tokenizer.EOS_ID)) is not None:
2207            self.gguf_writer.add_eos_token_id(field.parts[-1].tolist()[0])
2208        if (field := vocab_reader.get_field(gguf.Keys.Tokenizer.UNK_ID)) is not None:
2209            self.gguf_writer.add_unk_token_id(field.parts[-1].tolist()[0])
2210        if (field := vocab_reader.get_field(gguf.Keys.Tokenizer.PAD_ID)) is not None:
2211            self.gguf_writer.add_pad_token_id(field.parts[-1].tolist()[0])
2212        if (field := vocab_reader.get_field(gguf.Keys.Tokenizer.ADD_BOS)) is not None:
2213            self.gguf_writer.add_add_bos_token(field.parts[-1].tolist()[0])
2214        if (field := vocab_reader.get_field(gguf.Keys.Tokenizer.ADD_EOS)) is not None:
2215            self.gguf_writer.add_add_eos_token(field.parts[-1].tolist()[0])
2216
2217    def _try_set_pooling_type(self) -> None:
2218        # get pooling path
2219        pooling_path = None
2220        module_path = self.dir_model / "modules.json"
2221        if module_path.is_file():
2222            with open(module_path, encoding="utf-8") as f:
2223                modules = json.load(f)
2224            for mod in modules:
2225                if mod["type"].endswith("Pooling"):
2226                    pooling_path = mod["path"]
2227                    break
2228
2229        mode_mapping = {
2230            "mean": gguf.PoolingType.MEAN,
2231            "cls": gguf.PoolingType.CLS,
2232            "lasttoken": gguf.PoolingType.LAST,
2233        }
2234
2235        # get pooling type
2236        if pooling_path is not None:
2237            with open(self.dir_model / pooling_path / "config.json", encoding="utf-8") as f:
2238                pooling = json.load(f)
2239            if pooling.get("pooling_mode_mean_tokens"):
2240                pooling_type = gguf.PoolingType.MEAN
2241            elif pooling.get("pooling_mode_cls_token"):
2242                pooling_type = gguf.PoolingType.CLS
2243            elif pooling.get("pooling_mode_lasttoken"):
2244                pooling_type = gguf.PoolingType.LAST
2245            elif (pooling_mode := pooling.get("pooling_mode")) in mode_mapping:
2246                pooling_type = mode_mapping[pooling_mode]
2247            else:
2248                raise NotImplementedError("Only MEAN, CLS, and LAST pooling types supported")
2249            self.gguf_writer.add_pooling_type(pooling_type)
2250
2251    def _set_vocab_glmedge(self):
2252        from transformers import AutoTokenizer
2253        tokenizer = AutoTokenizer.from_pretrained(self.dir_model)
2254        special_vocab = gguf.SpecialVocab(self.dir_model, load_merges=True)
2255        tokens, toktypes, tokpre = self.get_vocab_base()
2256        self.gguf_writer.add_tokenizer_model("gpt2")
2257        self.gguf_writer.add_tokenizer_pre(tokpre)
2258        self.gguf_writer.add_token_list(tokens)
2259        self.gguf_writer.add_token_types(toktypes)
2260        special_vocab._set_special_token("eos", tokenizer.get_added_vocab()["<|endoftext|>"])  # ty: ignore[unresolved-attribute]
2261        special_vocab._set_special_token("eot", tokenizer.get_added_vocab()["<|user|>"])  # ty: ignore[unresolved-attribute]
2262        special_vocab._set_special_token("unk", tokenizer.get_added_vocab()["<|endoftext|>"])  # ty: ignore[unresolved-attribute]
2263        special_vocab._set_special_token("bos", tokenizer.get_added_vocab()["<|endoftext|>"])  # ty: ignore[unresolved-attribute]
2264        special_vocab.add_to_gguf(self.gguf_writer)
2265
2266    def _set_vocab_glm(self):
2267        from transformers import AutoTokenizer
2268        tokenizer = AutoTokenizer.from_pretrained(self.dir_model)
2269        special_vocab = gguf.SpecialVocab(self.dir_model, load_merges=True)
2270        tokens, toktypes, tokpre = self.get_vocab_base()
2271        self.gguf_writer.add_tokenizer_model("gpt2")
2272        self.gguf_writer.add_tokenizer_pre(tokpre)
2273        self.gguf_writer.add_token_list(tokens)
2274        self.gguf_writer.add_token_types(toktypes)
2275        # Special tokens
2276        # Note: Using <|endoftext|> (151329) for eot causes endless generation
2277        special_vocab._set_special_token("bos", tokenizer.get_added_vocab()["[gMASK]"])  # ty: ignore[unresolved-attribute]  # 151331
2278        special_vocab._set_special_token("eot", tokenizer.get_added_vocab()["<|user|>"])  # ty: ignore[unresolved-attribute]  # 151336
2279        special_vocab._set_special_token("unk", tokenizer.get_added_vocab()["<|endoftext|>"])  # ty: ignore[unresolved-attribute]  # 151329
2280        special_vocab._set_special_token("eom", tokenizer.get_added_vocab()["<|observation|>"])  # ty: ignore[unresolved-attribute]  # 151338
2281        special_vocab.add_to_gguf(self.gguf_writer)
2282
2283    def _set_vocab_interns1(self):
2284        tokens: list[str] = []
2285        toktypes: list[int] = []
2286
2287        from transformers import AutoTokenizer
2288        tokenizer = AutoTokenizer.from_pretrained(self.dir_model, trust_remote_code=True)
2289        vocab = getattr(tokenizer, 'vocab', tokenizer.get_vocab())  # ty: ignore[unresolved-attribute]
2290        vocab_size = self.hparams.get("vocab_size", len(vocab))
2291        assert max(vocab.values()) < vocab_size
2292
2293        tokpre = self.get_vocab_base_pre(tokenizer)
2294
2295        reverse_vocab = {id_: encoded_tok for encoded_tok, id_ in vocab.items()}
2296        added_vocab = tokenizer.get_added_vocab()  # ty: ignore[unresolved-attribute]
2297
2298        added_tokens_decoder = tokenizer.added_tokens_decoder  # ty: ignore[unresolved-attribute]
2299
2300        for i in range(vocab_size):
2301            if i not in reverse_vocab:
2302                tokens.append(f"[PAD{i}]")
2303                toktypes.append(gguf.TokenType.UNUSED)
2304            else:
2305                token: str = reverse_vocab[i]
2306                if token in added_vocab:
2307                    # The tokenizer in llama.cpp assumes the CONTROL and USER_DEFINED tokens are pre-normalized.
2308                    # To avoid unexpected issues - we make sure to normalize non-normalized tokens
2309                    if not added_tokens_decoder[i].normalized:
2310                        previous_token = token
2311                        token = tokenizer.decode(tokenizer.encode(token, add_special_tokens=False))  # ty: ignore[unresolved-attribute, invalid-assignment]
2312                        if previous_token != token:
2313                            logger.info(f"{repr(previous_token)} is encoded and decoded back to {repr(token)} using AutoTokenizer")
2314
2315                    if added_tokens_decoder[i].special or self.does_token_look_special(token):
2316                        toktypes.append(gguf.TokenType.CONTROL)
2317                    else:
2318                        toktypes.append(gguf.TokenType.USER_DEFINED)
2319                else:
2320                    toktypes.append(gguf.TokenType.NORMAL)
2321                tokens.append(token)
2322
2323        self.gguf_writer.add_tokenizer_model("gpt2")
2324        self.gguf_writer.add_tokenizer_pre(tokpre)
2325        self.gguf_writer.add_token_list(tokens)
2326        self.gguf_writer.add_token_types(toktypes)
2327
2328        special_vocab = gguf.SpecialVocab(self.dir_model, load_merges=True)
2329        special_vocab._set_special_token("bos", 151643)
2330        special_vocab.add_to_gguf(self.gguf_writer)
2331
2332    def _set_vocab_mistral(self):
2333        from .mistral import MistralModel
2334
2335        if not _mistral_common_installed:
2336            raise ImportError(_mistral_import_error_msg)
2337
2338        vocab = MistralVocab(self.dir_model)
2339        logger.info(
2340            f"Converting tokenizer {vocab.tokenizer_type} of size {vocab.vocab_size}."
2341        )
2342
2343        self.gguf_writer.add_tokenizer_model(vocab.gguf_tokenizer_model)
2344
2345        tokens = []
2346        scores = []
2347        toktypes = []
2348
2349        for text, score, toktype in vocab.all_tokens():
2350            tokens.append(text)
2351            scores.append(score)
2352            toktypes.append(toktype)
2353
2354        assert len(tokens) == vocab.vocab_size, (
2355            f"token count ({len(tokens)}) != vocab size ({vocab.vocab_size})"
2356        )
2357
2358        if vocab.tokenizer_type == MistralTokenizerType.tekken:
2359            self.gguf_writer.add_tokenizer_pre("tekken")
2360            self.gguf_writer.add_token_merges(
2361                vocab.extract_vocab_merges_from_model()
2362            )
2363
2364        logger.info(
2365            f"Setting bos, eos, unk and pad token IDs to {vocab.bos_id}, {vocab.eos_id}, {vocab.unk_id}, {vocab.pad_id}."
2366        )
2367
2368        self.gguf_writer.add_bos_token_id(vocab.bos_id)
2369        self.gguf_writer.add_eos_token_id(vocab.eos_id)
2370        self.gguf_writer.add_unk_token_id(vocab.unk_id)
2371        self.gguf_writer.add_pad_token_id(vocab.pad_id)
2372
2373        self.gguf_writer.add_token_list(tokens)
2374        self.gguf_writer.add_token_scores(scores)
2375        self.gguf_writer.add_token_types(toktypes)
2376        self.gguf_writer.add_vocab_size(vocab.vocab_size)
2377
2378        self.gguf_writer.add_add_bos_token(True)
2379        self.gguf_writer.add_add_eos_token(False)
2380
2381        local_template_file_path = self.dir_model / "chat_template.jinja"
2382
2383        if self.is_mistral_format and local_template_file_path.is_file():
2384            # Ministral-3 and other new Mistral models come with chat templates.
2385            # ref: https://huggingface.co/mistralai/Ministral-3-14B-Instruct-2512/tree/main
2386            logger.info("Using an existing Mistral local chat template.")
2387
2388            with open(local_template_file_path, "r", encoding="utf-8") as f:
2389                template = f.read()
2390        elif not self.is_mistral_format or not self.disable_mistral_community_chat_template:
2391            template_dir = Path(__file__).parent.parent / "models/templates/"
2392
2393            # Log only for Mistral format that the official tokenization and detokenization is via `mistral-common`.
2394            if self.is_mistral_format:
2395                logger.info(
2396                    "Using a Mistral community chat template. These templates can be subject to errors in early days or weeks after a release. "
2397                    "Mistral recommends to use `mistral-common` to perform tokenization and detokenization."
2398                )
2399            template = MistralModel.get_community_chat_template(vocab, template_dir, self.is_mistral_format)
2400        else:
2401            logger.info("Not using a Mistral local or community chat template. Ensure to perform the tokenization and detokenization via `mistral-common`.")
2402            template = None
2403
2404        if template is not None:
2405            self.gguf_writer.add_chat_template(template)
2406
2407    def _set_vocab_plamo(self):
2408        # PLaMo models use a custom tokenizer with a .jsonl file
2409        tokenizer_jsonl_path = self.dir_model / "tokenizer.jsonl"
2410        tokenizer_config_path = self.dir_model / "tokenizer_config.json"
2411
2412        if not tokenizer_jsonl_path.is_file():
2413            raise FileNotFoundError(f"PLaMo tokenizer file not found: {tokenizer_jsonl_path}")
2414
2415        # Load tokenizer config
2416        with open(tokenizer_config_path, "r", encoding="utf-8") as f:
2417            tokenizer_config = json.load(f)
2418
2419        # Load tokens from JSONL file (actually a list format)
2420        tokens = []
2421        scores = []
2422        toktypes = []
2423
2424        with open(tokenizer_jsonl_path, "r", encoding="utf-8") as f:
2425            for line_num, line in enumerate(f):
2426                if line.strip():
2427                    token_data = json.loads(line)
2428                    # Format: [token, score, type, ?, ?, ?, ?]
2429                    token = token_data[0].encode("utf-8")
2430                    score = float(token_data[1])
2431                    token_type_str = token_data[2] if len(token_data) > 2 else "NORMAL"
2432
2433                    tokens.append(token)
2434                    scores.append(score)
2435
2436                    if token_type_str == "UNKNOWN":
2437                        toktypes.append(gguf.TokenType.UNKNOWN)
2438                    elif token_type_str == "CONTROL":
2439                        toktypes.append(gguf.TokenType.CONTROL)
2440                    elif token_type_str == "BYTE":
2441                        toktypes.append(gguf.TokenType.BYTE)
2442                    else:
2443                        token_str = token_data[0]
2444                        if token_str.startswith("<|plamo:") and token_str.endswith("|>"):
2445                            toktypes.append(gguf.TokenType.CONTROL)
2446                        else:
2447                            toktypes.append(gguf.TokenType.NORMAL)
2448
2449        vocab_size = self.hparams["vocab_size"]
2450        if vocab_size > len(tokens):
2451            pad_count = vocab_size - len(tokens)
2452            logger.debug(f"Padding vocab with {pad_count} token(s) - [PAD1] through [PAD{pad_count}]")
2453            for i in range(1, pad_count + 1):
2454                tokens.append(bytes(f"[PAD{i}]", encoding="utf-8"))
2455                scores.append(-1000.0)
2456                toktypes.append(gguf.TokenType.UNUSED)
2457
2458        self.gguf_writer.add_tokenizer_model("plamo2")
2459        self.gguf_writer.add_tokenizer_pre("default")
2460        self.gguf_writer.add_token_list(tokens)
2461        self.gguf_writer.add_token_scores(scores)
2462        self.gguf_writer.add_token_types(toktypes)
2463
2464        if "bos_token" in tokenizer_config and tokenizer_config["bos_token"] is not None:
2465            token_id = tokens.index(tokenizer_config["bos_token"].encode("utf-8"))
2466            self.gguf_writer.add_bos_token_id(token_id)
2467        if "eos_token" in tokenizer_config and tokenizer_config["eos_token"] is not None:
2468            token_id = tokens.index(tokenizer_config["eos_token"].encode("utf-8"))
2469            self.gguf_writer.add_eos_token_id(token_id)
2470        if "pad_token" in tokenizer_config and tokenizer_config["pad_token"] is not None:
2471            token_id = tokens.index(tokenizer_config["pad_token"].encode("utf-8"))
2472            self.gguf_writer.add_pad_token_id(token_id)
2473        if "sep_token" in tokenizer_config and tokenizer_config["sep_token"] is not None:
2474            token_id = tokens.index(tokenizer_config["sep_token"].encode("utf-8"))
2475            self.gguf_writer.add_sep_token_id(token_id)
2476        if "unk_token" in tokenizer_config and tokenizer_config["unk_token"] is not None:
2477            token_id = tokens.index(tokenizer_config["unk_token"].encode("utf-8"))
2478            self.gguf_writer.add_unk_token_id(token_id)
2479
2480        # Add <|plamo:op|> as EOT to ensure appropriate end of generation
2481        self.gguf_writer.add_eot_token_id(4)
2482
2483        self.gguf_writer.add_add_space_prefix(False)
2484
2485
2486class MmprojModel(ModelBase):
2487    model_type = ModelType.MMPROJ
2488    model_arch = gguf.MODEL_ARCH.MMPROJ
2489    preprocessor_config: dict[str, Any]
2490    global_config: dict[str, Any]
2491
2492    n_block_keys = ["n_layers", "num_hidden_layers", "n_layer", "num_layers", "depth", "layers", "encoder_layers", "vt_num_hidden_layers"]
2493
2494    has_vision_encoder: bool = True # by default
2495    has_audio_encoder: bool = False
2496
2497    # for models having multiple encoders, we need to separate their hparams
2498    hparams_vision: dict[str, Any] | None = None
2499    hparams_audio: dict[str, Any] | None = None
2500
2501    def __init__(self, *args, **kwargs):
2502        super().__init__(*args, **kwargs)
2503
2504        if self.model_arch != gguf.MODEL_ARCH.MMPROJ:
2505            raise TypeError("MmprojModel must be subclassed with model_arch = gguf.MODEL_ARCH.MMPROJ")
2506
2507        # get n_embd of the text model
2508        if not self.is_mistral_format:
2509            if "text_config" not in self.hparams:
2510                self.hparams["text_config"] = {}
2511            if "audio_config" not in self.hparams:
2512                self.hparams["audio_config"] = {}
2513            text_config = {**self.hparams, **self.hparams["text_config"]}
2514            self.n_embd_text = text_config.get("hidden_size", text_config.get("n_embd", 0))
2515        else:
2516            text_config = {
2517                k: v for k, v in self.hparams.items() if k not in ["vision_encoder", "audio_encoder"]
2518            }
2519            # mistral native params.json: "dim" is the text hidden size ("hidden_dim" is the FFN intermediate size)
2520            self.n_embd_text = text_config.get("dim", 0)
2521
2522        assert self.n_embd_text > 0, "n_embd not found in hparams"
2523
2524        # move vision config to the top level, while preserving the original hparams in global_config
2525        import copy
2526        self.global_config = copy.deepcopy(self.hparams)
2527        self.hparams_vision = self.get_vision_config()
2528        self.hparams_audio = self.get_audio_config()
2529
2530        if self.hparams_vision is None and self.hparams_audio is None:
2531            raise ValueError("vision_config / audio_config not found in hparams")
2532
2533        # for compat with vision-only models
2534        self.hparams = self.hparams_vision or self.hparams_audio or self.hparams
2535
2536        # TODO @ngxson : this is a hack to support both vision and audio encoders
2537        have_multiple_encoders = self.has_audio_encoder and self.has_vision_encoder
2538        self.block_count = 128 if have_multiple_encoders else self.find_hparam(self.n_block_keys, True)
2539        self.tensor_map = gguf.get_tensor_name_map(gguf.MODEL_ARCH.MMPROJ, self.block_count)
2540
2541        # load preprocessor config
2542        self.preprocessor_config = {}
2543
2544        # prefer preprocessor_config.json if possible
2545        preprocessor_config_path = self.dir_model / "preprocessor_config.json"
2546        if preprocessor_config_path.is_file():
2547            with open(preprocessor_config_path, "r", encoding="utf-8") as f:
2548                cfg = json.load(f)
2549                # move media_proc_cfg to root level for compat
2550                if "media_proc_cfg" in cfg:
2551                    cfg = {
2552                        **cfg,
2553                        **cfg["media_proc_cfg"],
2554                    }
2555                # merge configs
2556                self.preprocessor_config = {**self.preprocessor_config, **cfg}
2557
2558        # prefer processor_config.json if possible
2559        processor_config_path = self.dir_model / "processor_config.json"
2560        if processor_config_path.is_file():
2561            with open(processor_config_path, "r", encoding="utf-8") as f:
2562                cfg = json.load(f)
2563                # move image_processor to root level for compat
2564                if "image_processor" in cfg:
2565                    cfg = {
2566                        **cfg,
2567                        **cfg["image_processor"],
2568                    }
2569                # merge configs
2570                self.preprocessor_config = {**self.preprocessor_config, **cfg}
2571
2572    @classmethod
2573    def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
2574        name, gen = item
2575
2576        # Skip non-multimodal tensors
2577        if "language_model." in name:
2578            return None
2579
2580        return super().filter_tensors(item)
2581
2582    def get_vision_config(self) -> dict[str, Any] | None:
2583        config_name = "vision_config" if not self.is_mistral_format else "vision_encoder"
2584        return self.global_config.get(config_name)
2585
2586    def get_audio_config(self) -> dict[str, Any] | None:
2587        mm_config_key = "whisper_config" if "whisper_config" in self.hparams else "audio_config"
2588        return self.global_config.get(mm_config_key)
2589
2590    def set_type(self):
2591        self.gguf_writer.add_type(gguf.GGUFType.MMPROJ)
2592
2593    def prepare_metadata(self, vocab_only: bool):
2594        super().prepare_metadata(vocab_only=vocab_only)
2595
2596        output_type: str = self.ftype.name.partition("_")[2]
2597
2598        if self.fname_out.is_dir():
2599            fname_default: str = gguf.naming_convention(self.metadata.name, self.metadata.basename, self.metadata.finetune, self.metadata.version, size_label=None, output_type=output_type, model_type=None)
2600            self.fname_out = self.fname_out / f"mmproj-{fname_default}.gguf"
2601        else:
2602            self.fname_out = self.fname_out.parent / gguf.fill_templated_filename(self.fname_out.name, output_type)
2603
2604    def set_gguf_parameters(self):
2605        self.gguf_writer.add_file_type(self.ftype)
2606
2607        if self.has_vision_encoder:
2608            self.gguf_writer.add_clip_has_vision_encoder(True)
2609            self.gguf_writer.add_vision_projection_dim(self.n_embd_text)
2610
2611            # vision config
2612            self.image_size = self.find_vparam(["image_size"])
2613            self.gguf_writer.add_vision_image_size(self.image_size)
2614            self.gguf_writer.add_vision_patch_size(self.find_vparam(["patch_size"]))
2615            self.gguf_writer.add_vision_embedding_length(self.find_vparam(["hidden_size", "width", "vt_hidden_size"]))
2616            self.gguf_writer.add_vision_feed_forward_length(self.find_vparam(["intermediate_size", "vt_intermediate_size"]))
2617            self.gguf_writer.add_vision_block_count(self.find_vparam(self.n_block_keys))
2618            self.gguf_writer.add_vision_head_count(self.find_vparam(["num_attention_heads", "num_heads", "heads", "vt_num_attention_heads"]))
2619
2620            # preprocessor config
2621            image_mean = _MISTRAL_COMMON_DATASET_MEAN if self.is_mistral_format else self.preprocessor_config["image_mean"]
2622            image_std = _MISTRAL_COMMON_DATASET_STD if self.is_mistral_format else self.preprocessor_config["image_std"]
2623
2624            self.gguf_writer.add_vision_image_mean(image_mean)
2625            self.gguf_writer.add_vision_image_std(image_std)
2626
2627        if self.has_audio_encoder:
2628            self.gguf_writer.add_clip_has_audio_encoder(True)
2629            self.gguf_writer.add_audio_projection_dim(self.n_embd_text)
2630
2631            # audio config
2632            self.gguf_writer.add_audio_embedding_length(self.find_aparam(["hidden_size"]))
2633            self.gguf_writer.add_audio_feed_forward_length(self.find_aparam(["intermediate_size"]))
2634            self.gguf_writer.add_audio_block_count(self.find_aparam(self.n_block_keys))
2635            self.gguf_writer.add_audio_head_count(self.find_aparam(["num_attention_heads"]))
2636
2637        if not self.has_vision_encoder and not self.has_audio_encoder:
2638            raise ValueError("MmprojModel must have either vision or audio encoder")
2639
2640    def write_vocab(self):
2641        raise ValueError("MmprojModel does not support vocab writing")
2642
2643    def find_vparam(self, keys: Iterable[str], optional: bool = False) -> Any:
2644        assert self.hparams_vision is not None
2645        return self._find_param(self.hparams_vision, keys, optional)
2646
2647    def find_aparam(self, keys: Iterable[str], optional: bool = False) -> Any:
2648        assert self.hparams_audio is not None
2649        return self._find_param(self.hparams_audio, keys, optional)
2650
2651    def _find_param(self, obj: dict[str, Any], keys: Iterable[str], optional: bool = False) -> Any:
2652        key = next((k for k in keys if k in obj), None)
2653        if key is not None:
2654            return obj[key]
2655        if optional:
2656            return None
2657        raise KeyError(f"could not find any of: {keys}")
2658
2659    def tensor_force_quant(self, name, new_name, bid, n_dims):
2660        if ".patch_embd.weight" in new_name or ".patch_merger.weight" in new_name:
2661            return gguf.GGMLQuantizationType.F16 if self.ftype == gguf.LlamaFileType.MOSTLY_F16 else gguf.GGMLQuantizationType.F32
2662        return super().tensor_force_quant(name, new_name, bid, n_dims)
2663
2664
2665class LazyTorchTensor(gguf.LazyBase):
2666    _tensor_type = torch.Tensor
2667    # to keep the type-checker happy
2668    dtype: torch.dtype
2669    shape: torch.Size
2670
2671    # only used when converting a torch.Tensor to a np.ndarray
2672    _dtype_map: dict[torch.dtype, type] = {
2673        torch.float16: np.float16,
2674        torch.float32: np.float32,
2675        torch.uint8: np.uint8,
2676        torch.int64: np.int64,
2677    }
2678
2679    # only used when byteswapping data. Only correct size is needed
2680    # TODO: uncomment uint64, uint32, and uint16, ref: https://github.com/pytorch/pytorch/issues/58734
2681    _dtype_byteswap_map: dict[torch.dtype, type] = {
2682        torch.float64: np.float64,
2683        torch.float32: np.float32,
2684        torch.bfloat16: np.float16,
2685        torch.float16: np.float16,
2686        torch.int64: np.int64,
2687        # torch.uint64: np.uint64,
2688        torch.int32: np.int32,
2689        # torch.uint32: np.uint32,
2690        torch.int16: np.int16,
2691        # torch.uint16: np.uint16,
2692        torch.int8: np.int8,
2693        torch.uint8: np.uint8,
2694        torch.bool: np.uint8,
2695        torch.float8_e4m3fn: np.uint8,
2696        torch.float8_e5m2: np.uint8,
2697    }
2698
2699    # used for safetensors slices
2700    # ref: https://github.com/huggingface/safetensors/blob/079781fd0dc455ba0fe851e2b4507c33d0c0d407/bindings/python/src/lib.rs#L1046
2701    # TODO: uncomment U64, U32, and U16, ref: https://github.com/pytorch/pytorch/issues/58734
2702    _dtype_str_map: dict[str, torch.dtype] = {
2703        "F64": torch.float64,
2704        "F32": torch.float32,
2705        "BF16": torch.bfloat16,
2706        "F16": torch.float16,
2707        # "U64": torch.uint64,
2708        "I64": torch.int64,
2709        # "U32": torch.uint32,
2710        "I32": torch.int32,
2711        # "U16": torch.uint16,
2712        "I16": torch.int16,
2713        "U8": torch.uint8,
2714        "I8": torch.int8,
2715        "BOOL": torch.bool,
2716        "F8_E4M3": torch.float8_e4m3fn,
2717        "F8_E5M2": torch.float8_e5m2,
2718    }
2719
2720    def numpy(self) -> gguf.LazyNumpyTensor:
2721        dtype = self._dtype_map[self.dtype]
2722        return gguf.LazyNumpyTensor(
2723            meta=gguf.LazyNumpyTensor.meta_with_dtype_and_shape(dtype, self.shape),
2724            args=(self,),
2725            func=(lambda s: s.numpy())
2726        )
2727
2728    @classmethod
2729    def meta_with_dtype_and_shape(cls, dtype: torch.dtype, shape: tuple[int, ...]) -> Tensor:
2730        return torch.empty(size=shape, dtype=dtype, device="meta")
2731
2732    @classmethod
2733    def from_safetensors_slice(cls, st_slice: Any) -> Tensor:
2734        dtype = cls._dtype_str_map[st_slice.get_dtype()]
2735        shape: tuple[int, ...] = tuple(st_slice.get_shape())
2736        lazy = cls(meta=cls.meta_with_dtype_and_shape(dtype, shape), args=(st_slice,), func=lambda s: s[...] if len(s.get_shape()) == 0 else s[:])
2737        return cast(torch.Tensor, lazy)
2738
2739    @classmethod
2740    def from_local_tensor(cls, t: gguf.utility.LocalTensor) -> Tensor:
2741        def load_tensor(tensor: gguf.utility.LocalTensor) -> Tensor:
2742            def byteswap_tensor(tensor: np.ndarray, dtype: type) -> np.ndarray:
2743                if sys.byteorder == 'big':
2744                    # switch data back to big endian
2745                    tensor = tensor.view(dtype).byteswap(inplace=False)
2746                return tensor
2747            dtype = cls._dtype_str_map[tensor.dtype]
2748            numpy_dtype = cls._dtype_byteswap_map[dtype]
2749            return torch.from_numpy(byteswap_tensor(tensor.mmap_bytes(), numpy_dtype)).view(dtype).reshape(tensor.shape)
2750        dtype = cls._dtype_str_map[t.dtype]
2751        shape = t.shape
2752        lazy = cls(meta=cls.meta_with_dtype_and_shape(dtype, shape), args=(t,), func=lambda r: load_tensor(r))
2753        return cast(torch.Tensor, lazy)
2754
2755    @classmethod
2756    def from_remote_tensor(cls, remote_tensor: gguf.utility.RemoteTensor):
2757        def byteswap_tensor(tensor: np.ndarray, dtype: type) -> np.ndarray:
2758            if sys.byteorder == 'big':
2759                # switch data back to big endian
2760                tensor = tensor.view(dtype).byteswap(inplace=False)
2761            return tensor
2762        dtype = cls._dtype_str_map[remote_tensor.dtype]
2763        numpy_dtype = cls._dtype_byteswap_map[dtype]
2764        shape = remote_tensor.shape
2765        meta = cls.meta_with_dtype_and_shape(dtype, shape)
2766        lazy = cls(meta=meta, args=(remote_tensor,), func=lambda r: torch.from_numpy(byteswap_tensor(np.frombuffer(r.data(), dtype=numpy_dtype), numpy_dtype)).view(dtype).reshape(shape))
2767        return cast(torch.Tensor, lazy)
2768
2769    @classmethod
2770    def __torch_function__(cls, func, types, args=(), kwargs=None):
2771        del types  # unused
2772
2773        if kwargs is None:
2774            kwargs = {}
2775
2776        if func is torch.Tensor.numpy:
2777            assert len(args)
2778            return args[0].numpy()
2779
2780        return cls._wrap_fn(func)(*args, **kwargs)
2781
2782
2783if hasattr(torch, "float8_e8m0fnu"):
2784    _torch_float8_e8m0 = torch.float8_e8m0fnu
2785    LazyTorchTensor._dtype_map[_torch_float8_e8m0] = np.uint8
2786    LazyTorchTensor._dtype_byteswap_map[_torch_float8_e8m0] = np.uint8
2787    LazyTorchTensor._dtype_str_map["F8_E8M0"] = _torch_float8_e8m0
2788else:
2789    # Older torch builds do not expose F8_E8M0. Keep the raw bytes so callers
2790    # that know the format can decode them explicitly.
2791    LazyTorchTensor._dtype_str_map["F8_E8M0"] = torch.uint8
2792
2793
2794def get_model_architecture(hparams: dict[str, Any], model_type: ModelType) -> str:
2795    # TODO @ngxson : this won't work correctly if the model has both audio & vision encoders
2796    # maybe we should fallback to text model's arch in that case, since not many models have both
2797    text_config = hparams.get("text_config", {})
2798    vision_config = hparams.get("vision_config", {})
2799    arch = None
2800    if (arches := hparams.get("architectures")) is not None and len(arches) > 0:
2801        arch = arches[0]
2802    elif "ssm_cfg" in hparams:
2803        # For non-hf Mamba and Mamba2 models
2804        arch = hparams["ssm_cfg"].get("layer", "Mamba") + "ForCausalLM"
2805
2806    # Step3-VL keeps text config under text_config but uses a custom top-level architecture.
2807    # For text conversion we route to a dedicated text-only class.
2808    # TODO: refactor this later to avoid adding exception here
2809    # Kimi-K3's text_config reports "KimiLinearForCausalLM", which is the older
2810    # Kimi-Linear-48B architecture and cannot load K3 (no attention residuals,
2811    # latent MoE, situ, ...). Route on the top-level architecture instead.
2812    if model_type == ModelType.TEXT and arch in ("StepVLForConditionalGeneration", "Sarashina2VisionForCausalLM", "Exaone4_5_ForConditionalGeneration", "Step3p7ForConditionalGeneration", "KimiK3ForConditionalGeneration"):
2813        return arch
2814
2815    # if "architectures" is found in the sub-config, use that instead
2816    if model_type == ModelType.TEXT and text_config.get("architectures") is not None:
2817        arch = text_config["architectures"][0]
2818    elif model_type == ModelType.MMPROJ and vision_config.get("architectures") is not None:
2819        arch = vision_config["architectures"][0]
2820    if arch is None:
2821        raise ValueError("Failed to detect model architecture")
2822    return arch