char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
9a7570587
char/aliceai-t5-moe
1from __future__import annotations 2 3import json 4 5from typing import Any ,Callable ,Iterable ,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 15 16@ ModelBase . register ( "QWenLMHeadModel" ) 17@ ModelBase . example ( "Qwen/Qwen-7B" ) 18class QwenModel (TextModel ): 19model_arch = gguf .MODEL_ARCH .QWEN 20 21@staticmethod 22def token_bytes_to_string (b ): 23from transformers .convert_slow_tokenizer import bytes_to_unicode 24byte_encoder = bytes_to_unicode () 25return '' .join ([byte_encoder [ord (char )]for char in b .decode ('latin-1' )]) 26 27@staticmethod 28def bpe (mergeable_ranks :dict [bytes ,int ],token :bytes ,max_rank :int | None = None )-> list [bytes ]: 29parts = [bytes ([b ])for b in token ] 30while True : 31min_idx = None 32min_rank = None 33for i ,pair in enumerate (zip (parts [:- 1 ],parts [1 :])): 34rank = mergeable_ranks .get (pair [0 ]+ pair [1 ]) 35if rank is not None and (min_rank is None or rank < min_rank ): 36min_idx = i 37min_rank = rank 38if min_rank is None or (max_rank is not None and min_rank >= max_rank ): 39break 40assert min_idx is not None 41parts = parts [:min_idx ]+ [parts [min_idx ]+ parts [min_idx + 1 ]]+ parts [min_idx + 2 :] 42return parts 43 44def set_vocab (self ): 45self ._set_vocab_qwen () 46 47 48@ ModelBase . register ( 49"Qwen2Model" , 50"Qwen2ForCausalLM" , 51"Qwen2AudioForConditionalGeneration" , 52"KORMoForCausalLM" , 53"AudioFlamingo3ForConditionalGeneration" , 54"DotsOCRForCausalLM" , 55) 56@ ModelBase . example ( "Qwen/Qwen2.5-7B-Instruct" ) 57class Qwen2Model (TextModel ): 58model_arch = gguf .MODEL_ARCH .QWEN2 59 60def set_vocab (self ): 61try : 62self ._set_vocab_sentencepiece () 63except FileNotFoundError : 64self ._set_vocab_gpt2 () 65 66def set_gguf_parameters (self ): 67super ().set_gguf_parameters () 68self ._try_set_pooling_type () 69 70def modify_tensors (self ,data_torch :Tensor ,name :str ,bid :int | None )-> Iterable [tuple [str ,Tensor ]]: 71if self .hf_arch == "Qwen2Model" : 72name = f"model. { name } " # map to Qwen2ForCausalLM tensors 73yield from super ().modify_tensors (data_torch ,name ,bid ) 74 75 76@ ModelBase . register ( "Qwen2MoeForCausalLM" ) 77@ ModelBase . example ( "Qwen/Qwen1.5-MoE-A2.7B" ) 78class Qwen2MoeModel (TextModel ): 79model_arch = gguf .MODEL_ARCH .QWEN2MOE 80 81def set_gguf_parameters (self ): 82super ().set_gguf_parameters () 83if (moe_intermediate_size := self .hparams .get ("moe_intermediate_size" ))is not None : 84self .gguf_writer .add_expert_feed_forward_length (moe_intermediate_size ) 85logger .info (f"gguf: expert feed forward length = { moe_intermediate_size } " ) 86if (shared_expert_intermediate_size := self .hparams .get ('shared_expert_intermediate_size' ))is not None : 87self .gguf_writer .add_expert_shared_feed_forward_length (shared_expert_intermediate_size ) 88logger .info (f"gguf: expert shared feed forward length = { shared_expert_intermediate_size } " ) 89 90_experts :list [dict [str ,Tensor ]]| None = None 91 92def modify_tensors (self ,data_torch :Tensor ,name :str ,bid :int | None )-> Iterable [tuple [str ,Tensor ]]: 93# handle aggregated expert tensors 94# GGUF stores dimensions reversed from PyTorch, so: 95# PyTorch (A,B,C) -> GGUF writes [C,B,A] -> GGML reads ne={C,B,A} 96# Input shapes from HF: (n_expert, n_ff_exp, n_embd) or (n_expert, n_embd, n_ff_exp) 97# Expected GGML ne: {n_embd, n_ff_exp, n_expert} for gate/up, {n_ff_exp, n_embd, n_expert} for down 98if name .endswith ("mlp.experts.down_proj" )or name .endswith ("mlp.experts.down_proj.weight" ): 99mapped = f" { name } .weight" if not name .endswith (".weight" )else name 100# HF: [n_expert, n_embd, n_ff] -> GGML: {n_ff, n_embd, n_expert} 101yield from super ().modify_tensors (data_torch ,mapped ,bid ) 102return 103 104if name .endswith ("mlp.experts.gate_up_proj" )or name .endswith ("mlp.experts.gate_up_proj.weight" ): 105if data_torch .ndim < 3 or data_torch .shape [- 2 ]% 2 != 0 : 106raise ValueError (f"Unexpected gate_up_proj shape for { name } : { tuple ( data_torch . shape ) } " ) 107# HF: [n_expert, 2*n_ff, n_embd] -> split on dim=-2 108n_ff = data_torch .shape [- 2 ]// 2 109gate = data_torch [..., :n_ff , :].contiguous () 110up = data_torch [...,n_ff :, :].contiguous () 111# gate/up: [n_expert, n_ff, n_embd] -> GGML: {n_embd, n_ff, n_expert} 112base_name = name .removesuffix (".weight" ).removesuffix (".gate_up_proj" ) 113mapped_gate = f" { base_name } .gate_proj.weight" 114mapped_up = f" { base_name } .up_proj.weight" 115yield from super ().modify_tensors (gate ,mapped_gate ,bid ) 116yield from super ().modify_tensors (up ,mapped_up ,bid ) 117return 118 119if name .find ("experts" )!= - 1 : 120n_experts = self .find_hparam (["num_local_experts" ,"num_experts" ]) 121assert bid is not None 122 123if self ._experts is None : 124self ._experts = [{}for _ in range (self .block_count )] 125 126self ._experts [bid ][name ]= data_torch 127 128if len (self ._experts [bid ])>= n_experts * 3 : 129# merge the experts into a single 3d tensor 130for w_name in ["down_proj" ,"gate_proj" ,"up_proj" ]: 131datas :list [Tensor ]= [] 132 133for xid in range (n_experts ): 134ename = f"model.layers. { bid } .mlp.experts. { xid } . { w_name } .weight" 135datas .append (self ._experts [bid ][ename ]) 136del self ._experts [bid ][ename ] 137 138data_torch = torch .stack (datas ,dim = 0 ) 139 140merged_name = f"model.layers. { bid } .mlp.experts. { w_name } .weight" 141 142yield from super ().modify_tensors (data_torch ,merged_name ,bid ) 143return 144else : 145return 146 147yield from super ().modify_tensors (data_torch ,name ,bid ) 148 149def prepare_tensors (self ): 150super ().prepare_tensors () 151 152if self ._experts is not None : 153# flatten `list[dict[str, Tensor]]` into `list[str]` 154experts = [k for d in self ._experts for k in d .keys ()] 155if len (experts )> 0 : 156raise ValueError (f"Unprocessed experts: { experts } " ) 157 158 159@ ModelBase . register ( "Qwen3ForCausalLM" , "Qwen3Model" ) 160@ ModelBase . example ( "Qwen/Qwen3-8B" ) 161class Qwen3Model (Qwen2Model ): 162model_arch = gguf .MODEL_ARCH .QWEN3 163 164# extra logic for rerank models 165is_rerank :bool = False 166is_tied_embeddings :bool = False 167token_false_id :int | None = None 168token_true_id :int | None = None 169 170def __init__ (self ,* args ,** kwargs ): 171super ().__init__ (* args ,** kwargs ) 172 173# track for intern-s1-mini 174hparams = ModelBase .load_hparams (self .dir_model ,is_mistral_format = False ) 175self .origin_hf_arch = hparams .get ('architectures' , [None ])[0 ] 176 177if self ._is_qwen3_reranker (): 178self ._find_rerank_config () 179 180def _is_qwen3_reranker (self )-> bool : 181readme_path = self .dir_model / "README.md" 182readme_text = "" 183if readme_path .exists (): 184with readme_path .open ("r" ,encoding = "utf-8" )as f : 185readme_text = f .read () 186 187name_hints = [ 188str (self .dir_model .name ), 189str (self .hparams .get ("_name_or_path" ,"" )), 190str (self .hparams .get ("model_type" ,"" )), 191str (self .origin_hf_arch or "" ), 192 ] 193name_hints = [hint .lower ()for hint in name_hints if hint ] 194 195if "# qwen3-reranker" in readme_text .lower ()or "# qwen3-vl-reranker" in readme_text .lower (): 196return True 197 198if any ("qwen3-reranker" in hint or "qwen3-vl-reranker" in hint for hint in name_hints ): 199return True 200 201return "sequenceclassification" in (self .origin_hf_arch or "" ).lower () 202 203def set_vocab (self ): 204# deal with intern-s1-mini 205if self .origin_hf_arch == 'InternS1ForConditionalGeneration' : 206self ._set_vocab_interns1 () 207return 208 209super ().set_vocab () 210 211def _find_rerank_config (self ): 212from transformers import AutoTokenizer 213tokenizer = AutoTokenizer .from_pretrained (self .dir_model ) 214 215self .is_rerank = True 216self .is_tied_embeddings = self .hparams .get ("tie_word_embeddings" ,False ) 217self .token_false_id = tokenizer .convert_tokens_to_ids ("no" )# ty: ignore[unresolved-attribute, invalid-assignment] 218self .token_true_id = tokenizer .convert_tokens_to_ids ("yes" )# ty: ignore[unresolved-attribute, invalid-assignment] 219self .sep_token_id = tokenizer .convert_tokens_to_ids ("|" )# ty: ignore[unresolved-attribute] 220 221assert self .token_false_id is not None and self .token_true_id is not None 222 223def set_gguf_parameters (self ): 224super ().set_gguf_parameters () 225if self .is_rerank : 226self .gguf_writer .add_pooling_type (gguf .PoolingType .RANK ) 227self .gguf_writer .add_classifier_output_labels (["yes" ,"no" ]) 228self .gguf_writer .add_chat_template ([{ 229"name" :"rerank" , 230"template" :"<|im_start|>system\nJudge whether the Document meets the requirements based on the Query and the Instruct provided. Note that the answer can only be \"yes\" or \"no\".<|im_end|>\n" 231"<|im_start|>user\n<Instruct>: Given a web search query, retrieve relevant passages that answer the query\n<Query>: {query}\n<Document>: {document}<|im_end|>\n" 232"<|im_start|>assistant\n<think>\n\n</think>\n\n" 233 }]) 234 235def _get_cls_out_tensor (self ,data_torch :Tensor )-> Tensor : 236# extract "yes" and "no" tokens from the output lm_head tensor 237false_row = data_torch [self .token_false_id ] 238true_row = data_torch [self .token_true_id ] 239return torch .stack ([true_row ,false_row ],dim = 0 ) 240 241def modify_tensors (self ,data_torch :Tensor ,name :str ,bid :int | None )-> Iterable [tuple [str ,Tensor ]]: 242if self .is_rerank : 243is_tied_head = self .is_tied_embeddings and "embed_tokens" in name 244is_real_head = not self .is_tied_embeddings and "lm_head" in name 245if is_tied_head or is_real_head : 246cls_out_head = ( 247gguf .TENSOR_NAMES [gguf .MODEL_TENSOR .CLS_OUT ]+ ".weight" , 248self ._get_cls_out_tensor (data_torch ), 249 ) 250yield cls_out_head 251if is_tied_head : 252yield from super ().modify_tensors (data_torch ,name ,bid ) 253return 254 255yield from super ().modify_tensors (data_torch ,name ,bid ) 256 257 258@ ModelBase . register ( "Qwen3MoeForCausalLM" ) 259@ ModelBase . example ( "Qwen/Qwen3-30B-A3B" ) 260class Qwen3MoeModel (Qwen2MoeModel ): 261model_arch = gguf .MODEL_ARCH .QWEN3MOE 262 263def __init__ (self ,* args ,** kwargs ): 264super ().__init__ (* args ,** kwargs ) 265hparams = ModelBase .load_hparams (self .dir_model ,False ) 266self .origin_hf_arch = hparams .get ('architectures' , [None ])[0 ] 267 268def set_vocab (self ): 269# deal with intern-s1 270if self .origin_hf_arch == 'InternS1ForConditionalGeneration' : 271self ._set_vocab_interns1 () 272return 273 274super ().set_vocab () 275 276 277class _QwenMtpMixin : 278"""Shared MTP wiring for Qwen3-Next and Qwen3.5/3.6 text variants. The HF 279config carries the MTP block under `mtp_num_hidden_layers` (computed from 280the checkpoint when absent, e.g. Qwen3-Next) and the tensors under 281`mtp.*`; we extend block_count, emit the nextn metadata key, and remap 282`mtp.*` to the standard layer-indexed nextn naming so the existing 283tensor_map handles them.""" 284 285supports_mtp_export = True 286hparams :dict [str ,Any ] 287model_arch :gguf .MODEL_ARCH 288gguf_writer :gguf .GGUFWriter 289block_count :int 290tensor_map :gguf .TensorNameMap 291no_mtp :bool 292mtp_only :bool 293_original_block_count :int | None = None 294opt_num_mtp_layers :int = 0 295 296def __init__ (self ,* args ,** kwargs ): 297super ().__init__ (* args ,** kwargs ) 298self .block_count = self .hparams ["num_hidden_layers" ] 299if not self .no_mtp : 300n_mtp = self .hparams .get ("mtp_num_hidden_layers" ,0 ) 301# Qwen-3-Next doesn't include `mtp_num_hidden_layers` in config. 302if n_mtp == 0 : 303assert self .opt_num_mtp_layers != 0 304n_mtp = self .opt_num_mtp_layers 305self .block_count += n_mtp 306self .tensor_map = gguf .get_tensor_name_map (self .model_arch ,self .block_count ) 307 308def index_tensors (self ,remote_hf_model_id :str | None = None )-> dict [str ,Callable [[],Tensor ]]: 309hparams = {** self .hparams ,** self .hparams .get ("text_config" , {})} 310key = next ((k for k in ["n_layers" ,"num_hidden_layers" ,"n_layer" ,"num_layers" ]if k in hparams ),None ) 311 type(self )._original_block_count = hparams .get (key ) 312 type(self ).opt_num_mtp_layers = 0 313return super ().index_tensors (remote_hf_model_id = remote_hf_model_id )# ty: ignore[unresolved-attribute] 314 315@classmethod 316def filter_tensors (cls ,item ): 317assert cls ._original_block_count is not None 318# TODO: change TextModel to super() 319if (titem := TextModel .filter_tensors (item ))is None : 320return None 321name ,gen = titem 322if name .startswith ("model.mtp." ): 323name = name .replace ("model." ,"" ,1 ) 324if name .startswith ("mtp." ): 325if cls .no_mtp : 326return None 327remapper = { 328"fc" :"eh_proj" , 329"pre_fc_norm_embedding" :"enorm" , 330"pre_fc_norm_hidden" :"hnorm" , 331"norm" :"shared_head.norm" , 332 } 333parts = name .split ("." ,3 ) 334if len (parts )== 4 and parts [1 ]== "layers" and parts [2 ].isdecimal (): 335mtp_idx = int (parts [2 ]) 336name = f"model.layers. { cls . _original_block_count + mtp_idx } . { parts [ 3 ] } " 337cls .opt_num_mtp_layers = max (cls .opt_num_mtp_layers ,mtp_idx + 1 ) 338elif len (parts )== 3 and parts [1 ]in remapper : 339name = f"model.layers. { cls . _original_block_count } . { remapper [ parts [ 1 ]] } . { parts [ 2 ] } " 340elif cls .mtp_only : 341keep = name in ( 342"model.embed_tokens.weight" ,"model.norm.weight" ,"lm_head.weight" , 343"embed_tokens.weight" ,"norm.weight" , 344 ) 345if not keep : 346return None 347return name ,gen 348 349def set_gguf_parameters (self ): 350super ().set_gguf_parameters ()# ty: ignore[unresolved-attribute] 351if self .no_mtp : 352return 353if (n := self .block_count - self .hparams ["num_hidden_layers" ])> 0 : 354self .gguf_writer .add_nextn_predict_layers (n ) 355 356def prepare_metadata (self ,vocab_only :bool ): 357from_dir = self .fname_out .is_dir () 358super ().prepare_metadata (vocab_only = vocab_only )# ty: ignore[unresolved-attribute] 359 360if not self .mtp_only or not from_dir : 361return 362 363output_type :str = self .ftype .name .partition ("_" )[2 ]# pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute] 364fname_default :str = gguf .naming_convention ( 365self .metadata .name ,self .metadata .basename ,self .metadata .finetune ,# pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute] 366self .metadata .version ,size_label = None ,output_type = output_type ,model_type = None )# pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute] 367self .fname_out = self .fname_out .parent / f"mtp- { fname_default } .gguf" 368 369 370@ ModelBase . register ( "Qwen3NextForCausalLM" ) 371@ ModelBase . example ( "Qwen/Qwen3-Next-80B-A3B-Instruct" ) 372class Qwen3NextModel (_QwenMtpMixin ,Qwen2MoeModel ): 373model_arch = gguf .MODEL_ARCH .QWEN3NEXT 374 375def set_gguf_parameters (self ): 376super ().set_gguf_parameters () 377self .gguf_writer .add_ssm_conv_kernel (self .hparams ["linear_conv_kernel_dim" ]) 378self .gguf_writer .add_ssm_state_size (self .hparams ["linear_key_head_dim" ]) 379self .gguf_writer .add_ssm_group_count (self .hparams ["linear_num_key_heads" ]) 380self .gguf_writer .add_ssm_time_step_rank (self .hparams ["linear_num_value_heads" ]) 381self .gguf_writer .add_ssm_inner_size (self .hparams ["linear_value_head_dim" ]* self .hparams ["linear_num_value_heads" ]) 382if (layer_types := self .hparams .get ("layer_types" ))is not None : 383n_layer = self .hparams ["num_hidden_layers" ] 384if len (layer_types )!= n_layer : 385raise ValueError (f"layer_types has { len ( layer_types ) } entries, expected num_hidden_layers ( { n_layer } )" ) 386recurrent = [t == "linear_attention" for t in layer_types ] 387recurrent += [False ]* (self .block_count - n_layer ) 388self .gguf_writer .add_recurrent_layers (recurrent ) 389self .gguf_writer .add_full_attention_interval (self .hparams .get ("full_attention_interval" ,4 )) 390if (rope_dim := self .hparams .get ("head_dim" ))is None : 391rope_dim = self .hparams ["hidden_size" ]// self .hparams ["num_attention_heads" ] 392self .gguf_writer .add_rope_dimension_count (int (rope_dim * self .rope_parameters .get ("partial_rotary_factor" ,0.25 ))) 393 394def modify_tensors (self ,data_torch :Tensor ,name :str ,bid :int | None )-> Iterable [tuple [str ,Tensor ]]: 395if name .endswith (".A_log" ): 396data_torch = - torch .exp (data_torch ) 397elif name .endswith (".dt_bias" ): 398name = name .rpartition (".dt_bias" )[0 ]+ ".dt_proj.bias" 399elif "conv1d" in name : 400data_torch = data_torch .squeeze () 401elif name .endswith ("norm.weight" )and not name .endswith ("linear_attn.norm.weight" ): 402data_torch = data_torch + 1 403 404if "in_proj_qkvz.weight" in name : 405# original order: [q, k, v, z] * head_count 406# corrected order: [q * head_count, k * head_count, v * head_count, z * head_count] 407head_k_dim = self .hparams ["linear_key_head_dim" ] 408head_v_dim = self .hparams ["linear_value_head_dim" ] 409num_v_heads = self .hparams ["linear_num_value_heads" ] 410num_k_heads = self .hparams ["linear_num_key_heads" ] 411hidden_size = self .hparams ["hidden_size" ] 412split_arg_list_qkvz = [ 413head_k_dim ,# q partition 414head_k_dim ,# k partition 415 (num_v_heads // num_k_heads * head_v_dim ),# v partition 416 (num_v_heads // num_k_heads * head_v_dim ),# z partition 417 ] 418# view as (n_embd, head_count, [q+k+v+z]) 419data_torch = data_torch .permute (1 ,0 ).contiguous () 420data_torch = data_torch .view (- 1 ,num_k_heads ,sum (split_arg_list_qkvz )) 421# split into q, k, v, z 422q ,k ,v ,z = torch .split (data_torch ,split_arg_list_qkvz ,dim =- 1 ) 423# flatten dim + head_count 424q = q .contiguous ().view (hidden_size ,- 1 ) 425k = k .contiguous ().view (hidden_size ,- 1 ) 426v = v .contiguous ().view (hidden_size ,- 1 ) 427z = z .contiguous ().view (hidden_size ,- 1 ) 428# stack back 429qkv = torch .cat ([q ,k ,v ],dim =- 1 ).permute (1 ,0 ).contiguous () 430z = z .permute (1 ,0 ).contiguous () 431yield (self .format_tensor_name (gguf .MODEL_TENSOR .ATTN_QKV ,bid ,".weight" ),qkv ) 432yield (self .format_tensor_name (gguf .MODEL_TENSOR .ATTN_GATE ,bid ,".weight" ),z ) 433else : 434yield from super ().modify_tensors (data_torch ,name ,bid ) 435 436 437@ ModelBase . register ( "RND1" ) 438@ ModelBase . example ( "radicalnumerics/RND1-Base-0910" ) 439class RND1Model (Qwen2MoeModel ): 440model_arch = gguf .MODEL_ARCH .RND1 441 442def set_gguf_parameters (self ): 443super ().set_gguf_parameters () 444 445# RND1 specific parameters 446# RND1 uses bidirectional attention 447self .gguf_writer .add_causal_attention (False ) 448 449if (mask_token_id := self .hparams .get ("mask_token_id" ))is not None : 450self .gguf_writer .add_mask_token_id (mask_token_id ) 451 452 453class _LinearAttentionVReorderBase (Qwen3NextModel ): 454model_arch = gguf .MODEL_ARCH .QWEN3NEXT # overridden by subclasses 455"""reorders V heads from grouped to tiled order for ggml broadcast 456 457see https://github.com/ggml-org/llama.cpp/pull/19468#discussion_r2786394306 458 459Linear attention may has num_k_heads < num_v_heads. The HF weights store 460V heads grouped by K head: [G0_v0..v{r-1}, G1_v0..v{r-1}, ...]. 461ggml binary ops use tiled broadcast: [K0, K1, ..., K0, K1, ...]. 462We reorder V heads to tiled order so ggml_repeat can replace the expensive 463interleaved repeat: [G0_v0, G1_v0, ..., G0_v1, G1_v1, ...]. 464""" 465 466@staticmethod 467def _reorder_v_heads (tensor :Tensor ,dim :int ,num_k_heads :int ,num_v_per_k :int ,head_dim :int )-> Tensor : 468"""Reorder V heads from grouped (by K head) to tiled order along the given dimension.""" 469shape = list (tensor .shape ) 470if dim < 0 : 471dim += len (shape ) 472new_shape = shape [:dim ]+ [num_k_heads ,num_v_per_k ,head_dim ]+ shape [dim + 1 :] 473tensor = tensor .reshape (* new_shape ) 474perm = list (range (len (new_shape ))) 475perm [dim ],perm [dim + 1 ]= perm [dim + 1 ],perm [dim ] 476return tensor .permute (* perm ).contiguous ().reshape (* shape ) 477 478def _transform_nvfp4_weight (self ,name :str ,weight :Tensor ,scale :Tensor )-> tuple [Tensor ,Tensor ]: 479if not name .endswith (( 480".linear_attn.in_proj_qkv.weight" , 481".linear_attn.in_proj_z.weight" , 482".linear_attn.in_proj_a.weight" , 483".linear_attn.in_proj_b.weight" , 484".linear_attn.out_proj.weight" , 485 )): 486return weight ,scale 487 488num_k_heads = self .hparams ["linear_num_key_heads" ] 489num_v_heads = self .hparams ["linear_num_value_heads" ] 490head_k_dim = self .hparams ["linear_key_head_dim" ] 491head_v_dim = self .hparams ["linear_value_head_dim" ] 492num_v_per_k = num_v_heads // num_k_heads 493 494def unpack_nibbles (qs :Tensor )-> Tensor : 495lo = torch .bitwise_and (qs ,0x0F ) 496hi = torch .bitwise_right_shift (qs ,4 ) 497return torch .stack ((lo ,hi ),dim =- 1 ).reshape (* qs .shape [:- 1 ],qs .shape [- 1 ]* 2 ) 498 499def pack_nibbles (codes :Tensor )-> Tensor : 500codes = codes .reshape (* codes .shape [:- 1 ],codes .shape [- 1 ]// 2 ,2 ) 501lo = torch .bitwise_and (codes [...,0 ],0x0F ) 502hi = torch .bitwise_left_shift (torch .bitwise_and (codes [...,1 ],0x0F ),4 ) 503return torch .bitwise_or (lo ,hi ).contiguous () 504 505def apply_col_perm (qs :Tensor ,scales :Tensor ,col_perm :Tensor )-> tuple [Tensor ,Tensor ]: 506assert qs .ndim >= 2 507assert scales .ndim >= 2 508 509k = qs .shape [- 1 ]* 2 510assert col_perm .numel ()== k 511assert k % 16 == 0 512 513group_cols = col_perm .reshape (- 1 ,16 ) 514group_starts = group_cols [:,0 ] 515expected = group_starts .unsqueeze (1 )+ torch .arange (16 ,dtype = col_perm .dtype ) 516assert torch .equal (group_cols ,expected ) 517assert torch .all (group_starts % 16 == 0 ) 518 519group_perm = (group_starts // 16 ).to (dtype = torch .long ) 520expected_groups = torch .arange (scales .shape [- 1 ],dtype = torch .long ) 521assert group_perm .numel ()== scales .shape [- 1 ] 522assert torch .equal (torch .sort (group_perm ).values ,expected_groups ) 523 524codes = unpack_nibbles (qs ) 525codes = codes .index_select (- 1 ,col_perm .to (device = qs .device ,dtype = torch .long )) 526qs = pack_nibbles (codes ) 527scales = scales .index_select (- 1 ,group_perm .to (device = scales .device )) 528return qs ,scales 529 530def reorder_rows (qs :Tensor ,scales :Tensor ,head_dim :int )-> tuple [Tensor ,Tensor ]: 531row_perm = self ._reorder_v_heads ( 532torch .arange (num_v_heads * head_dim ,dtype = torch .long ).unsqueeze (- 1 ), 5330 ,num_k_heads ,num_v_per_k ,head_dim , 534 ).squeeze (- 1 ) 535return ( 536qs .index_select (0 ,row_perm .to (device = qs .device )), 537scales .index_select (0 ,row_perm .to (device = scales .device )), 538 ) 539 540if name .endswith (".linear_attn.in_proj_qkv.weight" ): 541q_dim = head_k_dim * num_k_heads 542k_dim = head_k_dim * num_k_heads 543q = weight [:q_dim ] 544k = weight [q_dim :q_dim + k_dim ] 545v = weight [q_dim + k_dim :] 546q_scale = scale [:q_dim ] 547k_scale = scale [q_dim :q_dim + k_dim ] 548v_scale = scale [q_dim + k_dim :] 549v ,v_scale = reorder_rows (v ,v_scale ,head_v_dim ) 550return torch .cat ([q ,k ,v ],dim = 0 ),torch .cat ([q_scale ,k_scale ,v_scale ],dim = 0 ) 551 552if name .endswith (".linear_attn.in_proj_z.weight" ): 553weight ,scale = reorder_rows (weight ,scale ,head_v_dim ) 554elif name .endswith ((".linear_attn.in_proj_a.weight" ,".linear_attn.in_proj_b.weight" )): 555weight ,scale = reorder_rows (weight ,scale ,1 ) 556elif name .endswith (".linear_attn.out_proj.weight" ): 557col_perm = self ._reorder_v_heads ( 558torch .arange (num_v_heads * head_v_dim ,dtype = torch .long ).unsqueeze (0 ), 5591 ,num_k_heads ,num_v_per_k ,head_v_dim , 560 ).squeeze (0 ) 561weight ,scale = apply_col_perm (weight ,scale ,col_perm ) 562 563return weight ,scale 564 565def _repack_nvfp4 (self ,name :str ,weight :Tensor ,scale :Tensor ,scale2 :Tensor ,input_scale :Tensor ): 566weight ,scale = self ._transform_nvfp4_weight (name ,weight ,scale ) 567super ()._repack_nvfp4 (name ,weight ,scale ,scale2 ,input_scale ) 568 569def modify_tensors (self ,data_torch :Tensor ,name :str ,bid :int | None )-> Iterable [tuple [str ,Tensor ]]: 570num_k_heads = self .hparams .get ("linear_num_key_heads" ,0 ) 571num_v_heads = self .hparams .get ("linear_num_value_heads" ,0 ) 572 573if num_k_heads > 0 and num_v_heads > 0 and num_k_heads != num_v_heads and "linear_attn." in name : 574head_k_dim = self .hparams ["linear_key_head_dim" ] 575head_v_dim = self .hparams ["linear_value_head_dim" ] 576num_v_per_k = num_v_heads // num_k_heads 577 578if ".in_proj_qkv." in name : 579# QKV weight: reorder only the V rows 580q_dim = head_k_dim * num_k_heads 581k_dim = head_k_dim * num_k_heads 582q = data_torch [:q_dim ] 583k = data_torch [q_dim :q_dim + k_dim ] 584v = data_torch [q_dim + k_dim :] 585v = self ._reorder_v_heads (v ,0 ,num_k_heads ,num_v_per_k ,head_v_dim ) 586data_torch = torch .cat ([q ,k ,v ],dim = 0 ) 587 588elif ".in_proj_z." in name : 589# Z gate weight: reorder rows (num_v_heads * head_v_dim) 590data_torch = self ._reorder_v_heads (data_torch ,0 ,num_k_heads ,num_v_per_k ,head_v_dim ) 591 592elif ".in_proj_b." in name or ".in_proj_a." in name : 593# Beta/Alpha weight: reorder rows (num_v_heads, head_dim=1) 594data_torch = self ._reorder_v_heads (data_torch ,0 ,num_k_heads ,num_v_per_k ,1 ) 595 596elif ".A_log" in name or ".dt_bias" in name or ".dt_proj" in name : 597# A_log / dt_bias: 1D parameters with num_v_heads elements 598if data_torch .ndim == 1 : 599data_torch = self ._reorder_v_heads ( 600data_torch .unsqueeze (- 1 ),0 ,num_k_heads ,num_v_per_k ,1 601 ).squeeze (- 1 ) 602else : 603data_torch = self ._reorder_v_heads (data_torch ,- 1 ,num_k_heads ,num_v_per_k ,1 ) 604 605elif ".conv1d" in name : 606# Conv1d kernel: reorder only the V channel portion 607data = data_torch .squeeze () 608qk_channels = head_k_dim * num_k_heads * 2 609qk_part = data [:qk_channels ] 610v_part = data [qk_channels :] 611v_part = self ._reorder_v_heads (v_part ,0 ,num_k_heads ,num_v_per_k ,head_v_dim ) 612data_torch = torch .cat ([qk_part ,v_part ],dim = 0 ) 613 614elif ".out_proj." in name : 615# Out projection weight: reorder columns (input dimension) 616data_torch = self ._reorder_v_heads (data_torch ,1 ,num_k_heads ,num_v_per_k ,head_v_dim ) 617 618yield from super ().modify_tensors (data_torch ,name ,bid ) 619 620 621class _Qwen35MRopeMixin : 622# Qwen3.5 always applies interleaved MRoPE (see Qwen3_5RotaryEmbedding in transformers); 623# the upstream default mrope_section is [11, 11, 10] and llama.cpp's QWEN35 / QWEN35MOE 624# loaders treat qwen35.rope.dimension_sections as required, so make sure it is always 625# written even when a particular checkpoint omits the field in `rope_parameters`. 626_QWEN35_DEFAULT_MROPE_SECTION = [11 ,11 ,10 ,0 ] 627 628gguf_writer :gguf .GGUFWriter 629rope_parameters :dict 630 631def set_gguf_parameters (self ): 632super ().set_gguf_parameters ()# ty: ignore[unresolved-attribute] 633if "mrope_section" not in self .rope_parameters : 634self .gguf_writer .add_rope_dimension_sections (self ._QWEN35_DEFAULT_MROPE_SECTION ) 635 636 637@ ModelBase . register ( "Qwen3_5ForConditionalGeneration" , "Qwen3_5ForCausalLM" ) 638@ ModelBase . example ( "Qwen/Qwen3.5-9B" ) 639class Qwen3_5TextModel (_Qwen35MRopeMixin ,_LinearAttentionVReorderBase ): 640model_arch = gguf .MODEL_ARCH .QWEN35 641 642 643@ ModelBase . register ( "Qwen3_5MoeForConditionalGeneration" , "Qwen3_5MoeForCausalLM" ) 644@ ModelBase . example ( "Qwen/Qwen3.5-35B-A3B" ) 645class Qwen3_5MoeTextModel (_Qwen35MRopeMixin ,_LinearAttentionVReorderBase ): 646model_arch = gguf .MODEL_ARCH .QWEN35MOE 647 648 649@ ModelBase . register ( "DFlashDraftModel" , "DFlash2DraftModel" ) 650@ ModelBase . example ( "z-lab/Qwen3.5-9B-DFlash" ) 651class DFlashModel (Qwen3Model ): 652model_arch = gguf .MODEL_ARCH .DFLASH 653 654def set_vocab (self ): 655if self .target_model_dir is None : 656raise ValueError ( 657"DFlash draft model requires --target-model-dir to be specified. " 658"Please provide the path to the target model directory containing the tokenizer." 659 ) 660logger .info (f"DFlash: Using tokenizer from target model: { self . target_model_dir } " ) 661original_dir = self .dir_model 662self .dir_model = self .target_model_dir 663 664# Reuse the target model's own vocab handler (e.g. Gemma-4 needs its 665# own tokenizer logic, not the Qwen default). 666from .import get_model_class 667with open (self .target_model_dir / "config.json" ,"r" ,encoding = "utf-8" )as f : 668target_hparams = json .load (f ) 669target_arch = target_hparams ["architectures" ][0 ] 670target_cls = get_model_class (target_arch ) 671 672if target_cls is not type (self ): 673if target_arch == "NemotronHForCausalLM" : 674setattr (self ,"is_moe" ,"num_experts_per_tok" in target_hparams ) 675target_cls .set_vocab (self )# ty: ignore[unresolved-attribute] 676else : 677super ().set_vocab () 678 679self .dir_model = original_dir 680 681mask_token_id = self .hparams .get ("dflash_config" , {}).get ("mask_token_id" ) 682if mask_token_id is not None : 683self .gguf_writer .add_mask_token_id (mask_token_id ) 684 685def set_gguf_parameters (self ): 686super ().set_gguf_parameters () 687 688dflash_config = self .hparams .get ("dflash_config" , {}) 689block_size = dflash_config .get ("block_size" ,self .hparams .get ("block_size" ,16 )) 690self .gguf_writer .add_block_size (block_size ) 691 692if "conv_kernel_size" in dflash_config : 693self .gguf_writer .add_conv_kernel_size (int (dflash_config ["conv_kernel_size" ])) 694self .gguf_writer .add_conv_group_size (int (dflash_config ["conv_group_size" ])) 695self .gguf_writer .add_selector_rank (int (dflash_config ["selector_rank" ])) 696self .gguf_writer .add_selector_top_k (int (dflash_config ["selector_top_k" ])) 697 698output_multiplier = dflash_config .get ( 699"output_multiplier" ,self .hparams .get ("output_multiplier" ) 700 ) 701if output_multiplier is not None : 702self .gguf_writer .add_logit_scale (float (output_multiplier )) 703softcap = dflash_config .get ( 704"final_logit_softcapping" ,self .hparams .get ("final_logit_softcapping" ) 705 ) 706if softcap is not None and float (softcap )> 0 : 707self .gguf_writer .add_final_logit_softcapping (float (softcap )) 708embedding_scale = dflash_config .get ( 709"input_embedding_scale" ,self .hparams .get ("input_embedding_scale" ) 710 ) 711if embedding_scale is not None : 712self .gguf_writer .add_embedding_scale (float (embedding_scale )) 713 714target_layer_ids = dflash_config .get ("target_layer_ids" , []) 715if target_layer_ids : 716extract_layer_ids = [i + 1 for i in target_layer_ids ] 717self .gguf_writer .add_target_layers (extract_layer_ids ) 718 719use_sliding_window = self .hparams .get ("use_sliding_window" ,False )or dflash_config .get ("use_swa" ,False ) 720sliding_window = dflash_config .get ("swa_window_size" )or self .hparams .get ("sliding_window" ) 721layer_types = self .hparams .get ("layer_types" ) 722if use_sliding_window and sliding_window and layer_types : 723is_swa = [lt == "sliding_attention" for lt in layer_types ] 724self .gguf_writer .add_sliding_window (sliding_window ) 725self .gguf_writer .add_sliding_window_pattern (is_swa ) 726 727causal = self .hparams .get ("is_causal" ) 728if causal is None : 729causal = dflash_config .get ("causal" ) 730if causal is not None : 731self .gguf_writer .add_causal_attention (bool (causal )) 732 733# M-RoPE target: the draft ropes on the temporal dim only, so write 734# degenerate sections [n_rot/2, 0, 0, 0] 735if self ._target_uses_mrope (): 736head_dim = self .hparams .get ("head_dim" )or self .hparams ["hidden_size" ]// self .hparams ["num_attention_heads" ] 737self .gguf_writer .add_rope_dimension_sections ([head_dim // 2 ,0 ,0 ,0 ]) 738 739def _target_uses_mrope (self )-> bool : 740if self .target_model_dir is None : 741return False 742with open (self .target_model_dir / "config.json" ,"r" ,encoding = "utf-8" )as f : 743cfg = json .load (f ) 744cfg = cfg .get ("text_config" ,cfg ) 745rope = cfg .get ("rope_parameters" )or cfg .get ("rope_scaling" )or {} 746return "mrope_section" in rope 747 748@classmethod 749def filter_tensors (cls ,item :tuple [str ,Callable [[],Tensor ]])-> tuple [str ,Callable [[],Tensor ]]| None : 750name ,gen = item 751if not name .startswith ("model." ): 752name = "model." + name 753if "sink" in name and not name .endswith (".weight" ): 754name += ".weight" 755return super ().filter_tensors ((name ,gen )) 756 757_ROPE_PERMUTE_SUFFIXES = ( 758"self_attn.q_proj.weight" , 759"self_attn.k_proj.weight" , 760"self_attn.q_norm.weight" , 761"self_attn.k_norm.weight" , 762 ) 763 764def modify_tensors (self ,data_torch :Tensor ,name :str ,bid :int | None )-> Iterable [tuple [str ,Tensor ]]: 765if name == "model.embed_tokens.weight" and not self .hparams .get ("has_embed_tokens" ,True ): 766return 767 768# interleaved-rope checkpoints (rope_is_neox_style = false) -> NeoX layout: per head, even dims first then odd 769if not self .hparams .get ("rope_is_neox_style" ,True )and name .endswith (self ._ROPE_PERMUTE_SUFFIXES ): 770head_dim = self .hparams ["head_dim" ] 771shape = data_torch .shape 772data_torch = data_torch .reshape (- 1 ,head_dim // 2 ,2 ,* shape [1 :]).transpose (1 ,2 ).reshape (shape ) 773 774if name in ( 775"model.candidate_selector.predecessor_codebook" , 776"model.candidate_selector.successor_codebook" , 777 ): 778name += ".weight" 779 780yield from super ().modify_tensors (data_torch ,name ,bid ) 781 782 783@ ModelBase . register ( 784"Qwen3DSparkModel" , 785"DSparkDraftModel" , 786"DSparkSpeculator" , 787"Lfm2DSparkDraftModel" , 788"LingDSparkModel" , 789) 790@ ModelBase . example ( "satgeze/Qwen3.6-27B-DSpark" ) 791class DSparkModel (DFlashModel ): 792# DSpark = DFlash + a semi-autoregressive Markov head. 793model_arch = gguf .MODEL_ARCH .DFLASH 794 795def __init__ (self ,dir_model ,* args ,** kwargs ): 796hparams = kwargs .pop ("hparams" ,None ) 797if hparams is None : 798hparams = ModelBase .load_hparams (dir_model ,False ) 799 800# EAGLE3-style exports use the 1+N bonus-anchor block, DFlash-lineage exports sample from the anchor 801self ._sample_from_anchor = hparams .get ( 802"sample_from_anchor" , 803"transformer_layer_config" not in hparams and "aux_hidden_state_layer_ids" not in hparams ) 804if "transformer_layer_config" in hparams : 805hparams = {** hparams ,** hparams ["transformer_layer_config" ]} 806 807super ().__init__ (dir_model ,* args ,hparams = hparams ,** kwargs ) 808 809# normalize both schemas to DFlash's nested dflash_config 810if "aux_hidden_state_layer_ids" in self .hparams : 811self .hparams .setdefault ("dflash_config" , { 812"mask_token_id" :self .hparams .get ("mask_token_id" ), 813"target_layer_ids" : [i - 1 for i in self .hparams ["aux_hidden_state_layer_ids" ]], 814 }) 815else : 816self .hparams .setdefault ("dflash_config" , { 817k :self .hparams [k ]for k in ("target_layer_ids" ,"mask_token_id" )if k in self .hparams 818 }) 819 820if (markov_head_type := self .hparams .get ("markov_head_type" ,"vanilla" ))!= "vanilla" : 821raise ValueError (f"unsupported markov_head_type { markov_head_type !r } (only 'vanilla' is supported)" ) 822 823n_vocab = self .hparams ["vocab_size" ] 824self ._n_vocab_draft = self .hparams .get ("draft_vocab_size" )or n_vocab 825if self ._n_vocab_draft > n_vocab : 826raise ValueError (f"draft_vocab_size { self . _n_vocab_draft } exceeds vocab_size { n_vocab } " ) 827self ._d2t :Tensor | None = None 828 829def set_gguf_parameters (self ): 830super ().set_gguf_parameters () 831self .gguf_writer .add_sample_from_anchor (self ._sample_from_anchor ) 832 833# confidence head is optional: vanilla-markov exports ship without it 834has_conf = any ("confidence_head.proj" in name for name in self .model_tensors ) 835self .gguf_writer .add_has_confidence_head (has_conf ) 836 837@classmethod 838def filter_tensors (cls ,item :tuple [str ,Callable [[],Tensor ]])-> tuple [str ,Callable [[],Tensor ]]| None : 839if item [0 ]== "t2d" :# not used at runtime 840return None 841return super ().filter_tensors (item ) 842 843_ROPE_PERMUTE_SUFFIXES = ( 844"self_attn.q_proj.weight" , 845"self_attn.k_proj.weight" , 846"self_attn.q_norm.weight" , 847"self_attn.k_norm.weight" , 848 ) 849 850def modify_tensors (self ,data_torch :Tensor ,name :str ,bid :int | None )-> Iterable [tuple [str ,Tensor ]]: 851if name == "model.d2t" : 852self ._d2t = data_torch 853return 854 855if self ._n_vocab_draft == self .hparams ["vocab_size" ]and name .endswith ("lm_head.weight" ): 856return 857 858# interleaved-rope checkpoints (rope_is_neox_style = false) -> NeoX layout: per head, even dims first then odd 859if not self .hparams .get ("rope_is_neox_style" ,True )and name .endswith (self ._ROPE_PERMUTE_SUFFIXES ): 860head_dim = self .hparams ["head_dim" ] 861shape = data_torch .shape 862data_torch = data_torch .reshape (- 1 ,head_dim // 2 ,2 ,* shape [1 :]).transpose (1 ,2 ).reshape (shape ) 863 864yield from super ().modify_tensors (data_torch ,name ,bid ) 865 866def prepare_tensors (self ): 867super ().prepare_tensors () 868 869n_vocab = self .hparams ["vocab_size" ] 870if self ._n_vocab_draft < n_vocab and self ._d2t is None : 871raise ValueError (f"draft_vocab_size { self . _n_vocab_draft } < vocab_size { n_vocab } but no d2t table found" ) 872 873# write d2t as absolute target token ids 874if self ._d2t is not None : 875data = LazyTorchTensor .to_eager (self ._d2t ).to (torch .int64 ).cpu ().numpy ().reshape (- 1 ) 876if data .size != self ._n_vocab_draft : 877raise ValueError (f"d2t size { data . size } does not match draft_vocab_size { self . _n_vocab_draft } " ) 878data = data + np .arange (data .size ,dtype = np .int64 ) 879if np .any ((data < 0 )| (data >= n_vocab )): 880raise ValueError (f"d2t target ids out of range for target vocab size { n_vocab } " ) 881if np .unique (data ).size != data .size : 882raise ValueError ("d2t contains duplicate target ids" ) 883logger .info (f" { 'd2t,' :<30 } --> I64, shape = {{ { data . size } }}" ) 884self .gguf_writer .add_tensor ("d2t" ,data ,raw_dtype = gguf .GGMLQuantizationType .I64 )