Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions convert_hf_to_gguf.py
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,9 @@ def get_vocab_base_pre(self, tokenizer) -> str:
if chkhsh == "0ef9807a4087ebef797fc749390439009c3b9eda9ad1a097abbe738f486c01e5":
# ref: https://huggingface.co/meta-llama/Meta-Llama-3-8B
res = "llama-bpe"
if chkhsh == "972da7b59cec44d1f0a490a86c96df53859e486e481563e5dddac155013d87ac":
# ref: https://huggingface.co/poolside/Laguna-XS.2
res = "laguna"
if chkhsh == "049ecf7629871e3041641907f3de7c733e4dbfdc736f57d882ba0b0845599754":
# ref: https://huggingface.co/deepseek-ai/deepseek-llm-7b-base
res = "deepseek-llm"
Expand Down Expand Up @@ -5041,6 +5044,138 @@ def prepare_tensors(self):
raise ValueError(f"Unprocessed experts: {experts}")


@Model.register("LagunaForCausalLM")
class LagunaModel(Model):
model_arch = gguf.MODEL_ARCH.LAGUNA

_experts: list[dict[str, Tensor]] | None = None

def set_gguf_parameters(self):
hparams = self.hparams
arch = gguf.MODEL_ARCH_NAMES[self.model_arch]
n_layers = int(hparams["num_hidden_layers"])
n_head_base = int(hparams["num_attention_heads"])
n_kv_base = int(hparams.get("num_key_value_heads", n_head_base))
head_dim = int(hparams.get("head_dim", hparams["hidden_size"] // n_head_base))

heads_per_layer = hparams.get("num_attention_heads_per_layer")
kv_per_layer = hparams.get("num_key_value_heads_per_layer")

head_arr: list[int] = []
kv_arr: list[int] = []
for i in range(n_layers):
head_arr.append(int(heads_per_layer[i]) if heads_per_layer is not None else n_head_base)
kv_arr.append(int(kv_per_layer[i]) if kv_per_layer is not None else n_kv_base)

rope_params = hparams.get("rope_parameters", {})
full_rope = rope_params.get("full_attention", rope_params)
swa_rope = rope_params.get("sliding_attention", {})

self.gguf_writer.add_context_length(int(hparams["max_position_embeddings"]))
self.gguf_writer.add_embedding_length(int(hparams["hidden_size"]))
self.gguf_writer.add_block_count(n_layers)
self.gguf_writer.add_feed_forward_length(int(hparams["intermediate_size"]))
self.gguf_writer.add_head_count(head_arr)
if all(n_kv == kv_arr[0] for n_kv in kv_arr):
self.gguf_writer.add_head_count_kv(kv_arr[0])
else:
self.gguf_writer.add_head_count_kv(kv_arr)
self.gguf_writer.add_key_length(head_dim)
self.gguf_writer.add_value_length(head_dim)
self.gguf_writer.add_layer_norm_rms_eps(float(hparams["rms_norm_eps"]))
self.gguf_writer.add_file_type(self.ftype)

self.gguf_writer.add_sliding_window(int(hparams["sliding_window"]))
self.gguf_writer.add_rope_dimension_count(head_dim // 2)
self.gguf_writer.add_uint32(f"{arch}.rope.dimension_count_swa", head_dim)
self.gguf_writer.add_rope_freq_base(float(full_rope.get("rope_theta", 500000.0)))
self.gguf_writer.add_float32(f"{arch}.rope.freq_base_swa", float(swa_rope.get("rope_theta", 10000.0)))
if full_rope.get("rope_type") == "yarn":
self.gguf_writer.add_rope_scaling_type(gguf.RopeScalingType.YARN)
self.gguf_writer.add_rope_scaling_factor(float(full_rope.get("factor", 1.0)))
self.gguf_writer.add_rope_scaling_orig_ctx_len(int(full_rope.get(
"original_max_position_embeddings",
rope_params.get("original_max_position_embeddings", hparams["max_position_embeddings"]),
)))
self.gguf_writer.add_rope_scaling_yarn_ext_factor(float(full_rope.get("factor", 1.0)))
self.gguf_writer.add_rope_scaling_yarn_attn_factor(float(full_rope.get("attention_factor", 1.0)))
self.gguf_writer.add_rope_scaling_yarn_beta_fast(float(full_rope.get("beta_fast", 32.0)))
self.gguf_writer.add_rope_scaling_yarn_beta_slow(float(full_rope.get("beta_slow", 1.0)))

self.gguf_writer.add_expert_count(int(hparams["num_experts"]))
self.gguf_writer.add_expert_used_count(int(hparams["num_experts_per_tok"]))
self.gguf_writer.add_expert_feed_forward_length(int(hparams["moe_intermediate_size"]))
if (shared_dim := hparams.get("shared_expert_intermediate_size")) is not None and int(shared_dim) > 0:
self.gguf_writer.add_expert_shared_feed_forward_length(int(shared_dim))
if (routing_scale := hparams.get("moe_routed_scaling_factor")) is not None:
self.gguf_writer.add_expert_weights_scale(float(routing_scale))
self.gguf_writer.add_expert_weights_norm(True)
self.gguf_writer.add_expert_gating_func(gguf.ExpertGatingFuncType.SIGMOID)

leading_dense = 0
for mlp_type in hparams.get("mlp_layer_types", []):
if mlp_type != "dense":
break
leading_dense += 1
self.gguf_writer.add_uint32(f"{arch}.leading_dense_block_count", leading_dense)

if hparams.get("moe_apply_router_weight_on_input", False):
raise ValueError("moe_apply_router_weight_on_input=True is not supported for Laguna")

def set_vocab(self) -> None:
super().set_vocab()
if isinstance(eos_token_id := self.hparams.get("eos_token_id"), list) and len(eos_token_id) > 1:
# Poolside uses token 24 (</assistant>) as a turn boundary.
self.gguf_writer.add_eot_token_id(int(eos_token_id[1]))
template_file = self.dir_model / "chat_template.jinja"
if template_file.is_file():
self.gguf_writer.add_chat_template(template_file.read_text(encoding="utf-8"))

def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
if bid is not None and name in (
f"model.layers.{bid}.mlp.experts.e_score_correction_bias",
f"model.layers.{bid}.mlp.experts.e_score_correction",
):
# The C++ loader asks for this tensor through the ".bias" suffix.
# Keep the Laguna converter aligned with existing community GGUFs.
yield f"blk.{bid}.exp_probs_b.bias", data_torch
return

if name.endswith(".self_attn.g_proj.weight"):
# HF stores the head-wise attention gate with a singleton dimension.
data_torch = data_torch.squeeze().contiguous()

if bid is not None and re.match(r"model\.layers\.\d+\.mlp\.experts\.\d+\.(gate_proj|up_proj|down_proj)\.weight$", name):
n_experts = int(self.find_hparam(["num_experts"]))
if self._experts is None:
self._experts = [{} for _ in range(self.block_count)]

self._experts[bid][name] = data_torch
if len(self._experts[bid]) < n_experts * 3:
return

for w_name in ("down_proj", "gate_proj", "up_proj"):
datas: list[Tensor] = []
for xid in range(n_experts):
ename = f"model.layers.{bid}.mlp.experts.{xid}.{w_name}.weight"
datas.append(self._experts[bid][ename])
del self._experts[bid][ename]

merged = torch.stack(datas, dim=0)
merged_name = f"model.layers.{bid}.mlp.experts.{w_name}.weight"
yield from super().modify_tensors(merged, merged_name, bid)
return

yield from super().modify_tensors(data_torch, name, bid)

def prepare_tensors(self):
super().prepare_tensors()
if self._experts is not None:
experts = [k for d in self._experts for k in d.keys()]
if experts:
raise ValueError(f"Unprocessed experts: {experts}")


###### CONVERSION LOGIC ######


Expand Down
37 changes: 37 additions & 0 deletions gguf-py/gguf/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,17 @@ class Attention:
REL_BUCKETS_COUNT = "{arch}.attention.relative_buckets_count"
SLIDING_WINDOW = "{arch}.attention.sliding_window"
SLIDING_WINDOW_PATTERN = "{arch}.attention.sliding_window_pattern"
SHARED_KV_LAYERS = "{arch}.attention.shared_kv_layers"
KEY_LENGTH_SWA = "{arch}.attention.key_length_swa"
VALUE_LENGTH_SWA = "{arch}.attention.value_length_swa"
OUTPUT_SCALE = "{arch}.attention.output_scale"
TEMPERATURE_LENGTH = "{arch}.attention.temperature_length"

class Rope:
DIMENSION_COUNT = "{arch}.rope.dimension_count"
DIMENSION_COUNT_SWA = "{arch}.rope.dimension_count_swa"
DIMENSION_COUNT_PER_LAYER = "{arch}.rope.dimension_count_per_layer"
DIMENSION_SECTIONS = "{arch}.rope.dimension_sections"
FREQ_BASE = "{arch}.rope.freq_base"
FREQ_BASE_SWA = "{arch}.rope.freq_base_swa"
SCALING_TYPE = "{arch}.rope.scaling.type"
Expand Down Expand Up @@ -162,6 +168,7 @@ class Tokenizer:
MASK_ID = "tokenizer.ggml.mask_token_id"
ADD_BOS = "tokenizer.ggml.add_bos_token"
ADD_EOS = "tokenizer.ggml.add_eos_token"
ADD_SEP = "tokenizer.ggml.add_sep_token"
ADD_PREFIX = "tokenizer.ggml.add_space_prefix"
REMOVE_EXTRA_WS = "tokenizer.ggml.remove_extra_whitespaces"
PRECOMPILED_CHARSMAP = "tokenizer.ggml.precompiled_charsmap"
Expand Down Expand Up @@ -262,6 +269,7 @@ class MODEL_ARCH(IntEnum):
MINIMAXM2 = auto()
SMOLLM3 = auto()
SEED_OSS = auto()
LAGUNA = auto()

class MODEL_TENSOR(IntEnum):
TOKEN_EMBD = auto()
Expand All @@ -282,6 +290,7 @@ class MODEL_TENSOR(IntEnum):
ATTN_NORM_2 = auto()
ATTN_OUT_NORM = auto()
ATTN_POST_NORM = auto()
ATTN_GATE = auto()
ATTN_ROT_EMBD = auto()
FFN_GATE_INP = auto()
FFN_GATE_INP_SHEXP = auto()
Expand Down Expand Up @@ -429,6 +438,7 @@ class MODEL_TENSOR(IntEnum):
MODEL_ARCH.MINIMAXM2: "minimax-m2",
MODEL_ARCH.SMOLLM3: "smollm3",
MODEL_ARCH.SEED_OSS: "seed_oss",
MODEL_ARCH.LAGUNA: "laguna",
}

TENSOR_NAMES: dict[MODEL_TENSOR, str] = {
Expand All @@ -453,6 +463,7 @@ class MODEL_TENSOR(IntEnum):
MODEL_TENSOR.ATTN_K_NORM: "blk.{bid}.attn_k_norm",
MODEL_TENSOR.ATTN_OUT_NORM: "blk.{bid}.attn_output_norm",
MODEL_TENSOR.ATTN_POST_NORM: "blk.{bid}.post_attention_norm",
MODEL_TENSOR.ATTN_GATE: "blk.{bid}.attn_gate",
MODEL_TENSOR.FFN_GATE_INP: "blk.{bid}.ffn_gate_inp",
MODEL_TENSOR.FFN_GATE_INP_SHEXP: "blk.{bid}.ffn_gate_inp_shexp",
MODEL_TENSOR.FFN_NORM: "blk.{bid}.ffn_norm",
Expand Down Expand Up @@ -1489,6 +1500,32 @@ class MODEL_TENSOR(IntEnum):
MODEL_TENSOR.OUTPUT_NORM,
MODEL_TENSOR.OUTPUT,
],
MODEL_ARCH.LAGUNA: [
MODEL_TENSOR.TOKEN_EMBD,
MODEL_TENSOR.OUTPUT_NORM,
MODEL_TENSOR.OUTPUT,
MODEL_TENSOR.ROPE_FREQS,
MODEL_TENSOR.ATTN_NORM,
MODEL_TENSOR.ATTN_Q,
MODEL_TENSOR.ATTN_Q_NORM,
MODEL_TENSOR.ATTN_K,
MODEL_TENSOR.ATTN_K_NORM,
MODEL_TENSOR.ATTN_V,
MODEL_TENSOR.ATTN_GATE,
MODEL_TENSOR.ATTN_OUT,
MODEL_TENSOR.FFN_NORM,
MODEL_TENSOR.FFN_GATE,
MODEL_TENSOR.FFN_DOWN,
MODEL_TENSOR.FFN_UP,
MODEL_TENSOR.FFN_GATE_INP,
MODEL_TENSOR.FFN_GATE_EXP,
MODEL_TENSOR.FFN_DOWN_EXP,
MODEL_TENSOR.FFN_UP_EXP,
MODEL_TENSOR.FFN_GATE_SHEXP,
MODEL_TENSOR.FFN_DOWN_SHEXP,
MODEL_TENSOR.FFN_UP_SHEXP,
MODEL_TENSOR.FFN_EXP_PROBS_B,
],
# TODO
}

Expand Down
6 changes: 6 additions & 0 deletions gguf-py/gguf/tensor_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,10 @@ class TensorNameMap:
"model.layers.{bid}.post_attention_layernorm", # gemma2
),

MODEL_TENSOR.ATTN_GATE: (
"model.layers.{bid}.self_attn.g_proj", # laguna
),

# Rotary embeddings
MODEL_TENSOR.ATTN_ROT_EMBD: (
"model.layers.{bid}.self_attn.rotary_emb.inv_freq", # llama-hf
Expand Down Expand Up @@ -280,6 +284,8 @@ class TensorNameMap:
"model.layers.{bid}.mlp.moe_statics.e_score_correction", # ernie4.5-moe
"model.layers.{bid}.mlp.gate.expert_bias", # bailingmoe2
"model.layers.{bid}.block_sparse_moe.e_score_correction", # minimax-m2
"model.layers.{bid}.mlp.experts.e_score_correction_bias", # laguna
"model.layers.{bid}.mlp.experts.e_score_correction", # laguna
),

# Feed-forward up
Expand Down
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ add_library(llama
graphs/build_mpt.cpp
graphs/build_stablelm.cpp
graphs/build_seedoss.cpp
graphs/build_laguna.cpp
graphs/build_step35.cpp
graphs/build_qwen.cpp
graphs/build_qwen2.cpp
Expand Down
Loading