Skip to content

Commit 32946fc

Browse files
authored
Merge branch 'ggml-org:master' into master
2 parents 14b23b3 + ced765b commit 32946fc

24 files changed

Lines changed: 594 additions & 50 deletions

‎common/chat-parser.cpp‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1395,6 +1395,14 @@ static void common_chat_parse_seed_oss(common_chat_msg_parser & builder) {
13951395
builder.consume_reasoning_with_xml_tool_calls(form, "<seed:think>", "</seed:think>");
13961396
}
13971397

1398+
static void common_chat_parse_solar_open(common_chat_msg_parser & builder) {
1399+
builder.try_parse_reasoning("<|think|>", "<|end|><|begin|>assistant<|content|>");
1400+
1401+
// TODO: Tool calling
1402+
1403+
builder.add_content(builder.consume_rest());
1404+
}
1405+
13981406
static void common_chat_parse_content_only(common_chat_msg_parser & builder) {
13991407
builder.try_parse_reasoning("<think>", "</think>");
14001408
builder.add_content(builder.consume_rest());
@@ -1479,6 +1487,9 @@ static void common_chat_parse(common_chat_msg_parser & builder) {
14791487
case COMMON_CHAT_FORMAT_XIAOMI_MIMO:
14801488
common_chat_parse_xiaomi_mimo(builder);
14811489
break;
1490+
case COMMON_CHAT_FORMAT_SOLAR_OPEN:
1491+
common_chat_parse_solar_open(builder);
1492+
break;
14821493
default:
14831494
throw std::runtime_error(std::string("Unsupported format: ") + common_chat_format_name(builder.syntax().format));
14841495
}

‎common/chat.cpp‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,7 @@ const char * common_chat_format_name(common_chat_format format) {
669669
case COMMON_CHAT_FORMAT_QWEN3_CODER_XML: return "Qwen3 Coder";
670670
case COMMON_CHAT_FORMAT_APRIEL_1_5: return "Apriel 1.5";
671671
case COMMON_CHAT_FORMAT_XIAOMI_MIMO: return "Xiaomi MiMo";
672+
case COMMON_CHAT_FORMAT_SOLAR_OPEN: return "Solar Open";
672673
case COMMON_CHAT_FORMAT_PEG_SIMPLE: return "peg-simple";
673674
case COMMON_CHAT_FORMAT_PEG_NATIVE: return "peg-native";
674675
case COMMON_CHAT_FORMAT_PEG_CONSTRUCTED: return "peg-constructed";
@@ -2517,6 +2518,27 @@ static common_chat_params common_chat_params_init_granite(const common_chat_temp
25172518
return data;
25182519
}
25192520

2521+
static common_chat_params common_chat_params_init_solar_open(const common_chat_template & tmpl, const struct templates_params & inputs) {
2522+
common_chat_params data;
2523+
2524+
// TODO: Reasoning effort
2525+
json additional_context = {};
2526+
2527+
data.prompt = apply(tmpl, inputs, std::nullopt, std::nullopt, additional_context);
2528+
data.format = COMMON_CHAT_FORMAT_SOLAR_OPEN;
2529+
2530+
data.preserved_tokens = {
2531+
"<|think|>",
2532+
"<|content|>",
2533+
"<|begin|>",
2534+
"<|end|>",
2535+
};
2536+
2537+
// TODO: Tool calling
2538+
2539+
return data;
2540+
}
2541+
25202542
static common_chat_params common_chat_params_init_without_tools(const common_chat_template & tmpl, const struct templates_params & inputs) {
25212543
common_chat_params data;
25222544
data.prompt = apply(tmpl, inputs);
@@ -2780,6 +2802,13 @@ static common_chat_params common_chat_templates_apply_jinja(
27802802
return common_chat_params_init_magistral(tmpl, params);
27812803
}
27822804

2805+
// Solar Open
2806+
if (src.find("<|tool_response:begin|>") != std::string::npos &&
2807+
src.find("<|tool_response:name|>") != std::string::npos &&
2808+
src.find("<|tool_response:result|>") != std::string::npos) {
2809+
return common_chat_params_init_solar_open(tmpl, params);
2810+
}
2811+
27832812
// Plain handler (no tools)
27842813
if (params.tools.is_null() || inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_NONE) {
27852814
return common_chat_params_init_without_tools(tmpl, params);

‎common/chat.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ enum common_chat_format {
124124
COMMON_CHAT_FORMAT_QWEN3_CODER_XML,
125125
COMMON_CHAT_FORMAT_APRIEL_1_5,
126126
COMMON_CHAT_FORMAT_XIAOMI_MIMO,
127+
COMMON_CHAT_FORMAT_SOLAR_OPEN,
127128

128129
// These are intended to be parsed by the PEG parser
129130
COMMON_CHAT_FORMAT_PEG_SIMPLE,

‎convert_hf_to_gguf.py‎

Lines changed: 123 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,6 +1062,9 @@ def get_vocab_base_pre(self, tokenizer) -> str:
10621062
if chkhsh == "66b8d4e19ab16c3bfd89bce5d785fb7e0155e8648708a1f42077cb9fe002c273":
10631063
# ref: https://huggingface.co/alvarobartt/grok-2-tokenizer
10641064
res = "grok-2"
1065+
if chkhsh == "b3d1dd861f1d4c5c0d2569ce36baf3f90fe8a102db3de50dd71ff860d91be3df":
1066+
# ref: https://huggingface.co/aari1995/German_Semantic_V3
1067+
res = "jina-v2-de"
10651068
if chkhsh == "0ef9807a4087ebef797fc749390439009c3b9eda9ad1a097abbe738f486c01e5":
10661069
# ref: https://huggingface.co/meta-llama/Meta-Llama-3-8B
10671070
res = "llama-bpe"
@@ -1230,6 +1233,12 @@ def get_vocab_base_pre(self, tokenizer) -> str:
12301233
if chkhsh == "4a2e2abae11ca2b86d570fc5b44be4d5eb5e72cc8f22dd136a94b37da83ab665":
12311234
# ref: https://huggingface.co/KORMo-Team/KORMo-tokenizer
12321235
res = "kormo"
1236+
if chkhsh == "9d70134b369a70e5735009b6de918f7581b5211f7c074d1f89f753aea8248af1":
1237+
# ref: https://huggingface.co/tencent/Youtu-LLM-2B
1238+
res = "youtu"
1239+
if chkhsh == "16389f0a1f51ee53e562ffd51c371dc508639ab0e4261502071836e50e223e91":
1240+
# ref: https://huggingface.co/upstage/Solar-Open-100B
1241+
res = "solar-open"
12331242

12341243
if res is None:
12351244
logger.warning("\n")
@@ -2486,6 +2495,7 @@ def prepare_tensors(self):
24862495
"VLlama3ForCausalLM",
24872496
"LlavaForConditionalGeneration",
24882497
"VoxtralForConditionalGeneration",
2498+
"IQuestCoderForCausalLM",
24892499
"LlamaModel")
24902500
class LlamaModel(TextModel):
24912501
model_arch = gguf.MODEL_ARCH.LLAMA
@@ -5284,13 +5294,14 @@ def set_vocab(self):
52845294
self.gguf_writer.add_token_type_count(self.hparams.get("type_vocab_size", 1))
52855295

52865296
# convert to phantom space vocab
5287-
def phantom(tok):
5288-
if tok.startswith("[") and tok.endswith("]"):
5297+
def phantom(tok, toktype):
5298+
if toktype == gguf.TokenType.CONTROL:
52895299
return tok
52905300
if tok.startswith("##"):
52915301
return tok[2:]
52925302
return "\u2581" + tok
5293-
tokens = list(map(phantom, tokens))
5303+
assert len(tokens) == len(toktypes)
5304+
tokens = list(map(phantom, tokens, toktypes))
52945305

52955306
# add vocab to gguf
52965307
self.gguf_writer.add_tokenizer_model("bert")
@@ -7181,6 +7192,7 @@ def prepare_tensors(self):
71817192
"DeepseekV2ForCausalLM",
71827193
"DeepseekV3ForCausalLM",
71837194
"KimiVLForConditionalGeneration",
7195+
"YoutuForCausalLM",
71847196
)
71857197
class DeepseekV2Model(TextModel):
71867198
model_arch = gguf.MODEL_ARCH.DEEPSEEK2
@@ -7247,7 +7259,15 @@ def set_gguf_parameters(self):
72477259
super().set_gguf_parameters()
72487260
hparams = self.hparams
72497261

7250-
self.gguf_writer.add_leading_dense_block_count(hparams["first_k_dense_replace"])
7262+
# first_k_dense_replace: number of leading layers using dense FFN instead of MoE
7263+
# For non-MoE models (like Youtu), set to n_layer to use dense FFN for all layers
7264+
# For MoE models (like DeepSeek-V2), this is the number of leading non-MoE layers
7265+
has_moe = hparams.get("n_routed_experts") is not None
7266+
first_k_dense_replace = hparams.get("first_k_dense_replace")
7267+
if first_k_dense_replace is None:
7268+
# Default: if no MoE, all layers are dense; if MoE, none are dense
7269+
first_k_dense_replace = hparams["num_hidden_layers"] if not has_moe else 0
7270+
self.gguf_writer.add_leading_dense_block_count(first_k_dense_replace)
72517271
self.gguf_writer.add_vocab_size(hparams["vocab_size"])
72527272
if "q_lora_rank" in hparams and hparams["q_lora_rank"] is not None:
72537273
self.gguf_writer.add_q_lora_rank(hparams["q_lora_rank"])
@@ -7259,11 +7279,24 @@ def set_gguf_parameters(self):
72597279
self.gguf_writer.add_key_length_mla(hparams["qk_nope_head_dim"] + hparams["qk_rope_head_dim"])
72607280
self.gguf_writer.add_value_length_mla(hparams["v_head_dim"])
72617281

7262-
self.gguf_writer.add_expert_feed_forward_length(hparams["moe_intermediate_size"])
7263-
self.gguf_writer.add_expert_count(hparams["n_routed_experts"])
7264-
self.gguf_writer.add_expert_shared_count(hparams["n_shared_experts"])
7265-
self.gguf_writer.add_expert_weights_scale(hparams["routed_scaling_factor"])
7266-
self.gguf_writer.add_expert_weights_norm(hparams["norm_topk_prob"])
7282+
# MoE parameters (required by C++ code for DEEPSEEK2 arch)
7283+
# For non-MoE models like Youtu, use intermediate_size as expert_feed_forward_length
7284+
moe_intermediate_size = self.find_hparam(["moe_intermediate_size", "intermediate_size"], optional=False)
7285+
self.gguf_writer.add_expert_feed_forward_length(moe_intermediate_size)
7286+
7287+
if (n_routed_experts := hparams.get("n_routed_experts")) is not None:
7288+
self.gguf_writer.add_expert_count(n_routed_experts)
7289+
7290+
# expert_shared_count is required by C++ code, default to 0 for non-MoE models
7291+
n_shared_experts = hparams.get("n_shared_experts", 0)
7292+
self.gguf_writer.add_expert_shared_count(n_shared_experts)
7293+
7294+
# When not set, C++ code will use scale_w = false to skip the no-op scaling
7295+
if (routed_scaling_factor := hparams.get("routed_scaling_factor")) is not None:
7296+
self.gguf_writer.add_expert_weights_scale(routed_scaling_factor)
7297+
7298+
if (norm_topk_prob := hparams.get("norm_topk_prob")) is not None and norm_topk_prob:
7299+
self.gguf_writer.add_expert_weights_norm(norm_topk_prob)
72677300

72687301
self.gguf_writer.add_rope_dimension_count(hparams["qk_rope_head_dim"])
72697302

@@ -7279,10 +7312,17 @@ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iter
72797312
# skip vision tensors and remove "language_model." for Kimi-VL
72807313
if "vision_tower" in name or "multi_modal_projector" in name:
72817314
return []
7282-
7315+
if name.startswith("siglip2.") or name.startswith("merger."):
7316+
return []
72837317
if name.startswith("language_model."):
72847318
name = name.replace("language_model.", "")
72857319

7320+
# skip lm_head.weight if tie_word_embeddings is True
7321+
if self.hparams.get("tie_word_embeddings", False):
7322+
if name == "lm_head.weight" or name == "model.lm_head.weight":
7323+
logger.info("Skipping tied output layer 'lm_head.weight' (will use token_embd.weight)")
7324+
return []
7325+
72867326
# rename e_score_correction_bias tensors
72877327
if name.endswith("e_score_correction_bias"):
72887328
name = name.replace("e_score_correction_bias", "e_score_correction.bias")
@@ -10617,6 +10657,79 @@ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iter
1061710657
return []
1061810658

1061910659

10660+
@ModelBase.register("YOUTUVLForConditionalGeneration", "YOUTUVLForCausalLM")
10661+
class YOUTUVLVisionModel(MmprojModel):
10662+
def __init__(self, *args, **kwargs):
10663+
super().__init__(*args, **kwargs)
10664+
assert self.hparams_vision is not None
10665+
self.hparams_vision["image_size"] = self.hparams_vision.get("image_size", 560)
10666+
10667+
def set_gguf_parameters(self):
10668+
super().set_gguf_parameters()
10669+
10670+
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.YOUTUVL)
10671+
self.gguf_writer.add_vision_attention_layernorm_eps(self.hparams.get("layer_norm_eps", 1e-6))
10672+
10673+
# Handle activation function
10674+
hidden_act = str(self.hparams.get("hidden_act", "gelu_pytorch_tanh")).lower()
10675+
if hidden_act in ("gelu", "gelu_pytorch_tanh", "gelu_fast", "gelu_new", "gelu_accurate"):
10676+
self.gguf_writer.add_vision_use_gelu(True)
10677+
elif hidden_act == "silu":
10678+
self.gguf_writer.add_vision_use_silu(True)
10679+
else:
10680+
raise ValueError(f"Unsupported activation function for YOUTUVL: {hidden_act}")
10681+
10682+
self.gguf_writer.add_vision_spatial_merge_size(self.hparams.get("spatial_merge_size", 2))
10683+
10684+
window_size = self.hparams.get("window_size")
10685+
if window_size is not None:
10686+
self.gguf_writer.add_vision_window_size(window_size)
10687+
# fullatt_block_indexes contains explicit layer indices that use full attention
10688+
# e.g., [2, 5, 8, 11] means layers 2, 5, 8, 11 use full attention
10689+
# All other layers use window attention
10690+
fullatt_block_indexes = self.hparams.get("fullatt_block_indexes")
10691+
assert fullatt_block_indexes is not None, "fullatt_block_indexes is required for youtuvl"
10692+
# Store the explicit layer indices for YoutuVL (irregular pattern approach)
10693+
self.gguf_writer.add_vision_wa_layer_indexes(layers=fullatt_block_indexes)
10694+
10695+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
10696+
del bid # unused
10697+
10698+
# Skip language model tensors
10699+
skip_prefixes = ('lm_head.', 'model.layers.', 'model.embed_tokens.', 'model.norm.')
10700+
if name.startswith(skip_prefixes):
10701+
return []
10702+
10703+
# Try to map the tensor using TensorNameMap (handles vision encoder and projector)
10704+
try:
10705+
new_name = self.map_tensor_name(name)
10706+
return [(new_name, data_torch)]
10707+
except ValueError:
10708+
# If mapping fails, log warning and skip
10709+
logger.warning(f"Cannot map tensor: {name}")
10710+
return []
10711+
10712+
10713+
@ModelBase.register("SolarOpenForCausalLM")
10714+
class SolarOpenModel(Glm4MoeModel):
10715+
model_arch = gguf.MODEL_ARCH.GLM4_MOE
10716+
10717+
def set_vocab(self):
10718+
from transformers import AutoTokenizer
10719+
tokenizer = AutoTokenizer.from_pretrained(self.dir_model)
10720+
special_vocab = gguf.SpecialVocab(self.dir_model, load_merges=True)
10721+
tokens, toktypes, tokpre = self.get_vocab_base()
10722+
self.gguf_writer.add_tokenizer_model("gpt2")
10723+
self.gguf_writer.add_tokenizer_pre(tokpre)
10724+
self.gguf_writer.add_token_list(tokens)
10725+
self.gguf_writer.add_token_types(toktypes)
10726+
special_vocab._set_special_token("eos", tokenizer.get_added_vocab()["<|endoftext|>"])
10727+
special_vocab._set_special_token("eot", tokenizer.get_added_vocab()["<|endoftext|>"])
10728+
special_vocab._set_special_token("unk", tokenizer.get_added_vocab()["<unk>"])
10729+
special_vocab._set_special_token("bos", tokenizer.get_added_vocab()["<|startoftext|>"])
10730+
special_vocab.add_to_gguf(self.gguf_writer)
10731+
10732+
1062010733
###### CONVERSION LOGIC ######
1062110734

1062210735

‎convert_hf_to_gguf_update.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ class TOKENIZER_TYPE(IntEnum):
145145
{"name": "granite-docling", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/ibm-granite/granite-docling-258M", },
146146
{"name": "minimax-m2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/MiniMaxAI/MiniMax-M2", },
147147
{"name": "kormo", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/KORMo-Team/KORMo-tokenizer", },
148+
{"name": "youtu", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/tencent/Youtu-LLM-2B", },
149+
{"name": "solar-open", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/upstage/Solar-Open-100B", },
148150
]
149151

150152
# some models are known to be broken upstream, so we will skip them as exceptions
@@ -165,6 +167,8 @@ class TOKENIZER_TYPE(IntEnum):
165167
{"name": "kimi-k2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/moonshotai/Kimi-K2-Base", "chkhsh": "81212dc7cdb7e0c1074ca62c5aeab0d43c9f52b8a737be7b12a777c953027890"},
166168
{"name": "qwen2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/Qwen/Qwen3-Embedding-0.6B", "chkhsh": "d4540891389ea895b53b399da6ac824becc30f2fba0e9ddbb98f92e55ca0e97c"},
167169
{"name": "grok-2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/alvarobartt/grok-2-tokenizer", "chkhsh": "66b8d4e19ab16c3bfd89bce5d785fb7e0155e8648708a1f42077cb9fe002c273"},
170+
# jina-v2-de variants
171+
{"name": "jina-v2-de", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/aari1995/German_Semantic_V3", "chkhsh": "b3d1dd861f1d4c5c0d2569ce36baf3f90fe8a102db3de50dd71ff860d91be3df"},
168172
]
169173

170174

‎gguf-py/gguf/constants.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,9 @@ class ClipVision:
294294
USE_GELU = "clip.use_gelu"
295295
USE_SILU = "clip.use_silu"
296296
N_WA_PATTERN = "clip.vision.n_wa_pattern" # used by qwen2.5vl
297+
WA_LAYER_INDEXES = "clip.vision.wa_layer_indexes" # used by youtuvl
297298
IS_DEEPSTACK_LAYERS = "clip.vision.is_deepstack_layers"
299+
WINDOW_SIZE = "clip.vision.window_size"
298300

299301
class Attention:
300302
HEAD_COUNT = "clip.vision.attention.head_count"
@@ -3494,6 +3496,7 @@ class VisionProjectorType:
34943496
LFM2A = "lfm2a" # audio
34953497
MUSIC_FLAMINGO = "musicflamingo" # audio
34963498
GLM4V = "glm4v"
3499+
YOUTUVL = "youtuvl"
34973500

34983501

34993502
# Items here are (block size, type size)

‎gguf-py/gguf/gguf_writer.py‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,11 +1129,40 @@ def add_vision_projector_scale_factor(self, value: int) -> None:
11291129
self.add_uint32(Keys.ClipVision.Projector.SCALE_FACTOR, value)
11301130

11311131
def add_vision_n_wa_pattern(self, value: int) -> None:
1132+
"""Add window attention pattern interval for vision models.
1133+
1134+
This defines the pattern interval for window attention vs full attention layers.
1135+
For example, if n_wa_pattern=4, then layers 3, 7, 11, ... use full attention,
1136+
while other layers use window attention.
1137+
1138+
Used by models like Qwen2.5-VL where full attention layers follow a regular pattern.
1139+
"""
11321140
self.add_uint32(Keys.ClipVision.N_WA_PATTERN, value)
11331141

1142+
def add_vision_wa_layer_indexes(self, layers: Sequence[int]) -> None:
1143+
"""Add explicit layer indexes that use full attention in vision models.
1144+
1145+
This specifies the exact layer indices (0-based) that should use full attention
1146+
instead of window attention. All other layers will use window attention.
1147+
1148+
Args:
1149+
layers: List of layer indices that use full attention (e.g., [3, 7, 11, 15])
1150+
1151+
Used by models like YoutuVL where full attention layers are explicitly specified
1152+
rather than following a regular pattern.
1153+
1154+
Difference from add_vision_n_wa_pattern:
1155+
- n_wa_pattern: Defines a regular interval pattern (every Nth layer uses full attention)
1156+
- wa_layer_indexes: Explicitly lists which layers use full attention (irregular pattern)
1157+
"""
1158+
self.add_array(Keys.ClipVision.WA_LAYER_INDEXES, layers)
1159+
11341160
def add_vision_is_deepstack_layers(self, layers: Sequence[bool]) -> None:
11351161
self.add_array(Keys.ClipVision.IS_DEEPSTACK_LAYERS, layers)
11361162

1163+
def add_vision_window_size(self, value: int) -> None:
1164+
self.add_uint32(Keys.ClipVision.WINDOW_SIZE, value)
1165+
11371166
# audio models
11381167

11391168
def add_audio_projection_dim(self, value: int) -> None:

0 commit comments

Comments
 (0)