CoolFace
Modelpublic

Felipe97/llama-cpp-compiled

sourceHugging Faceupdated 3d agoView on Hugging Face
0likes1.1kdownloads
granite.py776 linesDownload Raw Back to conversion
1from __future__ import annotations2 3import re4from typing import Any, Callable, Iterable, TYPE_CHECKING5 6import torch7 8if TYPE_CHECKING:9    from torch import Tensor10 11from .base import MmprojModel, ModelBase, gguf, logger12 13from .llama import LlamaModel14from .mamba import Mamba2Model15 16 17@ModelBase.register("GraniteForCausalLM")18@ModelBase.example("ibm-granite/granite-3.3-2b-instruct")19class GraniteModel(LlamaModel):20    """Conversion for IBM's GraniteForCausalLM"""21    model_arch = gguf.MODEL_ARCH.GRANITE22 23    def set_gguf_parameters(self):24        """Granite uses standard llama parameters with the following differences:25 26        - No head_dim support27        - New multiplier params:28            - attention_scale29            - embedding_scale30            - residual_scale31        - logits_scaling32        """33        if head_dim := self.hparams.pop("head_dim", None):34            logger.warning("Ignoring head_dim (%s) from config for Granite", head_dim)35        super().set_gguf_parameters()36        # NOTE: Convert _multiplier params to _scale params for naming37        #   consistency38        if attention_scale := self.hparams.get("attention_multiplier"):39            self.gguf_writer.add_attention_scale(attention_scale)40            logger.info("gguf: (granite) attention_scale = %s", attention_scale)41        if embedding_scale := self.hparams.get("embedding_multiplier"):42            self.gguf_writer.add_embedding_scale(embedding_scale)43            logger.info("gguf: (granite) embedding_scale = %s", embedding_scale)44        if residual_scale := self.hparams.get("residual_multiplier"):45            self.gguf_writer.add_residual_scale(residual_scale)46            logger.info("gguf: (granite) residual_scale = %s", residual_scale)47        if logits_scale := self.hparams.get("logits_scaling"):48            self.gguf_writer.add_logit_scale(logits_scale)49            logger.info("gguf: (granite) logits_scale = %s", logits_scale)50 51        # If being used as the base for Granite4 Vision, add deepstack_layer_arr52        if self.hparams.get("spatial_target_layers") or self.hparams.get("deepstack_layer_map"):53            normalized_projector_map = Granite4VisionMmprojModel.get_normalized_projector_map(self.hparams)54            deepstack_mapping_arr = [-1 for _ in range(self.block_count)] # Populate with -1 sentinels55            for proj_idx, (_, llm_layer, _, _) in enumerate(normalized_projector_map):56                # Skip the first projector which is handled as the base embedding57                # stream like normal58                if proj_idx == 0:59                    continue60                deepstack_mapping_arr[llm_layer] = proj_idx61            self.gguf_writer.add_deepstack_mapping(deepstack_mapping_arr)62 63    @classmethod64    def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:65        name, gen = item66        # Skip multimodal tensors67        if (68            name.startswith(("encoder."))69            or "image_" in name70            or "layerwise_projectors" in name71            or "spatial_projectors" in name72        ):73            return74        return super().filter_tensors(item)75 76 77@ModelBase.register("GraniteSWAForCausalLM")78class GraniteSWAModel(GraniteModel):79    """Conversion for IBM's GraniteSWAForCausalLM (interleaved sliding window attention)"""80    model_arch = gguf.MODEL_ARCH.GRANITE_SWA81 82    @classmethod83    def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:84        name, gen = item85 86        if name.endswith("sinks"):87            name += ".weight"88 89        return super().filter_tensors((name, gen))90 91    def set_gguf_parameters(self):92        """GraniteSWA uses Granite parameters plus sliding window configuration."""93        super().set_gguf_parameters()94 95        # Add sliding_window from config96        sliding_window = self.hparams.get("sliding_window", 128)97        self.gguf_writer.add_sliding_window(sliding_window)98        logger.info("gguf: (granite_swa) sliding_window = %s", sliding_window)99 100        # Derive sliding_window_pattern from layer_types101        if layer_types := self.hparams.get("layer_types"):102            is_swa = [t == "sliding_attention" for t in layer_types]103            self.gguf_writer.add_sliding_window_pattern(is_swa)104            logger.info("gguf: (granite_swa) sliding_window_pattern = %d SWA layers / %d total",105                        sum(is_swa), len(is_swa))106        else:107            # Fall back to period-based pattern: i % 4 != 0108            # This matches the transformers default pattern109            n_layers = self.block_count110            is_swa = [i % 4 != 0 for i in range(n_layers)]111            self.gguf_writer.add_sliding_window_pattern(is_swa)112            logger.info("gguf: (granite_swa) sliding_window_pattern (inferred) = %d SWA layers / %d total",113                        sum(is_swa), n_layers)114 115        # Add rope_pattern from no_rope_layers116        if no_rope_layers := self.hparams.get("no_rope_layers"):117            # Convert 1/0 to bool (1 = use RoPE, 0 = NoPE)118            rope_pattern = [bool(x) for x in no_rope_layers]119            self.gguf_writer.add_rope_pattern(rope_pattern)120            logger.info("gguf: (granite_swa) rope_pattern = %d RoPE layers / %d total",121                        sum(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)"""127    model_arch = gguf.MODEL_ARCH.GRANITE_SWA128 129    def set_gguf_parameters(self):130        super().set_gguf_parameters()131        if shared_intermediate_size := self.hparams.get("shared_intermediate_size"):132            self.gguf_writer.add_expert_shared_feed_forward_length(shared_intermediate_size)133            logger.info("gguf: (granitemoewa) shared_intermediate_size = %s", shared_intermediate_size)134 135    def 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_proj139        # Kept fused since inference (build_moe_ffn) supports a single gate_up_exps140        # tensor for the routed experts.141        if name.endswith("block_sparse_moe.experts.gate_up_proj"):142            ffn_dim = self.hparams["intermediate_size"]143            assert data_torch.shape[-2] == 2 * ffn_dim, f"Merged FFN tensor size must be 2 * intermediate_size, got {data_torch.shape[-2]}"144            yield from ModelBase.modify_tensors(self, data_torch, self.format_tensor_name(gguf.MODEL_TENSOR.FFN_GATE_UP_EXP, bid), bid)145            return146 147        # Handle expert FFN down projection - swash format: experts.down_proj148        if name.endswith("block_sparse_moe.experts.down_proj"):149            yield from ModelBase.modify_tensors(self, data_torch, self.format_tensor_name(gguf.MODEL_TENSOR.FFN_DOWN_EXP, bid), bid)150            return151 152        # Handle expert FFN tensors (merged gate+up) - standard granite format: input_linear.weight153        # Kept fused since inference (build_moe_ffn) supports a single gate_up_exps154        # tensor for the routed experts.155        if name.endswith("block_sparse_moe.input_linear.weight"):156            ffn_dim = self.hparams["intermediate_size"]157            assert data_torch.shape[-2] == 2 * ffn_dim, "Merged FFN tensor size must be 2 * intermediate_size"158            yield from ModelBase.modify_tensors(self, data_torch, self.format_tensor_name(gguf.MODEL_TENSOR.FFN_GATE_UP_EXP, bid), bid)159            return160 161        # Handle shared expert FFN tensors (if present) - kept fused since162        # inference (build_ffn) supports a single ffn_up_shexp tensor with163        # LLM_FFN_SWIGLU for the shared expert.164        if name.endswith("shared_mlp.input_linear.weight"):165            ffn_dim = self.hparams.get("shared_intermediate_size", self.hparams["intermediate_size"])166            assert data_torch.shape[-2] == 2 * ffn_dim, "Merged FFN tensor size must be 2 * shared_intermediate_size"167            yield from ModelBase.modify_tensors(self, data_torch, self.format_tensor_name(gguf.MODEL_TENSOR.FFN_UP_SHEXP, bid), bid)168            return169 170        # Handle shared expert output (if present)171        if name.endswith("shared_mlp.output_linear.weight"):172            yield from ModelBase.modify_tensors(self, data_torch, self.format_tensor_name(gguf.MODEL_TENSOR.FFN_DOWN_SHEXP, bid), bid)173            return174 175        # Pass through to parent for all other tensors (including sinks)176        yield 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"""183    model_arch = gguf.MODEL_ARCH.GRANITE_MOE184 185    def set_gguf_parameters(self):186        """GraniteMoeShared uses GraniteMoe parameters plus the following:187        - shared_intermediate_size188        """189        super().set_gguf_parameters()190        if shared_feed_forward_length := self.hparams.get("shared_intermediate_size"):191            self.gguf_writer.add_expert_shared_feed_forward_length(shared_feed_forward_length)192            logger.info("gguf: (granitemoeshared) shared_feed_forward_length = %s", shared_feed_forward_length)193 194    def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:195        """In modeling_granitemoe, the JetMoe implementation of parallel experts196        is used. This essentially merges w1 and w3 into a single tensor with 2x197        the hidden size that is then split during forward. To keep compatibility198        with existing mixtral support, we pull them apart here.199        """200 201        if name.endswith("block_sparse_moe.input_linear.weight"):202            ffn_dim = self.hparams["intermediate_size"]203            assert data_torch.shape[-2] == 2 * ffn_dim, "Merged FFN tensor size must be 2 * intermediate_size"204            gate, up = data_torch.split(ffn_dim, dim=-2)205            yield from ModelBase.modify_tensors(self, gate, self.format_tensor_name(gguf.MODEL_TENSOR.FFN_GATE_EXP, bid), bid)206            yield from ModelBase.modify_tensors(self, up, self.format_tensor_name(gguf.MODEL_TENSOR.FFN_UP_EXP, bid), bid)207            return208 209        has_experts = bool(self.hparams.get('num_local_experts'))210 211        if name.endswith("shared_mlp.input_linear.weight"):212            ffn_dim = self.hparams["shared_intermediate_size"]213            assert data_torch.shape[-2] == 2 * ffn_dim, "Merged FFN tensor size must be 2 * shared_intermediate_size"214            gate, up = data_torch.split(ffn_dim, dim=-2)215            if has_experts:216                yield from ModelBase.modify_tensors(self, gate,self.format_tensor_name(gguf.MODEL_TENSOR.FFN_GATE_SHEXP, bid), bid)217                yield from ModelBase.modify_tensors(self, up, self.format_tensor_name(gguf.MODEL_TENSOR.FFN_UP_SHEXP, bid), bid)218                return219            yield from ModelBase.modify_tensors(self, gate, self.format_tensor_name(gguf.MODEL_TENSOR.FFN_GATE, bid), bid)220            yield from ModelBase.modify_tensors(self, up, self.format_tensor_name(gguf.MODEL_TENSOR.FFN_UP, bid), bid)221            return222 223        if not has_experts and name.endswith("shared_mlp.output_linear.weight"):224            yield from ModelBase.modify_tensors(self, data_torch, self.format_tensor_name(gguf.MODEL_TENSOR.FFN_DOWN, bid), bid)225            return226 227        yield 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, stacked234    over the adapter dim with a zero adapter at slot 0 (N = num_adapters + 1)."""235    model_arch = gguf.MODEL_ARCH.GRANITE_SWITCH236 237    # permute q/k per-slice below (NORM-rope layout), not via the parent's auto-permute238    undo_permute = False239 240    def __init__(self, *args, **kwargs):241        super().__init__(*args, **kwargs)242        # the weightless switch reserves one cache slot: one fewer block than num_hidden_layers243        self.block_count = self.block_count - 1244        self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count)245 246        self._n_adapters = int(self.hparams["num_adapters"])247        self._max_lora_rank = int(self.hparams["max_lora_rank"])248        self._n_slots = self._n_adapters + 1  # +1 for the zero slot at index 0249 250        n_head = int(self.hparams["num_attention_heads"])251        n_kv_head = int(self.hparams["num_key_value_heads"])252        head_dim = (253            self.hparams.get("projection_head_dim")254            or self.hparams.get("head_dim")255            or (self.hparams["hidden_size"] // n_head)256        )257        self._n_head = n_head258        self._n_kv_head = n_kv_head259        self._head_dim = int(head_dim)260        self._q_size = n_head * self._head_dim261        self._kv_size = n_kv_head * self._head_dim262 263    def set_gguf_parameters(self):264        super().set_gguf_parameters()265 266        # dense: pin expert_used_count to 0 (config carries a leftover num_experts_per_tok)267        if not self.hparams.get("num_local_experts"):268            self.gguf_writer.add_expert_used_count(0)269 270        self.gguf_writer.add_adapter_count(self._n_adapters)271        self.gguf_writer.add_adapter_lora_rank(self._max_lora_rank)272        self.gguf_writer.add_adapter_token_ids_activate(self.hparams["adapter_token_ids"])273        self.gguf_writer.add_adapter_token_ids_substitute(self.hparams["adapter_substitute_token_ids"])274        router_gain = float(self.hparams.get("control_token_gain", 15.0))275        self.gguf_writer.add_adapter_router_gain(router_gain)276        logger.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 278    def _lora_a(self, data: Tensor) -> Tensor:279        # on-disk A: [n_adapters, 1, max_rank, in] -> [n_adapters+1, max_rank, in]280        a = data.squeeze(1)281        zero = torch.zeros_like(a[:1])282        return torch.cat([zero, a], dim=0).contiguous()283 284    def _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]286        b = data.squeeze(1)287        if permute_n_head is not None:288            # permute each adapter's B output rows to match the permuted q/k base289            b = torch.stack([self.permute(b[i], permute_n_head, permute_n_head) for i in range(b.shape[0])], dim=0)290        zero = torch.zeros_like(b[:1])291        return torch.cat([zero, b], dim=0).contiguous()292 293    def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:294        T = gguf.MODEL_TENSOR295 296        # skip the weightless switch + control-token buffers (rebuilt at load time)297        bare = name.split(".")[-1]298        if (299            name.startswith("model.switch.") or name.startswith("switch.")300            or bare in ("adapter_token_ids", "control_to_substitute_lut")301        ):302            return303 304        if "self_attn.qkv_proj" in name:305            if name.endswith("base_layer.weight"):306                # fused [q|k|v] rows: permute q/k row-blocks for ggml's NORM-rope layout307                q, k, v = data_torch.split([self._q_size, self._kv_size, self._kv_size], dim=0)308                q = self.permute(q, self._n_head, self._n_head)309                k = self.permute(k, self._n_kv_head, self._n_kv_head)310                fused = torch.cat([q, k, v], dim=0)311                yield (self.format_tensor_name(T.ATTN_QKV, bid), fused)312                return313            if "lora_A_slices." in name:314                slot = int(name.rsplit(".", 1)[1])315                key = {0: T.ATTN_Q, 1: T.ATTN_K, 2: T.ATTN_V}[slot]316                yield (self.format_tensor_name(key, bid, suffix=".lora_a"), self._lora_a(data_torch))317                return318            if "lora_B_slices." in name:319                slot = int(name.rsplit(".", 1)[1])320                key, ph = {321                    0: (T.ATTN_Q, self._n_head),322                    1: (T.ATTN_K, self._n_kv_head),323                    2: (T.ATTN_V, None),324                }[slot]325                yield (self.format_tensor_name(key, bid, suffix=".lora_b"), self._lora_b(data_torch, ph))326                return327            raise ValueError(f"Unexpected qkv_proj tensor: {name}")328 329        if "self_attn.o_proj" in name:330            if name.endswith("base_layer.weight"):331                yield (self.format_tensor_name(T.ATTN_OUT, bid), data_torch)332                return333            if name.endswith("lora_A"):334                yield (self.format_tensor_name(T.ATTN_OUT, bid, suffix=".lora_a"), self._lora_a(data_torch))335                return336            if name.endswith("lora_B"):337                yield (self.format_tensor_name(T.ATTN_OUT, bid, suffix=".lora_b"), self._lora_b(data_torch))338                return339            raise ValueError(f"Unexpected o_proj tensor: {name}")340 341        if "shared_mlp.input_linear" in name:342            ffn = self.hparams["shared_intermediate_size"]343            if name.endswith("base_layer.weight"):344                gate, up = data_torch.split([ffn, ffn], dim=0)345                yield (self.format_tensor_name(T.FFN_GATE, bid), gate)346                yield (self.format_tensor_name(T.FFN_UP, bid), up)347                return348            if "lora_A_slices." in name:349                slot = int(name.rsplit(".", 1)[1])350                key = {0: T.FFN_GATE, 1: T.FFN_UP}[slot]351                yield (self.format_tensor_name(key, bid, suffix=".lora_a"), self._lora_a(data_torch))352                return353            if "lora_B_slices." in name:354                slot = int(name.rsplit(".", 1)[1])355                key = {0: T.FFN_GATE, 1: T.FFN_UP}[slot]356                yield (self.format_tensor_name(key, bid, suffix=".lora_b"), self._lora_b(data_torch))357                return358            raise ValueError(f"Unexpected shared_mlp.input_linear tensor: {name}")359 360        if "shared_mlp.output_linear" in name:361            if name.endswith("base_layer.weight"):362                yield (self.format_tensor_name(T.FFN_DOWN, bid), data_torch)363                return364            if name.endswith("lora_A"):365                yield (self.format_tensor_name(T.FFN_DOWN, bid, suffix=".lora_a"), self._lora_a(data_torch))366                return367            if name.endswith("lora_B"):368                yield (self.format_tensor_name(T.FFN_DOWN, bid, suffix=".lora_b"), self._lora_b(data_torch))369                return370            raise ValueError(f"Unexpected shared_mlp.output_linear tensor: {name}")371 372        if bid is not None and ".layers." in name and (373            "input_layernorm" in name or "post_attention_layernorm" in name374        ):375            key = T.ATTN_NORM if "input_layernorm" in name else T.FFN_NORM376            yield (self.format_tensor_name(key, bid), data_torch)377            return378 379        if name in ("model.embed_tokens.weight", "embed_tokens.weight"):380            yield (self.format_tensor_name(T.TOKEN_EMBD), data_torch)381            return382        if name in ("model.norm.weight", "norm.weight"):383            yield (self.format_tensor_name(T.OUTPUT_NORM), data_torch)384            return385        if name == "lm_head.weight":386            return  # tied to token_embd387 388        raise 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 SSM395    layers and optionally uses MoE w/ a shared expert"""396    model_arch = gguf.MODEL_ARCH.GRANITE_HYBRID397    undo_permute = True398 399    def __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 configurable403        self.hparam_prefixes = ["mamba"]404 405        super().__init__(*args, **kwargs)406 407        # Lists of which layers use ssm vs attention408        self._attn_layers = self.get_attn_layers()409        self._ssm_layers = [410            i for i in range(self.block_count)411            if i not in self._attn_layers412        ]413 414        # There are some models in this family that are non-hybrid, but keep the415        # same parent class by setting all layers to "attention." If this is the416        # case, the model architecture needs to be updated to a standard417        # "granite" or "granitemoe" model418        if not self._ssm_layers:419            has_experts = self.find_hparam(["num_experts_per_tok", "num_experts_per_token"], optional=True)420            new_arch = (421                gguf.MODEL_ARCH.GRANITE_MOE422                if has_experts else423                gguf.MODEL_ARCH.GRANITE424            )425            self.model_arch = new_arch426            self.gguf_writer.arch = gguf.MODEL_ARCH_NAMES[new_arch]427            self.gguf_writer.add_architecture()428 429        # n_group and d_inner are used during reshape_tensors for mamba2430        # NOTE: Explicitly include hparam prefix prefix for d_model to431        #   disambiguate with top-level head_dim432        # NOTE 2: If needed for future models, this can be isolated in a method433        #   to separate the prefix setting and the keys used434        self.d_model = self.find_hparam([f"{self.hparam_prefixes[0]}_head_dim", "hidden_size", "d_model"])435        self.n_group = self.find_hparam(["n_groups", "num_groups"])436        self.d_inner = self.find_hparam(["expand", "num_heads"]) * self.d_model437 438    def get_attn_layers(self):439        # Explicit list of layer type names440        if layer_types := self.hparams.get("layer_types"):441            return [442                i for i, typ in enumerate(layer_types)443                if typ == "attention"444            ]445 446        # Layer types indicated by index or period447        attn_layers = self.hparams.get("attn_layer_indices", [])448        if not attn_layers:449            attn_period = self.hparams.get("attn_layer_period")450            assert attn_period, "Didn't find attn_layer_indices or attn_layer_period"451            attn_offset = self.hparams.get("attn_layer_offset")452            assert attn_offset is not None, "No attention layer offset set with attn_layer_period"453            attn_layers = [454                i for i in range(self.block_count)455                if i % attn_period == attn_offset456            ]457        return attn_layers458 459    def find_hparam(self, keys: Iterable[str], *args, **kwargs) -> Any:460        prefixed = []461        for pfx in self.hparam_prefixes:462            prefixed.extend(463                "_".join([pfx, k])464                for k in keys465            )466        keys = list(keys) + prefixed467        return Mamba2Model.find_hparam(self, keys, *args, **kwargs)468 469    def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:470        if (471            name.endswith("block_sparse_moe.input_linear.weight")472            or "shared_mlp" in name473        ):474            yield from GraniteMoeModel.modify_tensors(self, data_torch, name, bid)475            return476 477        # Determine whether this is a mamba layer or an attention layer478        if bid in self._ssm_layers:479            yield from Mamba2Model.modify_tensors(self, data_torch, name, bid)480            return481        elif bid in self._attn_layers:482            yield from GraniteMoeModel.modify_tensors(self, data_torch, name, bid)483            return484        yield from ModelBase.modify_tensors(self, data_torch, name, bid)485 486    def set_gguf_parameters(self):487        """This method merges params from both parents and some that are488        specific to this model. The result is some duplication of how the params489        get set. The following warnings are expected during conversion:490 491        WARNING:Duplicated key name 'granitehybrid.attention.head_count_kv'492        WARNING:Duplicated key name 'granitehybrid.context_length'493        """494        GraniteMoeModel.set_gguf_parameters(self)495 496        ## Mamba mixer params ##497        self.gguf_writer.add_ssm_conv_kernel(self.find_hparam(["conv_kernel", "d_conv"]))498        self.gguf_writer.add_ssm_state_size(self.find_hparam(["state_size", "d_state", "state_dim", "ssm_state_size"]))499        self.gguf_writer.add_ssm_group_count(self.n_group)500        self.gguf_writer.add_ssm_inner_size(self.d_inner)501        # NOTE: The mamba_dt_rank is _not_ the right field for how this is used502        #   in llama.cpp503        self.gguf_writer.add_ssm_time_step_rank(self.find_hparam(["n_heads", "num_heads"]))504 505        ## Attention params ##506        head_count_kv = self.find_hparam(["num_key_value_heads", "n_head_kv"])507        head_count_kv_vec = [508            head_count_kv if i in self._attn_layers else 0 for i in range(self.block_count)509        ]510        if rope_dim := self.hparams.get("attn_rotary_emb"):511            self.gguf_writer.add_rope_dimension_count(rope_dim)512        self.gguf_writer.add_head_count_kv(head_count_kv_vec)513 514        ## If Bamba or non-hybrid, use rope, otherwise don't515        use_rope = (516            "BambaForCausalLM" in self.hparams["architectures"]517            or not self._ssm_layers518        )519        self.gguf_writer.add_rope_scaling_finetuned(use_rope)520        if not use_rope:521            self.gguf_writer.add_context_length(2**20)522 523        ## Validation ##524        d_head = self.find_hparam(["d_head"], optional=True) or 64525        assert self.hparams.get("hidden_act") in [None, "silu"], "Only SILU activation supported"526        assert self.d_inner % d_head == 0, f"SSM inner size {self.d_inner} not a multiple of head dim {d_head}"527 528    def set_vocab(self):529        # For models with no ssm layers, don't pad for mamba2530        self.hparams["pad_vocab_size_multiple"] = 8 if self._ssm_layers else 1531        Mamba2Model.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):537    has_vision_encoder = False538    has_audio_encoder = True539 540    _batch_norm_tensors: list[dict[str, Tensor]] | None = None541 542    def get_audio_config(self) -> dict[str, Any] | None:543        return self.global_config.get("encoder_config")544 545    def set_gguf_parameters(self):546        assert self.hparams_audio is not None547        a = self.hparams_audio548        a["hidden_size"] = a["hidden_dim"]549        a["intermediate_size"] = a["hidden_dim"] * a["feedforward_mult"]550        a["num_attention_heads"] = a["num_heads"]551        a["num_hidden_layers"] = a["num_layers"]552 553        super().set_gguf_parameters()554 555        self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.GRANITE_SPEECH)556        self.gguf_writer.add_audio_num_mel_bins(a["input_dim"])557        self.gguf_writer.add_audio_attention_layernorm_eps(1e-5)558        self.gguf_writer.add_audio_chunk_size(a["context_size"])559        self.gguf_writer.add_audio_conv_kernel_size(a["conv_kernel_size"])560        self.gguf_writer.add_audio_max_pos_emb(a["max_pos_emb"])561 562        p = self.global_config563        self.gguf_writer.add_audio_projector_window_size(p["window_size"])564        self.gguf_writer.add_audio_projector_downsample_rate(p["downsample_rate"])565        self.gguf_writer.add_audio_projector_head_count(p["projector_config"]["num_attention_heads"])566 567    def tensor_force_quant(self, name, new_name, bid, n_dims):568        if "encoder" in name or "projector" in name:569            if ".conv" in name and ".weight" in name:570                return gguf.GGMLQuantizationType.F32571        return super().tensor_force_quant(name, new_name, bid, n_dims)572 573    @classmethod574    def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:575        name, gen = item576        if "attention_dists" in name or "num_batches_tracked" in name:577            return None578        return super().filter_tensors(item)579 580    def 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_norm582        if "batch_norm" in name and "encoder.layers." in name:583            if self._batch_norm_tensors is None:584                self._batch_norm_tensors = [{} for _ in range(self.block_count)]585            assert bid is not None586            self._batch_norm_tensors[bid][name] = data_torch587            if len(self._batch_norm_tensors[bid]) < 4:588                return589            prefix = f"encoder.layers.{bid}.conv.batch_norm"590            weight = self._batch_norm_tensors[bid][f"{prefix}.weight"]591            bias = self._batch_norm_tensors[bid][f"{prefix}.bias"]592            running_mean = self._batch_norm_tensors[bid][f"{prefix}.running_mean"]593            running_var = self._batch_norm_tensors[bid][f"{prefix}.running_var"]594            eps = 1e-5595            a = weight / torch.sqrt(running_var + eps)596            b = bias - running_mean * a597            yield from super().modify_tensors(a, f"encoder.layers.{bid}.conv.batch_norm.weight", bid)598            yield from super().modify_tensors(b, f"encoder.layers.{bid}.conv.batch_norm.bias", bid)599            return600 601        if ".attn.to_kv.weight" in name:602            k_weight, v_weight = data_torch.chunk(2, dim=0)603            yield from super().modify_tensors(k_weight, name.replace("to_kv", "to_k"), bid)604            yield from super().modify_tensors(v_weight, name.replace("to_kv", "to_v"), bid)605            return606 607        if ("up_conv" in name or "down_conv" in name) and name.endswith(".weight"):608            if data_torch.ndim == 3 and data_torch.shape[2] == 1:609                data_torch = data_torch.squeeze(2)610 611        if "depth_conv" in name and name.endswith(".weight"):612            if data_torch.ndim == 3 and data_torch.shape[1] == 1:613                data_torch = data_torch.squeeze(1)614 615        yield 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"""622    has_vision_encoder = False623    has_audio_encoder = True624 625    def set_gguf_parameters(self):626        assert self.hparams_audio is not None627        super().set_gguf_parameters()628 629        # Add feature_layer if present in encoder config630        if feature_layers := self.hparams_audio.get("cat_hidden_layers"):631            self.gguf_writer.add_audio_feature_layers(feature_layers)632            logger.info(f"gguf: audio feature_layers = {feature_layers}")633 634            # Validate projector dimension matches concatenated encoder output635            hidden_dim = self.hparams_audio["hidden_dim"]636            expected_dim = hidden_dim * (len(feature_layers) + 1)637            projector_dim = self.global_config["projector_config"]["encoder_hidden_size"]638 639            if projector_dim != expected_dim:640                raise ValueError(641                    f"Projector encoder_hidden_size ({projector_dim}) does not match "642                    f"expected concatenated dimension ({expected_dim}). "643                    f"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):650    has_vision_encoder = True651    has_audio_encoder = False652 653    @staticmethod654    def 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 658        This is then used to populate the following mappings:659        - vision_feature_layers (mmproj hparam): ordered list of all660          vision_layer values where order corresponds with the order of the661          stacked projector tensors662          NOTE: Values may appear multiple times for spatial projectors663        - tensor_prefix_map (mmproj tensors): mapping from tensor prefixes to664          the index of the corresponding projector in the stacked tensors665        - deepstack_layer_arr (llm hparam): per-text-layer array indicating666          which input vision feature should be injected at that layer667          (-1 if none)668 669        Output: (vision_layer, llm_layer, <type>, type_index)670        """671        deepstack_map = global_config.get("deepstack_layer_map", [])  # [[vis_layer, llm_layer], ...]672        spatial_layers = global_config.get("spatial_target_layers", [])  # [llm_layer, ...]673        n_text_layers = global_config["text_config"]["num_hidden_layers"]674        n_vision_layers = global_config["vision_config"]["num_hidden_layers"]675        normalized_projector_map = []676        if deepstack_map:677            for deepstack_idx, (vision_layer, llm_layer) in enumerate(sorted(deepstack_map)):678                if vision_layer < 0:679                    vision_layer = n_vision_layers + vision_layer680                if llm_layer < 0:681                    llm_layer = n_text_layers + llm_layer682                normalized_projector_map.append((vision_layer, llm_layer, "layerwise", deepstack_idx))683        if spatial_layers:684            spatial_vision_layer = global_config.get("spatial_vision_layer", -1)685            if spatial_vision_layer < 0:686                spatial_vision_layer = n_vision_layers + spatial_vision_layer687            for spatial_idx, llm_layer in enumerate(spatial_layers):688                normalized_projector_map.append((spatial_vision_layer, llm_layer, "spatial", spatial_idx))689        return list(sorted(normalized_projector_map, key=(lambda entry: entry[1])))690 691    def __init__(self, *args, **kwargs):692        super().__init__(*args, **kwargs)693        normalized_projector_map = self.get_normalized_projector_map(self.global_config)694        self._n_proj = len(normalized_projector_map)695 696        self._tensor_prefix_map = {697            f"model.{proj_type}_projectors.{type_idx}": proj_idx698            for proj_idx, (_, _, proj_type, type_idx) in enumerate(normalized_projector_map)699        }700        self._vision_feature_layers = [vision_layer for vision_layer, _, _, _ in normalized_projector_map]701        self._spatial_offsets = [702            type_idx if proj_type == "spatial" else -1703            for _, _, proj_type, type_idx in normalized_projector_map704        ]705 706    def set_gguf_parameters(self):707        assert self.hparams_vision is not None708        super().set_gguf_parameters()709 710        self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.GRANITE4_VISION)711 712        # SigLIP encoder hparams713        self.gguf_writer.add_vision_attention_layernorm_eps(self.hparams.get("layer_norm_eps", 1e-6))714        self.gguf_writer.add_vision_use_gelu(True)715 716        # Preprocessor717        self.gguf_writer.add_vision_preproc_image_size(self.hparams.get("image_size", 384))718 719        # QFormer projector config720        ds_rate = self.global_config["downsample_rate"]721        ds_parts = ds_rate.split("/")722        assert len(ds_parts) == 2, f"Invalid 'downsample_rate' value: {ds_rate}"723        query_side, window_side = [int(p) for p in ds_parts]724        self.gguf_writer.add_vision_projector_query_side(query_side)725        self.gguf_writer.add_vision_projector_window_side(window_side)726 727        # Set vision feature layers728        self.gguf_writer.add_vision_feature_layers(self._vision_feature_layers)729 730        # Set the spatial offests per projector731        self.gguf_writer.add_vision_spatial_offsets(self._spatial_offsets)732 733        # Add flattened image grind pinpoints (resolution candidates internally)734        if pinpoints := self.global_config.get("image_grid_pinpoints"):735            # Flatten with h, w -> w, h inversion736            pinpoints = [val for h, w in pinpoints for val in (w, h)]737            self.gguf_writer.add_vision_image_grid_pinpoints(pinpoints)738 739    @classmethod740    def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:741        name, _ = item742        if ("vision_model.head" in name or name.startswith("lm_head")):743            return None744        return super().filter_tensors(item)745 746    def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:747 748        # Detect projector tensors and bin them749        projector_idx = None750        for prefix, proj_idx in self._tensor_prefix_map.items():751            if name.startswith(prefix):752                projector_idx = proj_idx753                break754        if projector_idx is not None:755            # If this projector tensor has a block id within the projector,756            # alias the bid to projector_idx757            #758            # TODO: currently, none of the Granite 4 Vision models have759            # projectors with multiple QFormer layers, so the `layer.{}` index760            # is always 0. This allows us to simply map to a single `bid` that761            # matches the projector index. If this changes, we'll need a762            # convention that merges the two IDs.763            id_matches = list(re.finditer(r"\.([0-9]+)\.", name))764            all_ids = [int(m.group(1)) for m in id_matches]765            assert 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 index767            new_bid = projector_idx768            if len(all_ids) == 1:769                new_name = name[:id_matches[0].span(1)[0]] + str(new_bid) + name[id_matches[0].span(1)[1]:]770            else: # len(all_ids) == 2771                new_bid = projector_idx # + all_ids[1]772                new_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]:]773            yield from super().modify_tensors(data_torch, new_name, new_bid)774            return775        yield from super().modify_tensors(data_torch, name, bid)776