char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
fa88ae936
char/aliceai-t5-moe
1from __future__import annotations 2 3import re 4from collections .abc import Iterable 5from typing import TYPE_CHECKING 6 7import torch 8 9if TYPE_CHECKING : 10from torch import Tensor 11 12from .base import ModelBase ,TextModel ,gguf ,logger 13 14 15@ ModelBase . register ( "LagunaForCausalLM" ) 16@ ModelBase . example ( "poolside/Laguna-XS.2" , "poolside/Laguna-S-2.1" ) 17class LagunaModel (TextModel ): 18model_arch = gguf .MODEL_ARCH .LAGUNA 19_experts :list [dict ]| None = None 20_gate_types :list [str ]| None = None 21 22# --- vocab --------------------------------------------------------------- 23 24def set_vocab (self )-> None : 25self ._set_vocab_gpt2 () 26 27# Some Laguna releases wrap the chat template in tokenizer_config.json as 28# "{% include 'chat_template.jinja' %}", which SpecialVocab embeds verbatim 29# and llama.cpp's jinja engine cannot process. Prefer the resolved template 30# from the chat_template.jinja file so the GGUF is self-contained. 31tmpl_file = self .dir_model / "chat_template.jinja" 32if tmpl_file .is_file (): 33self .gguf_writer .add_chat_template (tmpl_file .read_text (encoding = "utf-8" )) 34logger .info ("gguf: embedded resolved chat_template.jinja (overriding include directive)" ) 35 36# eos_token_id is a list [2, 24]: token 2 (EOS, also BOS) and token 24 37# (</assistant>, the turn-end). _set_vocab_gpt2 only records the scalar 38# eos, so register the extra id as eot; llama.cpp folds eot into its EOG 39# set, so the model halts on </assistant> natively. 40eos_ids = self .hparams .get ("eos_token_id" ) 41if isinstance (eos_ids ,list ): 42bos_id = self .hparams .get ("bos_token_id" ) 43extra = [e for e in eos_ids if e != bos_id ] 44if extra : 45self .gguf_writer .add_eot_token_id (extra [0 ]) 46logger .info (f"gguf: registered eot_token_id= { extra [ 0 ] } from eos list { eos_ids } " ) 47 48def get_vocab_base (self )-> tuple [list [str ],list [int ],str ]: 49# </assistant> is the assistant turn-end (registered as eot below). The 50# HF tokenizer flags it special=false, so the base classifies it as 51# USER_DEFINED and llama.cpp renders its text into generated content, 52# leaking "</assistant>" and breaking response parsing. It is a control 53# marker, so promote it to CONTROL: llama.cpp then treats it as 54# end-of-generation and suppresses its text. 55tokens ,toktypes ,tokpre = super ().get_vocab_base () 56for i ,tok in enumerate (tokens ): 57if tok == "</assistant>" : 58toktypes [i ]= gguf .TokenType .CONTROL 59logger .info (f"gguf: marked </assistant> (id { i } ) as CONTROL token" ) 60return tokens ,toktypes ,tokpre 61 62# --- hparams ------------------------------------------------------------- 63 64def set_gguf_parameters (self )-> None : 65super ().set_gguf_parameters () 66hparams = self .hparams 67 68# super() does not emit vocab_size for the gpt2 vocab path; head_count is 69# overridden with a per-layer array (XS.2 varies heads per layer via 70# num_attention_heads_per_layer; M.1 is uniform and omits it). 71self .gguf_writer .add_vocab_size (hparams ["vocab_size" ]) 72 73per_layer_heads = hparams .get ("num_attention_heads_per_layer" ) 74if not per_layer_heads : 75per_layer_heads = [hparams ["num_attention_heads" ]]* hparams ["num_hidden_layers" ] 76assert len (per_layer_heads )== hparams ["num_hidden_layers" ], ( 77f"num_attention_heads_per_layer length { len ( per_layer_heads ) } != " 78f"num_hidden_layers { hparams [ 'num_hidden_layers' ] } " 79 ) 80self .gguf_writer .add_head_count (per_layer_heads ) 81 82# Resolve + validate the attention gate type now so an inconsistent 83# `gating` field fails at conversion time. See _attn_gate_types. 84self ._attn_gate_types () 85 86# SWA window size (M.1 has none -> key omitted, swa_type stays NONE). 87sliding_window = hparams .get ("sliding_window" )or 0 88if sliding_window > 0 : 89self .gguf_writer .add_sliding_window (sliding_window ) 90 91# MoE (expert_count / expert_used_count come from super().set_gguf_parameters()) 92self .gguf_writer .add_expert_feed_forward_length (hparams ["moe_intermediate_size" ]) 93self .gguf_writer .add_expert_shared_feed_forward_length (hparams ["shared_expert_intermediate_size" ]) 94self .gguf_writer .add_expert_weights_norm (True )# HF reference always sum-normalises after top-k 95self .gguf_writer .add_expert_weights_scale (float (hparams ["moe_routed_scaling_factor" ])) 96self .gguf_writer .add_expert_gating_func (gguf .ExpertGatingFuncType .SIGMOID ) 97 98# Leading dense layers (XS.2 has 1, M.1 has 3) before the MoE layers. 99mlp_layer_types :list [str ]= hparams ["mlp_layer_types" ] 100leading_dense = 0 101for t in mlp_layer_types : 102if t == "dense" : 103leading_dense += 1 104else : 105break 106self .gguf_writer .add_leading_dense_block_count (leading_dense ) 107 108# Per-layer-type RoPE dimension count (partial rotary). base emits 109# rope_freq_base(_swa) and the YaRN params from self.rope_parameters. 110head_dim = hparams ["head_dim" ] 111full_rope = self .rope_parameters ["full_attention" ] 112self .gguf_writer .add_rope_dimension_count ( 113int (head_dim * float (full_rope .get ("partial_rotary_factor" ,1.0 )))) 114swa_rope = self .rope_parameters .get ("sliding_attention" ) 115if swa_rope is not None : 116self .gguf_writer .add_rope_dimension_count_swa ( 117int (head_dim * float (swa_rope .get ("partial_rotary_factor" ,1.0 )))) 118 119def _attn_gate_types (self )-> list [str ]: 120"""Per-layer attention output gate type: "per_head" or "per_element". 121 122`gating_types` (per layer) is authoritative when present; otherwise the 123scalar `gating` field is used (the "per-element"/"per-head" string, or 124the legacy boolean True == per-head, as in Laguna-XS.2). 125 126Fails loudly when the model is per-element but the `gating` field does 127not declare that as a string: runtimes that key off `gating` (vLLM, 128transformers) ignore gating_types and read a bare boolean True as 129per-head, silently corrupting the model. Surfacing it here keeps a 130broken checkpoint from being packaged as if it were fine. 131""" 132if self ._gate_types is not None : 133return self ._gate_types 134hparams = self .hparams 135n_layer = hparams ["num_hidden_layers" ] 136gating = hparams .get ("gating" ) 137gating_types = hparams .get ("gating_types" ) 138 139def _norm (t :object )-> str : 140sval = str (t ).replace ("-" ,"_" ) 141if sval in ("per_element" ,"per_head" ): 142return sval 143raise ValueError (f"Laguna: unrecognised attention gate type { t !r } " ) 144 145if gating_types : 146assert len (gating_types )== n_layer , ( 147f"gating_types length { len ( gating_types ) } != num_hidden_layers { n_layer } " ) 148types = [_norm (t )for t in gating_types ] 149elif isinstance (gating ,str ): 150types = [_norm (gating )]* n_layer 151elif gating is True : 152types = ["per_head" ]* n_layer 153else : 154raise ValueError ( 155f"Laguna: cannot determine attention gate type " 156f"(gating= { gating !r } , gating_types= { gating_types !r } )" ) 157 158if any (t == "per_element" for t in types )and not ( 159isinstance (gating ,str )and _norm (gating )== "per_element" ): 160raise ValueError ( 161f"Laguna config declares a per-element attention gate but " 162f"`gating`= { gating !r } is not the string \"per-element\". Runtimes that " 163f"read `gating` (vLLM, transformers) will mis-handle this checkpoint as " 164f"per-head. Set gating=\"per-element\" in the source config." ) 165 166self ._gate_types = types 167return types 168 169# --- tensor handling ----------------------------------------------------- 170 171def modify_tensors (self ,data_torch :Tensor ,name :str ,bid :int | None )-> Iterable [tuple [str ,Tensor ]]: 172# Per-expert MoE weights: model.layers.{bid}.mlp.experts.{xid}.{w}.weight. 173# Only the NUMBERED per-expert weights are stacked; the router bias 174# (mlp.experts.e_score_correction_bias) takes the normal mapping path. 175if re .search (r"mlp\.experts\.\d+\." ,name ): 176n_experts = self .find_hparam (["num_local_experts" ,"num_experts" ]) 177assert bid is not None 178if self ._experts is None : 179self ._experts = [{}for _ in range (self .block_count )] 180self ._experts [bid ][name ]= data_torch 181needed = [f"model.layers. { bid } .mlp.experts. { x } . { w } .weight" 182for x in range (n_experts )for w in ("gate_proj" ,"up_proj" ,"down_proj" )] 183if all (e in self ._experts [bid ]for e in needed ): 184for w_name in ["gate_proj" ,"up_proj" ,"down_proj" ]: 185datas = [self ._experts [bid ][f"model.layers. { bid } .mlp.experts. { x } . { w_name } .weight" ] 186for x in range (n_experts )] 187stacked = torch .stack (datas ,dim = 0 ) 188merged = f"model.layers. { bid } .mlp.experts. { w_name } .weight" 189yield from TextModel .modify_tensors (self ,stacked ,merged ,bid ) 190self ._experts [bid ].clear () 191return 192return 193# Cross-check the gate projection width against the declared gate type; 194# a mismatch means the weights and config disagree -> fail, do not guess. 195if bid is not None and name .endswith ("self_attn.g_proj.weight" ): 196heads = (self .hparams .get ("num_attention_heads_per_layer" ) 197or [self .hparams ["num_attention_heads" ]]* self .hparams ["num_hidden_layers" ]) 198n_head = heads [bid ] 199head_dim = self .hparams ["head_dim" ] 200gate_type = self ._attn_gate_types ()[bid ] 201expected = n_head * head_dim if gate_type == "per_element" else n_head 202out_features = int (data_torch .shape [0 ]) 203if out_features != expected : 204raise ValueError ( 205f"Laguna layer { bid } : g_proj output width { out_features } contradicts the " 206f"declared { gate_type } gate (expected { expected } ); weights and config disagree." ) 207 208yield from TextModel .modify_tensors (self ,data_torch ,name ,bid )