char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
7221e24f5
char/aliceai-t5-moe
1from __future__import annotations 2 3import re 4from typing import Any ,Callable ,Iterable ,TYPE_CHECKING 5 6import torch 7 8if TYPE_CHECKING : 9from torch import Tensor 10 11from .base import MmprojModel ,ModelBase ,gguf ,logger 12 13from .llama import LlamaModel 14from .mamba import Mamba2Model 15 16 17@ ModelBase . register ( "GraniteForCausalLM" ) 18@ ModelBase . example ( "ibm-granite/granite-3.3-2b-instruct" ) 19class GraniteModel (LlamaModel ): 20"""Conversion for IBM's GraniteForCausalLM""" 21model_arch = gguf .MODEL_ARCH .GRANITE 22 23def set_gguf_parameters (self ): 24"""Granite uses standard llama parameters with the following differences: 25 26- No head_dim support 27- New multiplier params: 28- attention_scale 29- embedding_scale 30- residual_scale 31- logits_scaling 32""" 33if head_dim := self .hparams .pop ("head_dim" ,None ): 34logger .warning ("Ignoring head_dim (%s) from config for Granite" ,head_dim ) 35super ().set_gguf_parameters () 36# NOTE: Convert _multiplier params to _scale params for naming 37# consistency 38if attention_scale := self .hparams .get ("attention_multiplier" ): 39self .gguf_writer .add_attention_scale (attention_scale ) 40logger .info ("gguf: (granite) attention_scale = %s" ,attention_scale ) 41if embedding_scale := self .hparams .get ("embedding_multiplier" ): 42self .gguf_writer .add_embedding_scale (embedding_scale ) 43logger .info ("gguf: (granite) embedding_scale = %s" ,embedding_scale ) 44if residual_scale := self .hparams .get ("residual_multiplier" ): 45self .gguf_writer .add_residual_scale (residual_scale ) 46logger .info ("gguf: (granite) residual_scale = %s" ,residual_scale ) 47if logits_scale := self .hparams .get ("logits_scaling" ): 48self .gguf_writer .add_logit_scale (logits_scale ) 49logger .info ("gguf: (granite) logits_scale = %s" ,logits_scale ) 50 51# If being used as the base for Granite4 Vision, add deepstack_layer_arr 52if self .hparams .get ("spatial_target_layers" )or self .hparams .get ("deepstack_layer_map" ): 53normalized_projector_map = Granite4VisionMmprojModel .get_normalized_projector_map (self .hparams ) 54deepstack_mapping_arr = [- 1 for _ in range (self .block_count )]# Populate with -1 sentinels 55for proj_idx , (_ ,llm_layer ,_ ,_ )in enumerate (normalized_projector_map ): 56# Skip the first projector which is handled as the base embedding 57# stream like normal 58if proj_idx == 0 : 59continue 60deepstack_mapping_arr [llm_layer ]= proj_idx 61self .gguf_writer .add_deepstack_mapping (deepstack_mapping_arr ) 62 63@classmethod 64def filter_tensors (cls ,item :tuple [str ,Callable [[],Tensor ]])-> tuple [str ,Callable [[],Tensor ]]| None : 65name ,gen = item 66# Skip multimodal tensors 67if ( 68name .startswith (("encoder." )) 69or "image_" in name 70or "layerwise_projectors" in name 71or "spatial_projectors" in name 72 ): 73return 74return super ().filter_tensors (item ) 75 76 77@ ModelBase . register ( "GraniteSWAForCausalLM" ) 78class GraniteSWAModel (GraniteModel ): 79"""Conversion for IBM's GraniteSWAForCausalLM (interleaved sliding window attention)""" 80model_arch = gguf .MODEL_ARCH .GRANITE_SWA 81 82@classmethod 83def filter_tensors (cls ,item :tuple [str ,Callable [[],Tensor ]])-> tuple [str ,Callable [[],Tensor ]]| None : 84name ,gen = item 85 86if name .endswith ("sinks" ): 87name += ".weight" 88 89return super ().filter_tensors ((name ,gen )) 90 91def set_gguf_parameters (self ): 92"""GraniteSWA uses Granite parameters plus sliding window configuration.""" 93super ().set_gguf_parameters () 94 95# Add sliding_window from config 96sliding_window = self .hparams .get ("sliding_window" ,128 ) 97self .gguf_writer .add_sliding_window (sliding_window ) 98logger .info ("gguf: (granite_swa) sliding_window = %s" ,sliding_window ) 99 100# Derive sliding_window_pattern from layer_types 101if layer_types := self .hparams .get ("layer_types" ): 102is_swa = [t == "sliding_attention" for t in layer_types ] 103self .gguf_writer .add_sliding_window_pattern (is_swa ) 104logger .info ("gguf: (granite_swa) sliding_window_pattern = %d SWA layers / %d total" , 105sum (is_swa ),len (is_swa )) 106else : 107# Fall back to period-based pattern: i % 4 != 0 108# This matches the transformers default pattern 109n_layers = self .block_count 110is_swa = [i % 4 != 0 for i in range (n_layers )] 111self .gguf_writer .add_sliding_window_pattern (is_swa ) 112logger .info ("gguf: (granite_swa) sliding_window_pattern (inferred) = %d SWA layers / %d total" , 113sum (is_swa ),n_layers ) 114 115# Add rope_pattern from no_rope_layers 116if no_rope_layers := self .hparams .get ("no_rope_layers" ): 117# Convert 1/0 to bool (1 = use RoPE, 0 = NoPE) 118rope_pattern = [bool (x )for x in no_rope_layers ] 119self .gguf_writer .add_rope_pattern (rope_pattern ) 120logger .info ("gguf: (granite_swa) rope_pattern = %d RoPE layers / %d total" , 121sum (rope_pattern ),len (rope_pattern )) 122 123 124@ ModelBase . register ( "GraniteMoeSWAForCausalLM" ) 125class GraniteMoeSWAModel (GraniteSWAModel ): 126"""Conversion for IBM's GraniteMoeSWAForCausalLM (unified dense + MoE with iSWA)""" 127model_arch = gguf .MODEL_ARCH .GRANITE_SWA 128 129def set_gguf_parameters (self ): 130super ().set_gguf_parameters () 131if shared_intermediate_size := self .hparams .get ("shared_intermediate_size" ): 132self .gguf_writer .add_expert_shared_feed_forward_length (shared_intermediate_size ) 133logger .info ("gguf: (granitemoewa) shared_intermediate_size = %s" ,shared_intermediate_size ) 134 135def modify_tensors (self ,data_torch :Tensor ,name :str ,bid :int | None )-> Iterable [tuple [str ,Tensor ]]: 136"""Split merged MoE tensors (gate+up) following standard MoE pattern.""" 137 138# Handle expert FFN tensors (merged gate+up) - swash format: experts.gate_up_proj 139# Kept fused since inference (build_moe_ffn) supports a single gate_up_exps 140# tensor for the routed experts. 141if name .endswith ("block_sparse_moe.experts.gate_up_proj" ): 142ffn_dim = self .hparams ["intermediate_size" ] 143assert data_torch .shape [- 2 ]== 2 * ffn_dim ,f"Merged FFN tensor size must be 2 * intermediate_size, got { data_torch . shape [ - 2 ] } " 144yield from ModelBase .modify_tensors (self ,data_torch ,self .format_tensor_name (gguf .MODEL_TENSOR .FFN_GATE_UP_EXP ,bid ),bid ) 145return 146 147# Handle expert FFN down projection - swash format: experts.down_proj 148if name .endswith ("block_sparse_moe.experts.down_proj" ): 149yield from ModelBase .modify_tensors (self ,data_torch ,self .format_tensor_name (gguf .MODEL_TENSOR .FFN_DOWN_EXP ,bid ),bid ) 150return 151 152# Handle expert FFN tensors (merged gate+up) - standard granite format: input_linear.weight 153# Kept fused since inference (build_moe_ffn) supports a single gate_up_exps 154# tensor for the routed experts. 155if name .endswith ("block_sparse_moe.input_linear.weight" ): 156ffn_dim = self .hparams ["intermediate_size" ] 157assert data_torch .shape [- 2 ]== 2 * ffn_dim ,"Merged FFN tensor size must be 2 * intermediate_size" 158yield from ModelBase .modify_tensors (self ,data_torch ,self .format_tensor_name (gguf .MODEL_TENSOR .FFN_GATE_UP_EXP ,bid ),bid ) 159return 160 161# Handle shared expert FFN tensors (if present) - kept fused since 162# inference (build_ffn) supports a single ffn_up_shexp tensor with 163# LLM_FFN_SWIGLU for the shared expert. 164if name .endswith ("shared_mlp.input_linear.weight" ): 165ffn_dim = self .hparams .get ("shared_intermediate_size" ,self .hparams ["intermediate_size" ]) 166assert data_torch .shape [- 2 ]== 2 * ffn_dim ,"Merged FFN tensor size must be 2 * shared_intermediate_size" 167yield from ModelBase .modify_tensors (self ,data_torch ,self .format_tensor_name (gguf .MODEL_TENSOR .FFN_UP_SHEXP ,bid ),bid ) 168return 169 170# Handle shared expert output (if present) 171if name .endswith ("shared_mlp.output_linear.weight" ): 172yield from ModelBase .modify_tensors (self ,data_torch ,self .format_tensor_name (gguf .MODEL_TENSOR .FFN_DOWN_SHEXP ,bid ),bid ) 173return 174 175# Pass through to parent for all other tensors (including sinks) 176yield from super ().modify_tensors (data_torch ,name ,bid ) 177 178 179@ ModelBase . register ( "GraniteMoeForCausalLM" , "GraniteMoeSharedForCausalLM" ) 180@ ModelBase . example ( "ibm-granite/granite-3.1-3b-a800m-instruct" ) 181class GraniteMoeModel (GraniteModel ): 182"""Conversion for IBM's GraniteMoeForCausalLM""" 183model_arch = gguf .MODEL_ARCH .GRANITE_MOE 184 185def set_gguf_parameters (self ): 186"""GraniteMoeShared uses GraniteMoe parameters plus the following: 187- shared_intermediate_size 188""" 189super ().set_gguf_parameters () 190if shared_feed_forward_length := self .hparams .get ("shared_intermediate_size" ): 191self .gguf_writer .add_expert_shared_feed_forward_length (shared_feed_forward_length ) 192logger .info ("gguf: (granitemoeshared) shared_feed_forward_length = %s" ,shared_feed_forward_length ) 193 194def modify_tensors (self ,data_torch :Tensor ,name :str ,bid :int | None )-> Iterable [tuple [str ,Tensor ]]: 195"""In modeling_granitemoe, the JetMoe implementation of parallel experts 196is used. This essentially merges w1 and w3 into a single tensor with 2x 197the hidden size that is then split during forward. To keep compatibility 198with existing mixtral support, we pull them apart here. 199""" 200 201if name .endswith ("block_sparse_moe.input_linear.weight" ): 202ffn_dim = self .hparams ["intermediate_size" ] 203assert data_torch .shape [- 2 ]== 2 * ffn_dim ,"Merged FFN tensor size must be 2 * intermediate_size" 204gate ,up = data_torch .split (ffn_dim ,dim =- 2 ) 205yield from ModelBase .modify_tensors (self ,gate ,self .format_tensor_name (gguf .MODEL_TENSOR .FFN_GATE_EXP ,bid ),bid ) 206yield from ModelBase .modify_tensors (self ,up ,self .format_tensor_name (gguf .MODEL_TENSOR .FFN_UP_EXP ,bid ),bid ) 207return 208 209has_experts = bool (self .hparams .get ('num_local_experts' )) 210 211if name .endswith ("shared_mlp.input_linear.weight" ): 212ffn_dim = self .hparams ["shared_intermediate_size" ] 213assert data_torch .shape [- 2 ]== 2 * ffn_dim ,"Merged FFN tensor size must be 2 * shared_intermediate_size" 214gate ,up = data_torch .split (ffn_dim ,dim =- 2 ) 215if has_experts : 216yield from ModelBase .modify_tensors (self ,gate ,self .format_tensor_name (gguf .MODEL_TENSOR .FFN_GATE_SHEXP ,bid ),bid ) 217yield from ModelBase .modify_tensors (self ,up ,self .format_tensor_name (gguf .MODEL_TENSOR .FFN_UP_SHEXP ,bid ),bid ) 218return 219yield from ModelBase .modify_tensors (self ,gate ,self .format_tensor_name (gguf .MODEL_TENSOR .FFN_GATE ,bid ),bid ) 220yield from ModelBase .modify_tensors (self ,up ,self .format_tensor_name (gguf .MODEL_TENSOR .FFN_UP ,bid ),bid ) 221return 222 223if not has_experts and name .endswith ("shared_mlp.output_linear.weight" ): 224yield from ModelBase .modify_tensors (self ,data_torch ,self .format_tensor_name (gguf .MODEL_TENSOR .FFN_DOWN ,bid ),bid ) 225return 226 227yield from super ().modify_tensors (data_torch ,name ,bid ) 228 229 230@ ModelBase . register ( "GraniteSwitchForCausalLM" ) 231@ ModelBase . example ( "ibm-granite/granite-switch-4.1-3b-preview" ) 232class GraniteSwitchModel (GraniteMoeModel ): 233"""Dense, all-attention Granite with N per-token embedded LoRA adapters, stacked 234over the adapter dim with a zero adapter at slot 0 (N = num_adapters + 1).""" 235model_arch = gguf .MODEL_ARCH .GRANITE_SWITCH 236 237# permute q/k per-slice below (NORM-rope layout), not via the parent's auto-permute 238undo_permute = False 239 240def __init__ (self ,* args ,** kwargs ): 241super ().__init__ (* args ,** kwargs ) 242# the weightless switch reserves one cache slot: one fewer block than num_hidden_layers 243self .block_count = self .block_count - 1 244self .tensor_map = gguf .get_tensor_name_map (self .model_arch ,self .block_count ) 245 246self ._n_adapters = int (self .hparams ["num_adapters" ]) 247self ._max_lora_rank = int (self .hparams ["max_lora_rank" ]) 248self ._n_slots = self ._n_adapters + 1 # +1 for the zero slot at index 0 249 250n_head = int (self .hparams ["num_attention_heads" ]) 251n_kv_head = int (self .hparams ["num_key_value_heads" ]) 252head_dim = ( 253self .hparams .get ("projection_head_dim" ) 254or self .hparams .get ("head_dim" ) 255or (self .hparams ["hidden_size" ]// n_head ) 256 ) 257self ._n_head = n_head 258self ._n_kv_head = n_kv_head 259self ._head_dim = int (head_dim ) 260self ._q_size = n_head * self ._head_dim 261self ._kv_size = n_kv_head * self ._head_dim 262 263def set_gguf_parameters (self ): 264super ().set_gguf_parameters () 265 266# dense: pin expert_used_count to 0 (config carries a leftover num_experts_per_tok) 267if not self .hparams .get ("num_local_experts" ): 268self .gguf_writer .add_expert_used_count (0 ) 269 270self .gguf_writer .add_adapter_count (self ._n_adapters ) 271self .gguf_writer .add_adapter_lora_rank (self ._max_lora_rank ) 272self .gguf_writer .add_adapter_token_ids_activate (self .hparams ["adapter_token_ids" ]) 273self .gguf_writer .add_adapter_token_ids_substitute (self .hparams ["adapter_substitute_token_ids" ]) 274router_gain = float (self .hparams .get ("control_token_gain" ,15.0 )) 275self .gguf_writer .add_adapter_router_gain (router_gain ) 276logger .info ("gguf: (graniteswitch) num_adapters=%s max_lora_rank=%s n_slots=%s router_gain=%s" ,self ._n_adapters ,self ._max_lora_rank ,self ._n_slots ,router_gain ) 277 278def _lora_a (self ,data :Tensor )-> Tensor : 279# on-disk A: [n_adapters, 1, max_rank, in] -> [n_adapters+1, max_rank, in] 280a = data .squeeze (1 ) 281zero = torch .zeros_like (a [:1 ]) 282return torch .cat ([zero ,a ],dim = 0 ).contiguous () 283 284def _lora_b (self ,data :Tensor ,permute_n_head :int | None = None )-> Tensor : 285# on-disk B: [n_adapters, 1, out, max_rank] -> [n_adapters+1, out, max_rank] 286b = data .squeeze (1 ) 287if permute_n_head is not None : 288# permute each adapter's B output rows to match the permuted q/k base 289b = torch .stack ([self .permute (b [i ],permute_n_head ,permute_n_head )for i in range (b .shape [0 ])],dim = 0 ) 290zero = torch .zeros_like (b [:1 ]) 291return torch .cat ([zero ,b ],dim = 0 ).contiguous () 292 293def modify_tensors (self ,data_torch :Tensor ,name :str ,bid :int | None )-> Iterable [tuple [str ,Tensor ]]: 294T = gguf .MODEL_TENSOR 295 296# skip the weightless switch + control-token buffers (rebuilt at load time) 297bare = name .split ("." )[- 1 ] 298if ( 299name .startswith ("model.switch." )or name .startswith ("switch." ) 300or bare in ("adapter_token_ids" ,"control_to_substitute_lut" ) 301 ): 302return 303 304if "self_attn.qkv_proj" in name : 305if name .endswith ("base_layer.weight" ): 306# fused [q|k|v] rows: permute q/k row-blocks for ggml's NORM-rope layout 307q ,k ,v = data_torch .split ([self ._q_size ,self ._kv_size ,self ._kv_size ],dim = 0 ) 308q = self .permute (q ,self ._n_head ,self ._n_head ) 309k = self .permute (k ,self ._n_kv_head ,self ._n_kv_head ) 310fused = torch .cat ([q ,k ,v ],dim = 0 ) 311yield (self .format_tensor_name (T .ATTN_QKV ,bid ),fused ) 312return 313if "lora_A_slices." in name : 314slot = int (name .rsplit ("." ,1 )[1 ]) 315key = {0 :T .ATTN_Q ,1 :T .ATTN_K ,2 :T .ATTN_V }[slot ] 316yield (self .format_tensor_name (key ,bid ,suffix = ".lora_a" ),self ._lora_a (data_torch )) 317return 318if "lora_B_slices." in name : 319slot = int (name .rsplit ("." ,1 )[1 ]) 320key ,ph = { 3210 : (T .ATTN_Q ,self ._n_head ), 3221 : (T .ATTN_K ,self ._n_kv_head ), 3232 : (T .ATTN_V ,None ), 324 }[slot ] 325yield (self .format_tensor_name (key ,bid ,suffix = ".lora_b" ),self ._lora_b (data_torch ,ph )) 326return 327raise ValueError (f"Unexpected qkv_proj tensor: { name } " ) 328 329if "self_attn.o_proj" in name : 330if name .endswith ("base_layer.weight" ): 331yield (self .format_tensor_name (T .ATTN_OUT ,bid ),data_torch ) 332return 333if name .endswith ("lora_A" ): 334yield (self .format_tensor_name (T .ATTN_OUT ,bid ,suffix = ".lora_a" ),self ._lora_a (data_torch )) 335return 336if name .endswith ("lora_B" ): 337yield (self .format_tensor_name (T .ATTN_OUT ,bid ,suffix = ".lora_b" ),self ._lora_b (data_torch )) 338return 339raise ValueError (f"Unexpected o_proj tensor: { name } " ) 340 341if "shared_mlp.input_linear" in name : 342ffn = self .hparams ["shared_intermediate_size" ] 343if name .endswith ("base_layer.weight" ): 344gate ,up = data_torch .split ([ffn ,ffn ],dim = 0 ) 345yield (self .format_tensor_name (T .FFN_GATE ,bid ),gate ) 346yield (self .format_tensor_name (T .FFN_UP ,bid ),up ) 347return 348if "lora_A_slices." in name : 349slot = int (name .rsplit ("." ,1 )[1 ]) 350key = {0 :T .FFN_GATE ,1 :T .FFN_UP }[slot ] 351yield (self .format_tensor_name (key ,bid ,suffix = ".lora_a" ),self ._lora_a (data_torch )) 352return 353if "lora_B_slices." in name : 354slot = int (name .rsplit ("." ,1 )[1 ]) 355key = {0 :T .FFN_GATE ,1 :T .FFN_UP }[slot ] 356yield (self .format_tensor_name (key ,bid ,suffix = ".lora_b" ),self ._lora_b (data_torch )) 357return 358raise ValueError (f"Unexpected shared_mlp.input_linear tensor: { name } " ) 359 360if "shared_mlp.output_linear" in name : 361if name .endswith ("base_layer.weight" ): 362yield (self .format_tensor_name (T .FFN_DOWN ,bid ),data_torch ) 363return 364if name .endswith ("lora_A" ): 365yield (self .format_tensor_name (T .FFN_DOWN ,bid ,suffix = ".lora_a" ),self ._lora_a (data_torch )) 366return 367if name .endswith ("lora_B" ): 368yield (self .format_tensor_name (T .FFN_DOWN ,bid ,suffix = ".lora_b" ),self ._lora_b (data_torch )) 369return 370raise ValueError (f"Unexpected shared_mlp.output_linear tensor: { name } " ) 371 372if bid is not None and ".layers." in name and ( 373"input_layernorm" in name or "post_attention_layernorm" in name 374 ): 375key = T .ATTN_NORM if "input_layernorm" in name else T .FFN_NORM 376yield (self .format_tensor_name (key ,bid ),data_torch ) 377return 378 379if name in ("model.embed_tokens.weight" ,"embed_tokens.weight" ): 380yield (self .format_tensor_name (T .TOKEN_EMBD ),data_torch ) 381return 382if name in ("model.norm.weight" ,"norm.weight" ): 383yield (self .format_tensor_name (T .OUTPUT_NORM ),data_torch ) 384return 385if name == "lm_head.weight" : 386return # tied to token_embd 387 388raise ValueError (f"graniteswitch: unhandled tensor { name !r } (bid= { bid } )" ) 389 390 391@ ModelBase . register ( "GraniteMoeHybridForCausalLM" , "BambaForCausalLM" ) 392@ ModelBase . example ( "ibm-granite/granite-4.0-h-tiny" , "ibm-ai-platform/Bamba-9B-v2" ) 393class GraniteHybridModel (Mamba2Model ,GraniteMoeModel ): 394"""GraniteHybrid is a hybrid SSM + Attention model that uses Mamba2 SSM 395layers and optionally uses MoE w/ a shared expert""" 396model_arch = gguf .MODEL_ARCH .GRANITE_HYBRID 397undo_permute = True 398 399def __init__ (self ,* args ,** kwargs ): 400 401# Hybrid mamba models use a prefix for the mamba-specific params. 402# TODO: Extend this if the prefix(es) need to be configurable 403self .hparam_prefixes = ["mamba" ] 404 405super ().__init__ (* args ,** kwargs ) 406 407# Lists of which layers use ssm vs attention 408self ._attn_layers = self .get_attn_layers () 409self ._ssm_layers = [ 410i for i in range (self .block_count ) 411if i not in self ._attn_layers 412 ] 413 414# There are some models in this family that are non-hybrid, but keep the 415# same parent class by setting all layers to "attention." If this is the 416# case, the model architecture needs to be updated to a standard 417# "granite" or "granitemoe" model 418if not self ._ssm_layers : 419has_experts = self .find_hparam (["num_experts_per_tok" ,"num_experts_per_token" ],optional = True ) 420new_arch = ( 421gguf .MODEL_ARCH .GRANITE_MOE 422if has_experts else 423gguf .MODEL_ARCH .GRANITE 424 ) 425self .model_arch = new_arch 426self .gguf_writer .arch = gguf .MODEL_ARCH_NAMES [new_arch ] 427self .gguf_writer .add_architecture () 428 429# n_group and d_inner are used during reshape_tensors for mamba2 430# NOTE: Explicitly include hparam prefix prefix for d_model to 431# disambiguate with top-level head_dim 432# NOTE 2: If needed for future models, this can be isolated in a method 433# to separate the prefix setting and the keys used 434self .d_model = self .find_hparam ([f" { self . hparam_prefixes [ 0 ] } _head_dim" ,"hidden_size" ,"d_model" ]) 435self .n_group = self .find_hparam (["n_groups" ,"num_groups" ]) 436self .d_inner = self .find_hparam (["expand" ,"num_heads" ])* self .d_model 437 438def get_attn_layers (self ): 439# Explicit list of layer type names 440if layer_types := self .hparams .get ("layer_types" ): 441return [ 442i for i ,typ in enumerate (layer_types ) 443if typ == "attention" 444 ] 445 446# Layer types indicated by index or period 447attn_layers = self .hparams .get ("attn_layer_indices" , []) 448if not attn_layers : 449attn_period = self .hparams .get ("attn_layer_period" ) 450assert attn_period ,"Didn't find attn_layer_indices or attn_layer_period" 451attn_offset = self .hparams .get ("attn_layer_offset" ) 452assert attn_offset is not None ,"No attention layer offset set with attn_layer_period" 453attn_layers = [ 454i for i in range (self .block_count ) 455if i % attn_period == attn_offset 456 ] 457return attn_layers 458 459def find_hparam (self ,keys :Iterable [str ],* args ,** kwargs )-> Any : 460prefixed = [] 461for pfx in self .hparam_prefixes : 462prefixed .extend ( 463"_" .join ([pfx ,k ]) 464for k in keys 465 ) 466keys = list (keys )+ prefixed 467return Mamba2Model .find_hparam (self ,keys ,* args ,** kwargs ) 468 469def modify_tensors (self ,data_torch :Tensor ,name :str ,bid :int | None )-> Iterable [tuple [str ,Tensor ]]: 470if ( 471name .endswith ("block_sparse_moe.input_linear.weight" ) 472or "shared_mlp" in name 473 ): 474yield from GraniteMoeModel .modify_tensors (self ,data_torch ,name ,bid ) 475return 476 477# Determine whether this is a mamba layer or an attention layer 478if bid in self ._ssm_layers : 479yield from Mamba2Model .modify_tensors (self ,data_torch ,name ,bid ) 480return 481elif bid in self ._attn_layers : 482yield from GraniteMoeModel .modify_tensors (self ,data_torch ,name ,bid ) 483return 484yield from ModelBase .modify_tensors (self ,data_torch ,name ,bid ) 485 486def set_gguf_parameters (self ): 487"""This method merges params from both parents and some that are 488specific to this model. The result is some duplication of how the params 489get set. The following warnings are expected during conversion: 490 491WARNING:Duplicated key name 'granitehybrid.attention.head_count_kv' 492WARNING:Duplicated key name 'granitehybrid.context_length' 493""" 494GraniteMoeModel .set_gguf_parameters (self ) 495 496## Mamba mixer params ## 497self .gguf_writer .add_ssm_conv_kernel (self .find_hparam (["conv_kernel" ,"d_conv" ])) 498self .gguf_writer .add_ssm_state_size (self .find_hparam (["state_size" ,"d_state" ,"state_dim" ,"ssm_state_size" ])) 499self .gguf_writer .add_ssm_group_count (self .n_group ) 500self .gguf_writer .add_ssm_inner_size (self .d_inner ) 501# NOTE: The mamba_dt_rank is _not_ the right field for how this is used 502# in llama.cpp 503self .gguf_writer .add_ssm_time_step_rank (self .find_hparam (["n_heads" ,"num_heads" ])) 504 505## Attention params ## 506head_count_kv = self .find_hparam (["num_key_value_heads" ,"n_head_kv" ]) 507head_count_kv_vec = [ 508head_count_kv if i in self ._attn_layers else 0 for i in range (self .block_count ) 509 ] 510if rope_dim := self .hparams .get ("attn_rotary_emb" ): 511self .gguf_writer .add_rope_dimension_count (rope_dim ) 512self .gguf_writer .add_head_count_kv (head_count_kv_vec ) 513 514## If Bamba or non-hybrid, use rope, otherwise don't 515use_rope = ( 516"BambaForCausalLM" in self .hparams ["architectures" ] 517or not self ._ssm_layers 518 ) 519self .gguf_writer .add_rope_scaling_finetuned (use_rope ) 520if not use_rope : 521self .gguf_writer .add_context_length (2 ** 20 ) 522 523## Validation ## 524d_head = self .find_hparam (["d_head" ],optional = True )or 64 525assert self .hparams .get ("hidden_act" )in [None ,"silu" ],"Only SILU activation supported" 526assert self .d_inner % d_head == 0 ,f"SSM inner size { self . d_inner } not a multiple of head dim { d_head } " 527 528def set_vocab (self ): 529# For models with no ssm layers, don't pad for mamba2 530self .hparams ["pad_vocab_size_multiple" ]= 8 if self ._ssm_layers else 1 531Mamba2Model .set_vocab (self ) 532 533 534@ ModelBase . register ( "GraniteSpeechForConditionalGeneration" ) 535@ ModelBase . example ( "ibm-granite/granite-speech-3.3-2b" , "ibm-granite/granite-4.0-1b-speech" ) 536class GraniteSpeechMmprojModel (MmprojModel ): 537has_vision_encoder = False 538has_audio_encoder = True 539 540_batch_norm_tensors :list [dict [str ,Tensor ]]| None = None 541 542def get_audio_config (self )-> dict [str ,Any ]| None : 543return self .global_config .get ("encoder_config" ) 544 545def set_gguf_parameters (self ): 546assert self .hparams_audio is not None 547a = self .hparams_audio 548a ["hidden_size" ]= a ["hidden_dim" ] 549a ["intermediate_size" ]= a ["hidden_dim" ]* a ["feedforward_mult" ] 550a ["num_attention_heads" ]= a ["num_heads" ] 551a ["num_hidden_layers" ]= a ["num_layers" ] 552 553super ().set_gguf_parameters () 554 555self .gguf_writer .add_clip_projector_type (gguf .VisionProjectorType .GRANITE_SPEECH ) 556self .gguf_writer .add_audio_num_mel_bins (a ["input_dim" ]) 557self .gguf_writer .add_audio_attention_layernorm_eps (1e-5 ) 558self .gguf_writer .add_audio_chunk_size (a ["context_size" ]) 559self .gguf_writer .add_audio_conv_kernel_size (a ["conv_kernel_size" ]) 560self .gguf_writer .add_audio_max_pos_emb (a ["max_pos_emb" ]) 561 562p = self .global_config 563self .gguf_writer .add_audio_projector_window_size (p ["window_size" ]) 564self .gguf_writer .add_audio_projector_downsample_rate (p ["downsample_rate" ]) 565self .gguf_writer .add_audio_projector_head_count (p ["projector_config" ]["num_attention_heads" ]) 566 567def tensor_force_quant (self ,name ,new_name ,bid ,n_dims ): 568if "encoder" in name or "projector" in name : 569if ".conv" in name and ".weight" in name : 570return gguf .GGMLQuantizationType .F32 571return super ().tensor_force_quant (name ,new_name ,bid ,n_dims ) 572 573@classmethod 574def filter_tensors (cls ,item :tuple [str ,Callable [[],Tensor ]])-> tuple [str ,Callable [[],Tensor ]]| None : 575name ,gen = item 576if "attention_dists" in name or "num_batches_tracked" in name : 577return None 578return super ().filter_tensors (item ) 579 580def modify_tensors (self ,data_torch :Tensor ,name :str ,bid :int | None )-> Iterable [tuple [str ,Tensor ]]: 581# fold running_mean, running_var and eps into weight and bias for batch_norm 582if "batch_norm" in name and "encoder.layers." in name : 583if self ._batch_norm_tensors is None : 584self ._batch_norm_tensors = [{}for _ in range (self .block_count )] 585assert bid is not None 586self ._batch_norm_tensors [bid ][name ]= data_torch 587if len (self ._batch_norm_tensors [bid ])< 4 : 588return 589prefix = f"encoder.layers. { bid } .conv.batch_norm" 590weight = self ._batch_norm_tensors [bid ][f" { prefix } .weight" ] 591bias = self ._batch_norm_tensors [bid ][f" { prefix } .bias" ] 592running_mean = self ._batch_norm_tensors [bid ][f" { prefix } .running_mean" ] 593running_var = self ._batch_norm_tensors [bid ][f" { prefix } .running_var" ] 594eps = 1e-5 595a = weight / torch .sqrt (running_var + eps ) 596b = bias - running_mean * a 597yield from super ().modify_tensors (a ,f"encoder.layers. { bid } .conv.batch_norm.weight" ,bid ) 598yield from super ().modify_tensors (b ,f"encoder.layers. { bid } .conv.batch_norm.bias" ,bid ) 599return 600 601if ".attn.to_kv.weight" in name : 602k_weight ,v_weight = data_torch .chunk (2 ,dim = 0 ) 603yield from super ().modify_tensors (k_weight ,name .replace ("to_kv" ,"to_k" ),bid ) 604yield from super ().modify_tensors (v_weight ,name .replace ("to_kv" ,"to_v" ),bid ) 605return 606 607if ("up_conv" in name or "down_conv" in name )and name .endswith (".weight" ): 608if data_torch .ndim == 3 and data_torch .shape [2 ]== 1 : 609data_torch = data_torch .squeeze (2 ) 610 611if "depth_conv" in name and name .endswith (".weight" ): 612if data_torch .ndim == 3 and data_torch .shape [1 ]== 1 : 613data_torch = data_torch .squeeze (1 ) 614 615yield from super ().modify_tensors (data_torch ,name ,bid ) 616 617 618@ ModelBase . register ( "GraniteSpeechPlusForConditionalGeneration" ) 619@ ModelBase . example ( "ibm-granite/granite-speech-4.1-2b-plus" ) 620class GraniteSpeechPlusMmprojModel (GraniteSpeechMmprojModel ): 621"""Conversion for GraniteSpeechPlus - extends GraniteSpeech with feature layer concatenation""" 622has_vision_encoder = False 623has_audio_encoder = True 624 625def set_gguf_parameters (self ): 626assert self .hparams_audio is not None 627super ().set_gguf_parameters () 628 629# Add feature_layer if present in encoder config 630if feature_layers := self .hparams_audio .get ("cat_hidden_layers" ): 631self .gguf_writer .add_audio_feature_layers (feature_layers ) 632logger .info (f"gguf: audio feature_layers = { feature_layers } " ) 633 634# Validate projector dimension matches concatenated encoder output 635hidden_dim = self .hparams_audio ["hidden_dim" ] 636expected_dim = hidden_dim * (len (feature_layers )+ 1 ) 637projector_dim = self .global_config ["projector_config" ]["encoder_hidden_size" ] 638 639if projector_dim != expected_dim : 640raise ValueError ( 641f"Projector encoder_hidden_size ( { projector_dim } ) does not match " 642f"expected concatenated dimension ( { expected_dim } ). " 643f"Expected: hidden_dim ( { hidden_dim } ) * (len(feature_layers) + 1) = { expected_dim } " 644 ) 645 646 647@ ModelBase . register ( "Granite4VisionForConditionalGeneration" ) 648@ ModelBase . example ( "ibm-granite/granite-4.0-3b-vision" ) 649class Granite4VisionMmprojModel (MmprojModel ): 650has_vision_encoder = True 651has_audio_encoder = False 652 653@staticmethod 654def get_normalized_projector_map (global_config :dict )-> list [tuple [int ,int ,str ,int ]]: 655"""Normalize both deepstack and spatial projector maps to the form: 656(vision_layer, llm_layer, <type>, type_index) 657 658This is then used to populate the following mappings: 659- vision_feature_layers (mmproj hparam): ordered list of all 660vision_layer values where order corresponds with the order of the 661stacked projector tensors 662NOTE: Values may appear multiple times for spatial projectors 663- tensor_prefix_map (mmproj tensors): mapping from tensor prefixes to 664the index of the corresponding projector in the stacked tensors 665- deepstack_layer_arr (llm hparam): per-text-layer array indicating 666which input vision feature should be injected at that layer 667(-1 if none) 668 669Output: (vision_layer, llm_layer, <type>, type_index) 670""" 671deepstack_map = global_config .get ("deepstack_layer_map" , [])# [[vis_layer, llm_layer], ...] 672spatial_layers = global_config .get ("spatial_target_layers" , [])# [llm_layer, ...] 673n_text_layers = global_config ["text_config" ]["num_hidden_layers" ] 674n_vision_layers = global_config ["vision_config" ]["num_hidden_layers" ] 675normalized_projector_map = [] 676if deepstack_map : 677for deepstack_idx , (vision_layer ,llm_layer )in enumerate (sorted (deepstack_map )): 678if vision_layer < 0 : 679vision_layer = n_vision_layers + vision_layer 680if llm_layer < 0 : 681llm_layer = n_text_layers + llm_layer 682normalized_projector_map .append ((vision_layer ,llm_layer ,"layerwise" ,deepstack_idx )) 683if spatial_layers : 684spatial_vision_layer = global_config .get ("spatial_vision_layer" ,- 1 ) 685if spatial_vision_layer < 0 : 686spatial_vision_layer = n_vision_layers + spatial_vision_layer 687for spatial_idx ,llm_layer in enumerate (spatial_layers ): 688normalized_projector_map .append ((spatial_vision_layer ,llm_layer ,"spatial" ,spatial_idx )) 689return list (sorted (normalized_projector_map ,key = (lambda entry :entry [1 ]))) 690 691def __init__ (self ,* args ,** kwargs ): 692super ().__init__ (* args ,** kwargs ) 693normalized_projector_map = self .get_normalized_projector_map (self .global_config ) 694self ._n_proj = len (normalized_projector_map ) 695 696self ._tensor_prefix_map = { 697f"model. { proj_type } _projectors. { type_idx } " :proj_idx 698for proj_idx , (_ ,_ ,proj_type ,type_idx )in enumerate (normalized_projector_map ) 699 } 700self ._vision_feature_layers = [vision_layer for vision_layer ,_ ,_ ,_ in normalized_projector_map ] 701self ._spatial_offsets = [ 702type_idx if proj_type == "spatial" else - 1 703for _ ,_ ,proj_type ,type_idx in normalized_projector_map 704 ] 705 706def set_gguf_parameters (self ): 707assert self .hparams_vision is not None 708super ().set_gguf_parameters () 709 710self .gguf_writer .add_clip_projector_type (gguf .VisionProjectorType .GRANITE4_VISION ) 711 712# SigLIP encoder hparams 713self .gguf_writer .add_vision_attention_layernorm_eps (self .hparams .get ("layer_norm_eps" ,1e-6 )) 714self .gguf_writer .add_vision_use_gelu (True ) 715 716# Preprocessor 717self .gguf_writer .add_vision_preproc_image_size (self .hparams .get ("image_size" ,384 )) 718 719# QFormer projector config 720ds_rate = self .global_config ["downsample_rate" ] 721ds_parts = ds_rate .split ("/" ) 722assert len (ds_parts )== 2 ,f"Invalid 'downsample_rate' value: { ds_rate } " 723query_side ,window_side = [int (p )for p in ds_parts ] 724self .gguf_writer .add_vision_projector_query_side (query_side ) 725self .gguf_writer .add_vision_projector_window_side (window_side ) 726 727# Set vision feature layers 728self .gguf_writer .add_vision_feature_layers (self ._vision_feature_layers ) 729 730# Set the spatial offests per projector 731self .gguf_writer .add_vision_spatial_offsets (self ._spatial_offsets ) 732 733# Add flattened image grind pinpoints (resolution candidates internally) 734if pinpoints := self .global_config .get ("image_grid_pinpoints" ): 735# Flatten with h, w -> w, h inversion 736pinpoints = [val for h ,w in pinpoints for val in (w ,h )] 737self .gguf_writer .add_vision_image_grid_pinpoints (pinpoints ) 738 739@classmethod 740def filter_tensors (cls ,item :tuple [str ,Callable [[],Tensor ]])-> tuple [str ,Callable [[],Tensor ]]| None : 741name ,_ = item 742if ("vision_model.head" in name or name .startswith ("lm_head" )): 743return None 744return super ().filter_tensors (item ) 745 746def modify_tensors (self ,data_torch :Tensor ,name :str ,bid :int | None )-> Iterable [tuple [str ,Tensor ]]: 747 748# Detect projector tensors and bin them 749projector_idx = None 750for prefix ,proj_idx in self ._tensor_prefix_map .items (): 751if name .startswith (prefix ): 752projector_idx = proj_idx 753break 754if projector_idx is not None : 755# If this projector tensor has a block id within the projector, 756# alias the bid to projector_idx 757# 758# TODO: currently, none of the Granite 4 Vision models have 759# projectors with multiple QFormer layers, so the `layer.{}` index 760# is always 0. This allows us to simply map to a single `bid` that 761# matches the projector index. If this changes, we'll need a 762# convention that merges the two IDs. 763id_matches = list (re .finditer (r"\.([0-9]+)\." ,name )) 764all_ids = [int (m .group (1 ))for m in id_matches ] 765assert len (all_ids )>= 1 and len (all_ids )<= 2 ,"Must have at least 1 and at most 2 ids in tensor names" 766# If not layer id, just use the projector index 767new_bid = projector_idx 768if len (all_ids )== 1 : 769new_name = name [:id_matches [0 ].span (1 )[0 ]]+ str (new_bid )+ name [id_matches [0 ].span (1 )[1 ]:] 770else :# len(all_ids) == 2 771new_bid = projector_idx # + all_ids[1] 772new_name = name [:id_matches [0 ].span (0 )[0 ]]+ name [id_matches [0 ].span (1 )[1 ]:id_matches [1 ].span (1 )[0 ]]+ str (new_bid )+ name [id_matches [1 ].span (1 )[1 ]:] 773yield from super ().modify_tensors (data_torch ,new_name ,new_bid ) 774return 775yield from super ().modify_tensors (data_torch ,name ,bid )