Skip to content
Open
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
2 changes: 2 additions & 0 deletions conversion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@
"GemmaForCausalLM": "gemma",
"Glm4ForCausalLM": "glm",
"Glm4MoeForCausalLM": "glm",
"Glm5NextForCausalLM": "glm5next",
"Glm5NextForConditionalGeneration": "glm5next",
"Glm4MoeLiteForCausalLM": "glm",
"Glm4vForConditionalGeneration": "glm",
"Glm4vMoeForConditionalGeneration": "glm",
Expand Down
106 changes: 106 additions & 0 deletions conversion/glm5next.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
from __future__ import annotations

import re
from typing import Iterable

import torch
from torch import Tensor

import gguf

from .base import ModelBase
from .glm import GlmMoeDsaModel


@ModelBase.register("Glm5NextForConditionalGeneration", "Glm5NextForCausalLM")
@ModelBase.example("zai-org/GLM-5.3-Flash")
class Glm5NextModel(GlmMoeDsaModel):
"""GLM-5.3-Flash.

Trunk that alternates KDA linear attention (34 layers) with MLA + DSA sparse
attention (11 layers), wrapped in hyper-connection streams. The pieces are
already in tree: the KDA tensors follow kimi-linear, the hyper-connection and
k-pool compressor tensors follow deepseek4, and the MLA/MoE/NextN half is
inherited from GLM-5.2 (GlmMoeDsaModel).
"""

model_arch = gguf.MODEL_ARCH.GLM5NEXT

# Tensors that carry no per-layer index and are named differently from the
# generic mapping, resolved by suffix (same approach as DeepseekV4Model).
_direct_map = {
"hc_attn_fn": (gguf.MODEL_TENSOR.HC_ATTN_FN, ""),
"hc_attn_base": (gguf.MODEL_TENSOR.HC_ATTN_BASE, ""),
"hc_attn_scale": (gguf.MODEL_TENSOR.HC_ATTN_SCALE, ""),
"hc_ffn_fn": (gguf.MODEL_TENSOR.HC_FFN_FN, ""),
"hc_ffn_base": (gguf.MODEL_TENSOR.HC_FFN_BASE, ""),
"hc_ffn_scale": (gguf.MODEL_TENSOR.HC_FFN_SCALE, ""),
"self_attn.indexer.index_kpool_compress_ape":
(gguf.MODEL_TENSOR.INDEXER_COMPRESSOR_APE, ""),
"self_attn.indexer.index_kpool_compress_gate":
(gguf.MODEL_TENSOR.INDEXER_COMPRESSOR_WGATE, ""),
}

def index_tensors(self, remote_hf_model_id: str | None = None):
# TextModel lifts text_config to the root, but only after this runs -
# and the parent already needs num_hidden_layers from it here.
# Skip None values: AutoConfig.to_dict() materialises keys that the JSON
# omits, so text_config carries architectures=None and would clobber the
# valid top-level value.
if "text_config" in self.hparams:
self.hparams = {
**self.hparams,
**{k: v for k, v in self.hparams["text_config"].items() if v is not None},
}
return super().index_tensors(remote_hf_model_id=remote_hf_model_id)

@classmethod
def filter_tensors(cls, item):
name = item[0]
# text-only for now: drop the vision tower
if name.startswith("model.visual.") or name.startswith("visual."):
return None
return super().filter_tensors(item)

def set_gguf_parameters(self):
super().set_gguf_parameters()
hparams = self.hparams

# hyper-connections (mHC): identical formulation to DeepSeek-V4, so the
# existing sinkhorn graph applies unchanged.
self.gguf_writer.add_hyper_connection_count(hparams["hc_mult"])
self.gguf_writer.add_hyper_connection_sinkhorn_iterations(hparams["hc_sinkhorn_iters"])
self.gguf_writer.add_hyper_connection_epsilon(hparams["hc_eps"])

# KDA linear attention
linear = hparams["linear_attn_config"]
self.gguf_writer.add_ssm_conv_kernel(linear["short_conv_kernel_size"])
self.gguf_writer.add_ssm_inner_size(linear["num_heads"] * linear["head_dim"])
self.gguf_writer.add_ssm_state_size(linear["head_dim"])
self.gguf_writer.add_ssm_group_count(linear["num_heads"])

# k-pool compression inside the DSA indexer
self.gguf_writer.add_indexer_block_size(hparams["index_kpool"])

# clamped SwiGLU
if (limit := hparams.get("swiglu_limit")) is not None:
self.gguf_writer.add_swiglu_clamp_exp([limit] * self.block_count)
self.gguf_writer.add_swiglu_clamp_shexp([limit] * self.block_count)

