char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
fa88ae936
char/aliceai-t5-moe
1from __future__import annotations 2 3import re 4from pathlib import Path 5from typing import Callable ,Iterable ,Iterator ,TYPE_CHECKING 6 7import numpy as np 8import torch 9 10if TYPE_CHECKING : 11from torch import Tensor 12 13from .base import LazyTorchTensor ,ModelBase ,TextModel ,gguf ,logger 14 15from .kimi_linear import KimiLinearModel 16 17 18@ ModelBase . register ( "KimiK3ForConditionalGeneration" ) 19@ ModelBase . example ( "moonshotai/Kimi-K3" ) 20class KimiK3Model (TextModel ): 21""" 22Kimi-K3 text model (KimiLinearForCausalLM under a `language_model.` prefix). 23 24Shares the hybrid MLA + KDA skeleton with kimi-linear, but that converter 25cannot load it: K3 adds cross-layer attention residuals, a latent MoE, the 26situ activation, an MLA output gate and a full-rank KDA gate. 27 28The vision tower and mm_projector are skipped - text only for now. 29""" 30 31model_arch = gguf .MODEL_ARCH .KIMI_K3 32 33_experts :list [dict [str ,Tensor ]]| None = None 34 35# `<x>_res_norm.weight` and `<x>_res_proj.weight` are only used as their 36# elementwise product, so they are fused into one [n_embd] vector here. 37# they arrive apart, so buffer the first one and tag it with its kind. 38_res_parts :dict [str ,tuple [str ,Tensor ]] 39 40# HF suffix -> (gguf tensor, per-layer?) 41_RES_FUSIONS = { 42"self_attention_res" : (gguf .MODEL_TENSOR .ATTN_RES_SCORE ,True ), 43"mlp_res" : (gguf .MODEL_TENSOR .FFN_RES_SCORE ,True ), 44"output_attn_res" : (gguf .MODEL_TENSOR .OUTPUT_RES_SCORE ,False ), 45 } 46 47# compressed-tensors MXFP4. the `language_model.` prefix is still there, as 48# self.model_tensors is keyed by the raw checkpoint names 49_MXFP4_FORMAT = "mxfp4-pack-quantized" 50_MXFP4_EXPERT_RE = re .compile ( 51r"^(?:language_model\.)?model\.layers\.(\d+)" 52r"\.block_sparse_moe\.experts\.(\d+)\.(w[123])\.weight_packed$" 53 ) 54_MXFP4_PROJ = { 55"w1" :gguf .MODEL_TENSOR .FFN_GATE_EXP , 56"w2" :gguf .MODEL_TENSOR .FFN_DOWN_EXP , 57"w3" :gguf .MODEL_TENSOR .FFN_UP_EXP , 58 } 59 60def __init__ (self ,* args ,** kwargs ): 61super ().__init__ (* args ,** kwargs ) 62self ._res_parts = {} 63 64def set_vocab (self ): 65# K3 has the same TikToken vocab as K2, so kimi-linear's vocab handling works. 66# borrowed, not inherited: the method only touches TextModel members, and K3 67# shares none of kimi-linear's tensor layout. 68KimiLinearModel .set_vocab (self )# ty: ignore[invalid-argument-type] 69 70# ...but that forces eos to the tokenizer's eos_id, which is [EOS], the 71# document terminator. K3's config says <|end_of_msg|>, the turn terminator; 72# with [EOS] the generation never stops at the end of a turn. 73if (eos := self .hparams .get ("eos_token_id" ))is not None : 74logger .info (f"restoring configured eos_token_id { eos } (kimi-linear forces the tokenizer's)" ) 75self .gguf_writer .add_eos_token_id (eos ) 76 77# K3 renders chats in python (encoding_k3.py) and ships no jinja template, 78# so add the bundled one when the model has none 79if gguf .SpecialVocab (self .dir_model ,load_merges = False ).chat_template is None : 80template_path = Path (__file__ ).parent .parent / "models" / "templates" / "Kimi-K3.jinja" 81logger .info (f"gguf: model has no chat template, using { template_path . name } " ) 82self .gguf_writer .add_chat_template (template_path .read_text (encoding = "utf-8" )) 83 84# 85# compressed-tensors MXFP4 -> ggml MXFP4 86# 87 88def _is_mxfp4_packed (self )-> bool : 89quant_config = self .hparams .get ("quantization_config" )or {} 90return (quant_config .get ("quant_method" )== "compressed-tensors" 91and quant_config .get ("format" )== self ._MXFP4_FORMAT ) 92 93def dequant_model (self ): 94if not self ._is_mxfp4_packed (): 95return super ().dequant_model () 96 97# skipping base.py's dequant is only safe if the experts are the only 98# quantized tensors, so check it 99stray = [n for n in self .model_tensors 100if n .endswith (".weight_packed" )and not self ._MXFP4_EXPERT_RE .match (n )] 101if stray : 102raise NotImplementedError ( 103f" { len ( stray ) } MXFP4 tensor(s) outside the routed experts, e.g. { stray [ 0 ]!r } ; " 104"only the routed experts have a repack path" 105 ) 106 107def _mxfp4_expert_tensor (self ,loaders :list [tuple [Callable [[],Tensor ],Callable [[],Tensor ]]]): 108""" 109One stacked [n_expert, rows, cols] MXFP4 tensor, built lazily. 110 111gguf_writer holds every added tensor until the final write, so building 112this eagerly (like the DeepSeek-V4 path does) keeps all ~1.38 TB of 113experts in memory. lazy means only the tensor being written is resident. 114""" 115# meta shapes, so this does not read any weights 116rows ,packed_cols = loaders [0 ][0 ]().shape 117n_blocks = (packed_cols * 2 )// 32 118byte_shape = (len (loaders ),rows ,n_blocks * 17 ) 119 120def load (fns :list [tuple [Callable [[],Tensor ],Callable [[],Tensor ]]])-> np .ndarray : 121out = np .empty (byte_shape ,dtype = np .uint8 ) 122for eid , (packed_fn ,scale_fn )in enumerate (fns ): 123out [eid ]= self .repack_mxfp4_blocks ( 124LazyTorchTensor .to_eager (packed_fn ()), 125LazyTorchTensor .to_eager (scale_fn ()), 126 ) 127return out 128 129# loaders goes through args, not the closure, so that `func` matches 130# LazyBase's single-argument shape 131return gguf .LazyNumpyTensor ( 132meta = gguf .LazyNumpyTensor .meta_with_dtype_and_shape (np .uint8 ,byte_shape ), 133args = (loaders ,), 134func = load , 135 ) 136 137def _write_mxfp4_experts (self )-> None : 138n_experts = self .hparams ["num_experts" ] 139 140# (bid, wid) -> {expert id: (packed name, scale name)} 141groups :dict [tuple [int ,str ],dict [int ,tuple [str ,str ]]]= {} 142for name in self .model_tensors : 143m = self ._MXFP4_EXPERT_RE .match (name ) 144if m is None : 145continue 146bid ,eid ,wid = int (m .group (1 )),int (m .group (2 )),m .group (3 ) 147scale_name = name .removesuffix ("_packed" )+ "_scale" 148if scale_name not in self .model_tensors : 149raise KeyError (f"missing { scale_name } for { name } " ) 150groups .setdefault ((bid ,wid ), {})[eid ]= (name ,scale_name ) 151 152consumed :list [str ]= [] 153for (bid ,wid ),experts in sorted (groups .items ()): 154missing = [e for e in range (n_experts )if e not in experts ] 155if missing : 156raise KeyError ( 157f"layer { bid } { wid } : { len ( missing ) } of { n_experts } experts missing, " 158f"first is { missing [ 0 ] } " 159 ) 160if len (experts )!= n_experts : 161raise KeyError (f"layer { bid } { wid } : { len ( experts ) } experts, expected { n_experts } " ) 162 163loaders = [] 164for eid in range (n_experts ): 165packed_name ,scale_name = experts [eid ] 166loaders .append ((self .model_tensors [packed_name ],self .model_tensors [scale_name ])) 167consumed += [packed_name ,scale_name ] 168 169data = self ._mxfp4_expert_tensor (loaders ) 170new_name = self .format_tensor_name (self ._MXFP4_PROJ [wid ],bid ) 171shape = gguf .quant_shape_from_byte_shape (data .shape ,gguf .GGMLQuantizationType .MXFP4 ) 172logger .info ( 173f" { new_name } : repacked { n_experts } experts to MXFP4, " 174f"shape = {{ { ', ' . join ( str ( n ) for n in reversed ( shape )) } }}" 175 ) 176self .gguf_writer .add_tensor (new_name ,data ,raw_dtype = gguf .GGMLQuantizationType .MXFP4 ) 177 178for name in consumed : 179del self .model_tensors [name ] 180 181def generate_extra_tensors (self )-> Iterable [tuple [str ,Tensor ]]: 182# not a generator on purpose: base.py chains this with get_tensors(), so the 183# tensors used here must be removed from model_tensors before that starts 184if self ._is_mxfp4_packed (): 185self ._write_mxfp4_experts () 186return () 187 188def get_tensors (self )-> Iterator [tuple [str ,Tensor ]]: 189for name ,data in super ().get_tensors (): 190if name .startswith (("vision_tower." ,"mm_projector." )): 191continue # text only 192if name .startswith ("language_model." ): 193name = name [len ("language_model." ):] 194yield name ,data 195 196def set_gguf_parameters (self ): 197# MLA is served as MQA with a single large head, then decompressed 198self .hparams ["num_key_value_heads" ]= 1 199 200super ().set_gguf_parameters () 201self .gguf_writer .add_vocab_size (self .hparams ["vocab_size" ]) 202 203linear_attn_config = self .hparams ["linear_attn_config" ] 204 205# n_head_kv == 0 marks a KDA (recurrent) layer. the layer lists are 1-indexed, 206# as KimiLinearConfig.is_kda_layer uses (layer_idx + 1) 207full_attn_layers = linear_attn_config ["full_attn_layers" ] 208n_kv_heads = [ 209self .hparams ["num_key_value_heads" ]if (il + 1 )in full_attn_layers else 0 210for il in range (self .hparams ["num_hidden_layers" ]) 211 ] 212assert len (n_kv_heads )== self .hparams ["num_hidden_layers" ] 213self .gguf_writer .add_head_count_kv (n_kv_heads ) 214 215# --- KDA --- 216self .gguf_writer .add_ssm_conv_kernel (linear_attn_config ["short_conv_kernel_size" ]) 217self .gguf_writer .add_kda_head_dim (linear_attn_config ["head_dim" ]) 218if (lb := linear_attn_config .get ("gate_lower_bound" ))is not None : 219self .gguf_writer .add_kda_gate_lower_bound (lb ) 220 221# --- MLA --- 222if (q_lora_rank := self .hparams .get ("q_lora_rank" ))is not None : 223self .gguf_writer .add_q_lora_rank (q_lora_rank ) 224kv_lora_rank = self .hparams ["kv_lora_rank" ] 225self .gguf_writer .add_kv_lora_rank (kv_lora_rank ) 226 227qk_nope_head_dim = self .hparams ["qk_nope_head_dim" ] 228qk_rope_head_dim = self .hparams ["qk_rope_head_dim" ] 229v_head_dim = self .hparams ["v_head_dim" ] 230# K3 is nope-only; qk_rope_head_dim still sizes the un-absorbed part of K 231assert self .hparams .get ("mla_use_nope" ),"K3 MLA is expected to be nope-only" 232self .gguf_writer .add_rope_dimension_count (qk_rope_head_dim ) 233# MLA is served as MQA, so the cache holds the compressed latent 234self .gguf_writer .add_key_length (kv_lora_rank + qk_rope_head_dim ) 235self .gguf_writer .add_value_length (kv_lora_rank ) 236self .gguf_writer .add_key_length_mla (qk_nope_head_dim + qk_rope_head_dim ) 237self .gguf_writer .add_value_length_mla (v_head_dim ) 238 239# --- MoE --- 240self .gguf_writer .add_expert_feed_forward_length (self .hparams ["moe_intermediate_size" ]) 241self .gguf_writer .add_expert_shared_count (self .hparams ["num_shared_experts" ]) 242self .gguf_writer .add_leading_dense_block_count (self .hparams ["first_k_dense_replace" ]) 243self .gguf_writer .add_expert_weights_scale (self .hparams ["routed_scaling_factor" ]) 244self .gguf_writer .add_expert_weights_norm (self .hparams ["moe_renormalize" ]) 245assert self .hparams ["moe_router_activation_func" ]== "sigmoid" 246self .gguf_writer .add_expert_gating_func (gguf .ExpertGatingFuncType .SIGMOID ) 247# latent MoE: routed experts live in a down-projected space 248if (latent := self .hparams .get ("routed_expert_hidden_size" ))is not None : 249self .gguf_writer .add_expert_latent_length (latent ) 250 251# --- situ activation --- 252assert self .hparams ["hidden_act" ]== "situ" , \ 253f"unexpected hidden_act { self . hparams [ 'hidden_act' ]!r } " 254self .gguf_writer .add_activation_situ_beta (self .hparams ["activation_situ_beta" ]) 255self .gguf_writer .add_activation_situ_linear_beta (self .hparams ["activation_situ_linear_beta" ]) 256 257# --- cross-layer attention residuals --- 258self .gguf_writer .add_attn_res_block_size (self .hparams ["attn_res_block_size" ]) 259 260def prepare_tensors (self ): 261super ().prepare_tensors () 262if self ._experts is not None : 263leftover = [k for d in self ._experts for k in d .keys ()] 264if leftover : 265raise ValueError (f"Unprocessed experts: { leftover } " ) 266if self ._res_parts : 267raise ValueError (f"Unpaired attention-residual tensors: { sorted ( self . _res_parts ) } " ) 268if self ._is_mxfp4_packed (): 269# label the file for what it is; prepare_metadata runs after this 270self ._is_mxfp4 = True 271self .ftype = gguf .LlamaFileType .MOSTLY_MXFP4_MOE 272 273def _try_fuse_res (self ,data_torch :Tensor ,name :str ,bid :int | None ): 274""" 275Pair <x>_res_norm.weight with <x>_res_proj.weight and emit their product. 276 277Returns None if this is not a res tensor, [] if buffered until its pair. 278""" 279for prefix , (tensor_id ,per_layer )in self ._RES_FUSIONS .items (): 280for kind in ("norm" ,"proj" ): 281if not name .endswith (f" { prefix } _ { kind } .weight" ): 282continue 283key = f" { prefix } . { bid } " 284other = self ._res_parts .pop (key ,None ) 285if other is None : 286self ._res_parts [key ]= (kind ,data_torch ) 287return [] 288other_kind ,other_data = other 289assert other_kind != kind ,f"duplicate { kind } for { key } " 290norm = data_torch if kind == "norm" else other_data 291proj = data_torch if kind == "proj" else other_data 292fused = norm .float ().flatten ()* proj .float ().flatten () 293# ".weight" suffix matches the convention map_tensor_name applies 294new_name = (self .format_tensor_name (tensor_id ,bid )if per_layer 295else gguf .TENSOR_NAMES [tensor_id ]+ ".weight" ) 296logger .info (f"fused { prefix } _norm * { prefix } _proj -> { new_name } " ) 297return [(new_name ,fused )] 298return None 299 300def modify_tensors (self ,data_torch :Tensor ,name :str ,bid :int | None )-> Iterable [tuple [str ,Tensor ]]: 301# --- cross-layer attention residuals: fuse norm * proj --- 302fused = self ._try_fuse_res (data_torch ,name ,bid ) 303if fused is not None : 304yield from fused 305return 306 307# --- KDA conv1d: HF [d_inner, 1, d_conv] -> ggml ne [d_conv, 1, d_inner, 1] --- 308# GGUF reverses the numpy shape on write, so target numpy (1, d_inner, 1, d_conv). 309# conv_step varies fastest in both layouts, so this is a pure reshape. 310if name .endswith ((".q_conv1d.weight" ,".k_conv1d.weight" ,".v_conv1d.weight" )): 311if data_torch .ndim == 3 :# [d_inner, 1, d_conv] 312d_inner ,_ ,d_conv = data_torch .shape 313elif data_torch .ndim == 2 :# [d_inner, d_conv] 314d_inner ,d_conv = data_torch .shape 315else : 316raise ValueError (f"unexpected conv1d rank { data_torch . ndim } for { name } " ) 317data_torch = data_torch .reshape (1 ,d_inner ,1 ,d_conv ) 318 319# -exp(A_log) is folded here so the graph does not have to 320if name .endswith (".A_log" ): 321n_head = self .hparams ["num_attention_heads" ] 322data_torch = - torch .exp (data_torch .float ()[:n_head ]) 323 324# dt_bias -> the name SSM_DT's mapping expects 325if name .endswith (".dt_bias" ): 326name = name .rpartition (".dt_bias" )[0 ]+ ".dt_proj.bias" 327 328# --- g_proj is two different tensors sharing one HF name --- 329# KDA layers: full-rank gate, [d_inner, n_embd] (replaces g_a/g_b) 330# MLA layers: output gate, [n_head*v_head_dim, n_embd] 331# Name-based mapping cannot tell them apart, so resolve by layer type. 332if name .endswith (".self_attn.g_proj.weight" ): 333assert bid is not None 334is_kda = (bid + 1 )not in self .hparams ["linear_attn_config" ]["full_attn_layers" ] 335tensor_id = gguf .MODEL_TENSOR .SSM_G if is_kda else gguf .MODEL_TENSOR .ATTN_GATE 336yield self .format_tensor_name (tensor_id ,bid ),data_torch 337return 338 339# --- routed experts: stack per-expert 2D weights into one 3D tensor --- 340if ".block_sparse_moe.experts." in name : 341n_experts = self .hparams ["num_experts" ] 342assert bid is not None 343 344if self ._experts is None : 345self ._experts = [{}for _ in range (self .block_count )] 346self ._experts [bid ][name ]= data_torch 347 348if len (self ._experts [bid ])< n_experts * 3 : 349return 350 351# w1: gate, w2: down, w3: up 352for wid ,tensor_id in (("w1" ,gguf .MODEL_TENSOR .FFN_GATE_EXP ), 353 ("w2" ,gguf .MODEL_TENSOR .FFN_DOWN_EXP ), 354 ("w3" ,gguf .MODEL_TENSOR .FFN_UP_EXP )): 355datas = [] 356for xid in range (n_experts ): 357ename = f"model.layers. { bid } .block_sparse_moe.experts. { xid } . { wid } .weight" 358datas .append (self ._experts [bid ].pop (ename )) 359stacked = torch .stack (datas ,dim = 0 ) 360yield from super ().modify_tensors (stacked ,self .format_tensor_name (tensor_id ,bid ),bid ) 361return 362 363# --- MLA absorption: split kv_b into k_b (transposed) and v_b --- 364if name .endswith ("kv_b_proj.weight" ): 365n_head_kv = self .hparams ["num_key_value_heads" ] 366v_head_dim = self .hparams ["v_head_dim" ] 367qk_nope_head_dim = self .hparams ["qk_nope_head_dim" ] 368assert data_torch .shape [0 ]== n_head_kv * (v_head_dim + qk_nope_head_dim ) 369kv_b = data_torch .view (n_head_kv ,v_head_dim + qk_nope_head_dim ,data_torch .shape [- 1 ]) 370k_b ,v_b = torch .split (kv_b , [qk_nope_head_dim ,v_head_dim ],dim = 1 ) 371k_b = k_b .transpose (1 ,2 ) 372yield from super ().modify_tensors (k_b ,name .replace ("kv_b_proj" ,"k_b_proj" ),bid ) 373yield from super ().modify_tensors (v_b ,name .replace ("kv_b_proj" ,"v_b_proj" ),bid ) 374return 375 376yield from super ().modify_tensors (data_torch ,name ,bid )