char-fork/llama.cpp

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

Xuan-Son Nguyenconvert: add @ModelBase.example (#27208)fa88ae936

char/aliceai-t5-moe
11.4 KiB224 linesraw
1from __future__ import annotations
2
3from typing import Iterable, TYPE_CHECKING
4
5import torch
6
7if TYPE_CHECKING:
8    from torch import Tensor
9
10from .base import ModelBase, TextModel, gguf, logger
11
12from .qwen import QwenModel
13
14
15@ModelBase.register("KimiLinearModel", "KimiLinearForCausalLM")
16@ModelBase.example("moonshotai/Kimi-Linear-48B-A3B-Instruct")
17class KimiLinearModel(TextModel):
18    """Kimi-Linear model with hybrid MLA+KDA architecture"""
19    model_arch = gguf.MODEL_ARCH.KIMI_LINEAR
20
21    _experts: list[dict[str, Tensor]] | None = None
22
23    def set_vocab(self):
24        try:
25            self._set_vocab_gpt2()
26            return
27        except Exception:
28            pass
29
30        from transformers import AutoTokenizer
31        tokenizer = AutoTokenizer.from_pretrained(self.dir_model, trust_remote_code=True)
32        tokpre = self.get_vocab_base_pre(tokenizer)
33
34        if tokpre == "kimi-k2":
35            # Build merges list using the approach similar to HunYuanMoE
36            merges = []
37            vocab = {}
38            mergeable_ranks = tokenizer.model._mergeable_ranks  # ty: ignore[unresolved-attribute]
39            for token, rank in mergeable_ranks.items():
40                vocab[QwenModel.token_bytes_to_string(token)] = rank
41                if len(token) == 1:
42                    continue
43                merged = QwenModel.bpe(mergeable_ranks, token, max_rank=rank)
44                if len(merged) == 2:
45                    merges.append(' '.join(map(QwenModel.token_bytes_to_string, merged)))
46            # Build token list
47            vocab_size = self.hparams["vocab_size"]
48            special_tokens = tokenizer.special_tokens  # ty: ignore[unresolved-attribute]
49            reverse_vocab = {id_ : encoded_tok for encoded_tok, id_ in {**vocab, **special_tokens}.items()}
50            tokens: list[str] = []
51            toktypes: list[int] = []
52
53            for i in range(vocab_size):
54                if i not in reverse_vocab:
55                    tokens.append(f"[PAD{i}]")
56                    toktypes.append(gguf.TokenType.UNUSED)
57                else:
58                    token = reverse_vocab[i]
59                    tokens.append(token)
60                    if i in special_tokens.values():
61                        toktypes.append(gguf.TokenType.CONTROL)
62                    else:
63                        toktypes.append(gguf.TokenType.NORMAL)
64
65            self.gguf_writer.add_tokenizer_model("gpt2")
66            self.gguf_writer.add_tokenizer_pre(tokpre)
67            self.gguf_writer.add_token_list(tokens)
68            self.gguf_writer.add_token_types(toktypes)
69            self.gguf_writer.add_token_merges(merges)
70
71            special_vocab = gguf.SpecialVocab(self.dir_model, load_merges=False)
72            special_vocab.add_to_gguf(self.gguf_writer)
73            # override eos id in config.json with tiktoken eos id
74            self.gguf_writer.add_eos_token_id(tokenizer.eos_id)  # ty: ignore[unresolved-attribute]
75        else:
76            raise NotImplementedError(f"Deepseek pre-tokenizer {tokpre!r} is not supported yet!")
77
78    def set_gguf_parameters(self):
79        # note: To enable MLA KV cache, attention needs to be converted into MQA (ie: GQA with 1 group)
80        self.hparams["num_key_value_heads"] = 1
81
82        super().set_gguf_parameters()
83        self.gguf_writer.add_vocab_size(self.hparams["vocab_size"])
84
85        # KDA & MLA params
86        # Get ssm_d_conv from linear_attn_config.short_conv_kernel_size or ssm_d_conv
87        linear_attn_config = self.hparams["linear_attn_config"]
88        # n_head == 0 for KDA layers, n_head > 0 for MLA layers
89        # full_attention_layers list will be used to distinguish layer type
90        _num_kv_heads = list()
91        _full_attn_layers = linear_attn_config["full_attn_layers"]
92        for il in range(self.hparams["num_hidden_layers"]):
93            if il + 1 in _full_attn_layers:
94                _num_kv_heads.append(self.hparams["num_key_value_heads"])
95            else:
96                _num_kv_heads.append(0)
97        assert len(_num_kv_heads) == self.hparams["num_hidden_layers"]
98        self.gguf_writer.add_head_count_kv(_num_kv_heads)
99
100        if (ssm_d_conv := linear_attn_config.get("short_conv_kernel_size")) is not None:
101            self.gguf_writer.add_ssm_conv_kernel(ssm_d_conv)
102        if (kda_head_dim := linear_attn_config.get("head_dim")) is not None:
103            self.gguf_writer.add_kda_head_dim(kda_head_dim)
104
105        # MLA params - use add_* methods that handle arch substitution
106        # Support both HuggingFace naming (q_lora_rank, kv_lora_rank) and internal naming (n_lora_q, n_lora_kv)
107        if (q_lora_rank := self.find_hparam(["q_lora_rank", "n_lora_q"], optional=True)) is not None:
108            self.gguf_writer.add_q_lora_rank(q_lora_rank)
109        # To enable MLA KV cache, MLA needs to be converted into MQA with larger heads, then decompresses to MHA
110        kv_lora_rank = self.find_hparam(["kv_lora_rank", "n_lora_kv"], optional=False)
111        self.gguf_writer.add_kv_lora_rank(kv_lora_rank)
112
113        # MLA head dimensions
114        # Support HuggingFace naming: qk_nope_head_dim, qk_rope_head_dim, v_head_dim
115        qk_nope_head_dim = self.hparams.get("qk_nope_head_dim")
116        # Rotation - use qk_rope_head_dim for Kimi
117        qk_rope_head_dim = self.find_hparam(["qk_rope_head_dim", "n_rot"], optional=False)
118        self.gguf_writer.add_rope_dimension_count(qk_rope_head_dim)
119        self.gguf_writer.add_key_length(kv_lora_rank + qk_rope_head_dim)
120        v_head_dim = self.hparams.get("v_head_dim")
121
122        # Calculate n_embd_head_k_mla = qk_nope_head_dim + qk_rope_head_dim
123        if (n_embd_head_k_mla := self.find_hparam(["n_embd_head_k_mla"], optional=True)) is not None:
124            self.gguf_writer.add_key_length_mla(n_embd_head_k_mla)
125        elif qk_nope_head_dim is not None:
126            n_embd_head_k_mla = qk_nope_head_dim + qk_rope_head_dim
127            self.gguf_writer.add_key_length_mla(n_embd_head_k_mla)
128
129        # n_embd_head_v_mla = v_head_dim
130        if (n_embd_head_v_mla := self.hparams.get("n_embd_head_v_mla")) is not None:
131            self.gguf_writer.add_value_length_mla(n_embd_head_v_mla)
132        elif v_head_dim is not None:
133            self.gguf_writer.add_value_length_mla(v_head_dim)
134
135        # moe_intermediate_size (1024 for Kimi)
136        self.gguf_writer.add_expert_feed_forward_length(self.hparams["moe_intermediate_size"])
137        # num_shared_experts (1 for Kimi)
138        self.gguf_writer.add_expert_shared_count(self.hparams["num_shared_experts"])
139        # first_k_dense_replace (1 for Kimi - first layer uses dense MLP)
140        self.gguf_writer.add_leading_dense_block_count(self.hparams["first_k_dense_replace"])
141        # Routed scaling factor (expert_weights_scale = 2.446 for Kimi)
142        self.gguf_writer.add_expert_weights_scale(self.hparams["routed_scaling_factor"])
143
144    def prepare_tensors(self):
145        super().prepare_tensors()
146        if self._experts is not None:
147            experts = [k for d in self._experts for k in d.keys()]
148            if len(experts) > 0:
149                raise ValueError(f"Unprocessed experts: {experts}")
150
151    def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
152        logger.info(f"Processing {name}: shape before = {tuple(data_torch.shape)}")
153
154        # Handle KDA conv1d weights
155        # HuggingFace/vLLM stores as [d_inner, d_conv] (2D), memory layout: conv_step changes fastest
156        # llama.cpp expects ggml ne = [d_conv, 1, d_inner, 1], memory layout: ne[0]=d_conv changes fastest
157        # GGUF reverses numpy shape when writing, so numpy (1, d_inner, 1, d_conv) -> ggml ne = [d_conv, 1, d_inner, 1]
158        # Memory layouts match: both have conv_step (d_conv) changing fastest
159        if name.endswith((".q_conv1d.weight", ".k_conv1d.weight", ".v_conv1d.weight")):
160            # HF shape: [d_inner, d_conv] e.g. [4096, 4]
161            # Target numpy shape: (1, d_inner, 1, d_conv) -> ggml ne = [d_conv, 1, d_inner, 1]
162            if data_torch.ndim == 2:
163                d_inner, d_conv = data_torch.shape
164                # Reshape to (1, d_inner, 1, d_conv) - memory layout preserved (d_conv fastest)
165                data_torch = data_torch.reshape(1, d_inner, 1, d_conv)
166                logger.info(f"Reshaped conv1d weight {name}: [d_inner={d_inner}, d_conv={d_conv}] -> numpy {tuple(data_torch.shape)} -> ggml ne=[{d_conv}, 1, {d_inner}, 1]")
167            elif data_torch.ndim == 3:
168                # Already 3D [d_inner, 1, d_conv] from unsqueeze
169                d_inner, _, d_conv = data_torch.shape
170                data_torch = data_torch.reshape(1, d_inner, 1, d_conv)
171                logger.info(f"Reshaped conv1d weight {name}: [d_inner={d_inner}, 1, d_conv={d_conv}] -> numpy {tuple(data_torch.shape)} -> ggml ne=[{d_conv}, 1, {d_inner}, 1]")
172
173        # Handle A_log: iHF stores as [1, 1, num_heads, 1]
174        # llama.cpp expects ggml ne = [1, num_heads, 1, 1]
175        # GGUF reverses numpy shape: numpy (1, 1, num_heads, 1) -> ggml ne = [1, num_heads, 1, 1]
176        if name.endswith(".A_log"):
177            data_torch = -torch.exp(data_torch)
178        if name.endswith(".dt_bias"):
179            name = name.rpartition(".dt_bias")[0] + ".dt_proj.bias"
180            logger.info("Changed dt_bias to dt_proj.bias")
181
182        # process the experts separately
183        if name.find("block_sparse_moe.experts") != -1:
184            n_experts = self.find_hparam(["num_local_experts", "num_experts"])
185            assert bid is not None
186
187            if self._experts is None:
188                self._experts = [{} for _ in range(self.block_count)]
189
190            self._experts[bid][name] = data_torch
191
192            if len(self._experts[bid]) >= n_experts * 3:
193                # merge the experts into a single 3d tensor
194                # w1: gate, w2: down, w3: up
195                for wid, tname in [("w1", gguf.MODEL_TENSOR.FFN_GATE_EXP),
196                                   ("w2", gguf.MODEL_TENSOR.FFN_DOWN_EXP),
197                                   ("w3", gguf.MODEL_TENSOR.FFN_UP_EXP)]:
198                    datas: list[Tensor] = []
199                    for xid in range(n_experts):
200                        ename = f"model.layers.{bid}.block_sparse_moe.experts.{xid}.{wid}.weight"
201                        datas.append(self._experts[bid][ename])
202                        del self._experts[bid][ename]
203                    data_torch = torch.stack(datas, dim=0)
204                    new_name = self.format_tensor_name(tname, bid)
205                    yield from super().modify_tensors(data_torch, new_name, bid)
206            return
207
208        # note: MLA with the absorption optimization, needs these two split and k_b_proj transposed
209        if name.endswith("kv_b_proj.weight"):
210            name_kb = name.replace("kv_b_proj", "k_b_proj")
211            name_vb = name.replace("kv_b_proj", "v_b_proj")
212            n_head_kv = self.hparams["num_key_value_heads"]
213            v_head_dim = self.find_hparam(["n_embd_head_v_mla", "v_head_dim"], optional=False)
214            qk_nope_head_dim = self.hparams["qk_nope_head_dim"]
215            logger.info("Split kv_b n_head_kv %d\n" % n_head_kv)
216            assert data_torch.shape[0] == n_head_kv * (v_head_dim + qk_nope_head_dim)
217            kv_b = data_torch.view(n_head_kv, v_head_dim + qk_nope_head_dim, data_torch.shape[-1])
218            k_b, v_b = torch.split(kv_b, [qk_nope_head_dim, v_head_dim], dim=1)
219            k_b = k_b.transpose(1, 2)
220            yield from super().modify_tensors(k_b, name_kb, bid)
221            yield from super().modify_tensors(v_b, name_vb, bid)
222            return
223
224        yield from super().modify_tensors(data_torch, name, bid)