def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
# the checkpoint wraps the trunk for the multimodal head
name = re.sub(r"^model\.language_model\.", "model.", name)

# KDA decay conventions, same as conversion/kimi_linear.py: the graph
# expects ssm_a to already hold -exp(A_log), and the time-step bias to
# be named like a bias so it is not loaded as a MUL_MAT weight.
if name.endswith(".A_log"):
data_torch = -torch.exp(data_torch.float())
if name.endswith(".dt_bias"):
name = name.rpartition(".dt_bias")[0] + ".dt_proj.bias"

for suffix, (tensor, ext) in self._direct_map.items():
if name.endswith(suffix) and bid is not None:
return [(self.format_tensor_name(tensor, bid) + ext, data_torch)]

return super().modify_tensors(data_torch, name, bid)
65 changes: 65 additions & 0 deletions gguf-py/gguf/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,7 @@ class MODEL_ARCH(IntEnum):
GLM4 = auto()
GLM4_MOE = auto()
GLM_DSA = auto()
GLM5NEXT = auto()
BITNET = auto()
T5 = auto()
T5ENCODER = auto()
Expand Down Expand Up @@ -1311,6 +1312,7 @@ class MODEL_TENSOR(IntEnum):
MODEL_ARCH.GLM4: "glm4",
MODEL_ARCH.GLM4_MOE: "glm4moe",
MODEL_ARCH.GLM_DSA: "glm-dsa",
MODEL_ARCH.GLM5NEXT: "glm5next",
MODEL_ARCH.BITNET: "bitnet",
MODEL_ARCH.T5: "t5",
MODEL_ARCH.T5ENCODER: "t5encoder",
Expand Down Expand Up @@ -4003,6 +4005,69 @@ class MODEL_TENSOR(IntEnum):
MODEL_TENSOR.NEXTN_SHARED_HEAD_HEAD,
MODEL_TENSOR.NEXTN_SHARED_HEAD_NORM,
],
MODEL_ARCH.GLM5NEXT: [
MODEL_TENSOR.TOKEN_EMBD,
MODEL_TENSOR.OUTPUT_NORM,
MODEL_TENSOR.OUTPUT,
MODEL_TENSOR.ATTN_NORM,
MODEL_TENSOR.ATTN_Q,
MODEL_TENSOR.ATTN_Q_A,
MODEL_TENSOR.ATTN_Q_B,
MODEL_TENSOR.ATTN_KV_A_MQA,
MODEL_TENSOR.ATTN_KV_B,
MODEL_TENSOR.ATTN_K_B,
MODEL_TENSOR.ATTN_V_B,
MODEL_TENSOR.ATTN_Q_A_NORM,
MODEL_TENSOR.ATTN_KV_A_NORM,
MODEL_TENSOR.ATTN_OUT,
MODEL_TENSOR.FFN_GATE_INP,
MODEL_TENSOR.FFN_NORM,
MODEL_TENSOR.FFN_GATE,
MODEL_TENSOR.FFN_DOWN,
MODEL_TENSOR.FFN_UP,
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,
MODEL_TENSOR.INDEXER_K_NORM,
MODEL_TENSOR.INDEXER_PROJ,
MODEL_TENSOR.INDEXER_ATTN_K,
MODEL_TENSOR.INDEXER_ATTN_Q_B,
# NextN/MTP tensors - preserved but unused
MODEL_TENSOR.NEXTN_EH_PROJ,
MODEL_TENSOR.NEXTN_EMBED_TOKENS,
MODEL_TENSOR.NEXTN_ENORM,
MODEL_TENSOR.NEXTN_HNORM,
MODEL_TENSOR.NEXTN_SHARED_HEAD_HEAD,
MODEL_TENSOR.NEXTN_SHARED_HEAD_NORM,
# --- KDA linear attention (34 of 45 layers), see kimi-linear ---
MODEL_TENSOR.ATTN_K,
MODEL_TENSOR.ATTN_V,
MODEL_TENSOR.SSM_CONV1D_Q,
MODEL_TENSOR.SSM_CONV1D_K,
MODEL_TENSOR.SSM_CONV1D_V,
MODEL_TENSOR.SSM_F_A,
MODEL_TENSOR.SSM_F_B,
MODEL_TENSOR.SSM_G_A,
MODEL_TENSOR.SSM_G_B,
MODEL_TENSOR.SSM_BETA,
MODEL_TENSOR.SSM_A,
MODEL_TENSOR.SSM_DT,
MODEL_TENSOR.SSM_NORM,
# --- hyper-connections (mHC, Sinkhorn), see deepseek4 ---
MODEL_TENSOR.HC_ATTN_FN,
MODEL_TENSOR.HC_ATTN_BASE,
MODEL_TENSOR.HC_ATTN_SCALE,
MODEL_TENSOR.HC_FFN_FN,
MODEL_TENSOR.HC_FFN_BASE,
MODEL_TENSOR.HC_FFN_SCALE,
# --- indexer: k-pool compression ---
MODEL_TENSOR.INDEXER_COMPRESSOR_APE,
MODEL_TENSOR.INDEXER_COMPRESSOR_WGATE,
],
MODEL_ARCH.BITNET: [
MODEL_TENSOR.ATTN_Q,
MODEL_TENSOR.ATTN_K,
Expand Down
4 changes: 4 additions & 0 deletions src/llama-arch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ static const std::map<llm_arch, const char *> LLM_ARCH_NAMES = {
{ LLM_ARCH_GLM4, "glm4" },
{ LLM_ARCH_GLM4_MOE, "glm4moe" },
{ LLM_ARCH_GLM_DSA, "glm-dsa" },
{ LLM_ARCH_GLM5NEXT, "glm5next" },
{ LLM_ARCH_BITNET, "bitnet" },
{ LLM_ARCH_T5, "t5" },
{ LLM_ARCH_T5ENCODER, "t5encoder" },
Expand Down Expand Up @@ -282,6 +283,7 @@ static const std::map<llm_kv, const char *> LLM_KV_NAMES = {
{ LLM_KV_ATTENTION_INDEXER_KEY_LENGTH, "%s.attention.indexer.key_length" },
{ LLM_KV_ATTENTION_INDEXER_TOP_K, "%s.attention.indexer.top_k" },
{ LLM_KV_ATTENTION_INDEXER_BLOCK_SIZE, "%s.attention.indexer.block_size" },
{ LLM_KV_ATTENTION_INDEXER_KPOOL, "%s.attention.indexer.kpool" },
{ LLM_KV_ATTENTION_INDEXER_LOCAL_BLOCKS, "%s.attention.indexer.local_blocks" },
{ LLM_KV_ATTENTION_INDEXER_TYPES, "%s.attention.indexer.types" },
{ LLM_KV_ATTENTION_OUTPUT_GROUP_COUNT, "%s.attention.output_group_count" },
Expand Down Expand Up @@ -1080,6 +1082,7 @@ bool llm_arch_is_hybrid(const llm_arch & arch) {
case LLM_ARCH_QWEN4EXP:
case LLM_ARCH_DEEPSEEK4:
case LLM_ARCH_MINIMAX_01:
case LLM_ARCH_GLM5NEXT:
return true;
default:
return false;
Expand Down Expand Up @@ -1144,6 +1147,7 @@ bool llm_arch_supports_sm_tensor(const llm_arch & arch) {
case LLM_ARCH_KIMI_LINEAR:
case LLM_ARCH_BAILINGMOE3:
case LLM_ARCH_KIMI_K3:
case LLM_ARCH_GLM5NEXT:
case LLM_ARCH_QWEN3TTS:
case LLM_ARCH_QWEN4EXP: // TODO: fix test-llama-archs
return false;
Expand Down
2 changes: 2 additions & 0 deletions src/llama-arch.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ enum llm_arch {
LLM_ARCH_GLM4,
LLM_ARCH_GLM4_MOE,
LLM_ARCH_GLM_DSA,
LLM_ARCH_GLM5NEXT,
LLM_ARCH_BITNET,
LLM_ARCH_T5,
LLM_ARCH_T5ENCODER,
Expand Down Expand Up @@ -287,6 +288,7 @@ enum llm_kv {
LLM_KV_ATTENTION_INDEXER_KEY_LENGTH,
LLM_KV_ATTENTION_INDEXER_TOP_K,
LLM_KV_ATTENTION_INDEXER_BLOCK_SIZE,
LLM_KV_ATTENTION_INDEXER_KPOOL,
LLM_KV_ATTENTION_INDEXER_LOCAL_BLOCKS,
LLM_KV_ATTENTION_INDEXER_TYPES,
LLM_KV_ATTENTION_OUTPUT_GROUP_COUNT,
Expand Down
1 change: 1 addition & 0 deletions src/llama-context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2314,6 +2314,7 @@ uint32_t llama_context::graph_max_nodes(uint32_t n_tokens) const {
model.arch == LLM_ARCH_QWEN35MOE ||
model.arch == LLM_ARCH_QWEN4EXP ||
model.arch == LLM_ARCH_DEEPSEEK4 ||
model.arch == LLM_ARCH_GLM5NEXT ||
(model.arch == LLM_ARCH_DFLASH && model.hparams.dsv4_hc_mult > 0) ||
model.arch == LLM_ARCH_NANBEIGE ||
model.arch == LLM_ARCH_MINIMAX_01 ||
Expand Down
Loading