Skip to content

Commit 6a257d4

Browse files
authored
mtmd, model : merge HunyuanOCR into HunyuanVL and fix OCR vision precision (ggml-org#23329)
- HunyuanOCR shares the same HF arch and vision layout as HunyuanVL butwas split into a separate path that skipped the +0.1 bilinear sampler used by the HF reference. - Collapse OCR into the HUNYUANVL projector + HUNYUAN_VL text arch
1 parent 3a479c9 commit 6a257d4

15 files changed

Lines changed: 58 additions & 134 deletions

File tree

conversion/hunyuan.py

Lines changed: 10 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,8 @@ def set_vocab(self):
189189
self.gguf_writer.add_token_list(tokens)
190190
self.gguf_writer.add_token_types(toktypes)
191191

192-
# HunyuanOCR has pad_token_id=-1 in config.json; exclude pad from SpecialVocab
192+
# Some HunYuanVL variants (e.g. OCR-style configs) have pad_token_id=-1;
193+
# guard SpecialVocab so it doesn't try to emit an invalid pad id.
193194
token_types = None
194195
if (self.hparams.get("pad_token_id") or 0) < 0:
195196
token_types = ('bos', 'eos', 'unk', 'sep', 'cls', 'mask')
@@ -250,7 +251,8 @@ def set_vocab(self):
250251
self._fix_special_tokens()
251252

252253
def set_gguf_parameters(self):
253-
# HunyuanOCR has num_experts=1 which is not MoE, prevent parent from writing it
254+
# Some HunYuanVL variants set num_experts=1 (not real MoE);
255+
# prevent the parent class from emitting expert_count metadata in that case.
254256
saved_num_experts = self.hparams.pop("num_experts", None)
255257
super().set_gguf_parameters()
256258
if saved_num_experts is not None and saved_num_experts > 1:
@@ -288,51 +290,21 @@ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iter
288290

289291
@ModelBase.register("HunYuanVLForConditionalGeneration")
290292
class HunyuanVLVisionModel(MmprojModel):
291-
# Handles both HunyuanOCR and HunyuanVL, which share the HF architecture name
292-
# "HunYuanVLForConditionalGeneration" and the `vit.perceive.*` vision layout.
293-
# Each variant maps to a different projector type in clip.cpp so image
294-
# preprocessing follows the correct code path.
295-
296293
def __init__(self, *args, **kwargs):
297294
super().__init__(*args, **kwargs)
298295
assert self.hparams_vision is not None
299-
# HunyuanOCR / HunyuanVL uses max_image_size instead of image_size
296+
# HunyuanVL uses max_image_size instead of image_size
300297
if "image_size" not in self.hparams_vision:
301298
self.hparams_vision["image_size"] = self.hparams_vision.get("max_image_size", 2048)
302299

303-
@staticmethod
304-
def is_ocr_variant(hparams: dict) -> bool:
305-
"""Return True for HunyuanOCR, False for HunyuanVL.
306-
307-
The projector's output dim must equal the text model's hidden_size by
308-
construction (that's what "projector" means). HunyuanOCR pairs a 1B text
309-
backbone (hidden=1024); HunyuanVL pairs a 4B one (hidden=3072). So the
310-
ViT -> LLM projection dim is a hard architectural signature, not a
311-
magic number.
312-
"""
313-
vision_out = int((hparams.get("vision_config") or {}).get("out_hidden_size", 0))
314-
return vision_out == 1024
315-
316300
def set_gguf_parameters(self):
317301
super().set_gguf_parameters()
318302
assert self.hparams_vision is not None
319303
vcfg = self.hparams_vision
320-
321-
if self.is_ocr_variant(self.global_config):
322-
# --- HunyuanOCR ---
323-
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.HUNYUANOCR)
324-
self.gguf_writer.add_vision_use_gelu(True)
325-
self.gguf_writer.add_vision_attention_layernorm_eps(vcfg.get("rms_norm_eps", 1e-5))
326-
self.gguf_writer.add_vision_spatial_merge_size(vcfg.get("spatial_merge_size", 2))
327-
self.gguf_writer.add_vision_min_pixels(self.preprocessor_config["min_pixels"])
328-
self.gguf_writer.add_vision_max_pixels(self.preprocessor_config["max_pixels"])
329-
return
330-
331-
# --- HunyuanVL ---
332304
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.HUNYUANVL)
333-
self.gguf_writer.add_vision_use_gelu(str(vcfg["hidden_act"]).lower() == "gelu")
334-
self.gguf_writer.add_vision_attention_layernorm_eps(float(vcfg["rms_norm_eps"]))
335-
self.gguf_writer.add_vision_spatial_merge_size(int(vcfg["spatial_merge_size"]))
305+
self.gguf_writer.add_vision_use_gelu(True)
306+
self.gguf_writer.add_vision_attention_layernorm_eps(vcfg.get("rms_norm_eps", 1e-5))
307+
self.gguf_writer.add_vision_spatial_merge_size(vcfg.get("spatial_merge_size", 2))
336308
self.gguf_writer.add_vision_min_pixels(int(self.preprocessor_config["min_pixels"]))
337309
self.gguf_writer.add_vision_max_pixels(int(self.preprocessor_config["max_pixels"]))
338310

@@ -353,48 +325,26 @@ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iter
353325

354326
def tensor_force_quant(self, name, new_name, bid, n_dims):
355327
# force conv weights to F32 or F16 to avoid BF16 IM2COL issues on Metal
356-
# Both HunyuanOCR and HunyuanVL emit the ViT -> LLM projection as mm.0/mm.2.
328+
# HunyuanVL emit the ViT -> LLM projection as mm.0/mm.2.
357329
if ("mm.0." in new_name or "mm.2." in new_name) and new_name.endswith(".weight"):
358330
return gguf.GGMLQuantizationType.F16 if self.ftype == gguf.LlamaFileType.MOSTLY_F16 else gguf.GGMLQuantizationType.F32
359331
return super().tensor_force_quant(name, new_name, bid, n_dims)
360332

361333

362334
@ModelBase.register("HunYuanVLForConditionalGeneration")
363335
class HunyuanVLTextModel(HunYuanModel):
364-
# The "HunYuanVLForConditionalGeneration" HF architecture covers both HunyuanOCR
365-
# and HunyuanVL. HunyuanOCR reuses the HunYuan-Dense text backbone (standard RoPE),
366-
# while HunyuanVL introduces a new LLM arch with XD-RoPE. Detect the variant from
367-
# the config and pick the matching GGUF architecture.
368336
model_arch = gguf.MODEL_ARCH.HUNYUAN_VL
369337

370-
@staticmethod
371-
def _is_ocr_config(hparams: dict) -> bool:
372-
# OCR pairs a 1B text backbone (hidden=1024) with a ViT projector that
373-
# outputs 1024-d; HunyuanVL uses 3072-d. Keep in sync with
374-
# HunyuanVLVisionModel.is_ocr_variant.
375-
return int((hparams.get("vision_config") or {}).get("out_hidden_size", 0)) == 1024
376-
377338
def __init__(self, dir_model: Path, *args, **kwargs):
378-
raw_hparams = kwargs.get("hparams") or ModelBase.load_hparams(dir_model, is_mistral_format=False)
379-
if self._is_ocr_config(raw_hparams):
380-
self.model_arch = gguf.MODEL_ARCH.HUNYUAN_DENSE
381-
else:
382-
self.model_arch = gguf.MODEL_ARCH.HUNYUAN_VL
383339
super().__init__(dir_model, *args, **kwargs)
384340

385341
def set_gguf_parameters(self):
386342
super().set_gguf_parameters()
387343

388-
# Only emit XD-RoPE metadata for the HunyuanVL backbone; HunyuanOCR uses
389-
# the HunYuan-Dense arch which already handles standard rope in super().
390-
if self.model_arch != gguf.MODEL_ARCH.HUNYUAN_VL:
391-
return
392-
344+
# XD-RoPE metadata for the HunyuanVL;
393345
if self.rope_parameters.get("rope_type") != "xdrope":
394346
return
395347

396-
# defaults for HunyuanVL. The C++ side later computes:
397-
# freq_base = rope_theta * alpha ** (head_dim / (head_dim - 2))
398348
self.gguf_writer.add_rope_freq_base(float(self.rope_parameters["rope_theta"]))
399349
self.gguf_writer.add_rope_scaling_alpha(float(self.rope_parameters["alpha"]))
400350
self.gguf_writer.add_rope_scaling_type(gguf.RopeScalingType.NONE)

gguf-py/gguf/constants.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -747,7 +747,7 @@ class MODEL_TENSOR(IntEnum):
747747
V_LAYER_OUT_SCALE = auto()
748748
V_PRE_NORM = auto()
749749
V_POST_NORM = auto()
750-
V_MM_PRE_NORM = auto() # hunyuanocr
750+
V_MM_PRE_NORM = auto() # hunyuanvl
751751
V_MM_POST_NORM = auto()
752752
V_MM_INP_NORM = auto()
753753
V_MM_INP_PROJ = auto() # gemma3
@@ -791,8 +791,8 @@ class MODEL_TENSOR(IntEnum):
791791
V_MM_GATE = auto() # cogvlm
792792
V_TOK_BOI = auto() # cogvlm
793793
V_TOK_EOI = auto() # cogvlm
794-
V_TOK_IMG_BEGIN = auto() # hunyuanocr
795-
V_TOK_IMG_END = auto() # hunyuanocr
794+
V_TOK_IMG_BEGIN = auto() # hunyuanvl
795+
V_TOK_IMG_END = auto() # hunyuanvl
796796
V_STD_BIAS = auto() # gemma4
797797
V_STD_SCALE = auto() # gemma4
798798
V_SAM_POS_EMBD = auto() # Deepseek-OCR
@@ -4273,7 +4273,6 @@ class VisionProjectorType:
42734273
GLM4V = "glm4v"
42744274
YOUTUVL = "youtuvl"
42754275
NEMOTRON_V2_VL = "nemotron_v2_vl"
4276-
HUNYUANOCR = "hunyuanocr"
42774276
HUNYUANVL = "hunyuanvl"
42784277
MINICPMV4_6 = "minicpmv4_6"
42794278
GRANITE_SPEECH = "granite_speech" # audio

gguf-py/gguf/tensor_mapping.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1366,15 +1366,15 @@ class TensorNameMap:
13661366
"mlp_AR.linear_{bid}", # PaddleOCR-VL
13671367
"merger.mlp.{bid}",
13681368
"vision_tower.merger.mlp.{bid}", # dots.ocr
1369-
"vit.perceive.proj.{bid}", # HunyuanOCR (proj.0 = conv1, proj.2 = conv2)
1369+
"vit.perceive.proj.{bid}", # HunyuanVL (proj.0 = conv1, proj.2 = conv2)
13701370
),
13711371

13721372
MODEL_TENSOR.V_MMPROJ_FC: (
13731373
"model.connector.modality_projection.proj", # SmolVLM
13741374
"model.vision.linear_proj.linear_proj", # cogvlm
13751375
"model.projector.layers", # Deepseek-OCR
13761376
"visual.merger.proj", # glm4v
1377-
"vit.perceive.mlp", # HunyuanOCR
1377+
"vit.perceive.mlp", # HunyuanVL
13781378
),
13791379

13801380
MODEL_TENSOR.V_MMPROJ_MLP: (
@@ -1403,7 +1403,7 @@ class TensorNameMap:
14031403
"model.vision_tower.embeddings.patch_embeddings.projection", # Intern-S1
14041404
"vpm.embeddings.patch_embedding",
14051405
"model.vision_model.embeddings.patch_embedding", # SmolVLM
1406-
"vit.embeddings.patch_embedding", # HunyuanOCR
1406+
"vit.embeddings.patch_embedding", # HunyuanVL
14071407
"vision_tower.patch_conv", # pixtral-hf
14081408
"vision_encoder.patch_conv", # pixtral
14091409
"vision_model.patch_embedding.linear", # llama 4
@@ -1429,7 +1429,7 @@ class TensorNameMap:
14291429
"model.vision_tower.embeddings.position_embeddings", # Intern-S1
14301430
"vpm.embeddings.position_embedding",
14311431
"model.vision_model.embeddings.position_embedding", # SmolVLM
1432-
"vit.embeddings.position_embedding", # HunyuanOCR
1432+
"vit.embeddings.position_embedding", # HunyuanVL
14331433
"vision_model.positional_embedding_vlm", # llama 4
14341434
"vision_tower.patch_embed.pos_emb", # kimi-vl
14351435
"visual.pos_embed", # qwen3vl
@@ -1442,12 +1442,12 @@ class TensorNameMap:
14421442

14431443
MODEL_TENSOR.V_ENC_EMBD_IMGNL: (
14441444
"model.image_newline", # Deepseek-OCR
1445-
"vit.perceive.image_newline", # HunyuanOCR
1445+
"vit.perceive.image_newline", # HunyuanVL
14461446
),
14471447

14481448
MODEL_TENSOR.V_ENC_EMBD_VSEP: (
14491449
"model.view_seperator", # Deepseek-OCR
1450-
"vit.perceive.image_sep", # HunyuanOCR
1450+
"vit.perceive.image_sep", # HunyuanVL
14511451
),
14521452

14531453
MODEL_TENSOR.V_ENC_ATTN_QKV: (
@@ -1466,7 +1466,7 @@ class TensorNameMap:
14661466
"model.vision_tower.encoder.layer.{bid}.attention.q_proj", # Intern-S1
14671467
"vpm.encoder.layers.{bid}.self_attn.q_proj",
14681468
"model.vision_model.encoder.layers.{bid}.self_attn.q_proj", # SmolVLM
1469-
"vit.layers.{bid}.self_attn.q_proj", # HunyuanOCR
1469+
"vit.layers.{bid}.self_attn.q_proj", # HunyuanVL
14701470
"vision_model.model.layers.{bid}.self_attn.q_proj", # llama4
14711471
"vision_tower.transformer.layers.{bid}.attention.q_proj", # pixtral-hf
14721472
"vision_encoder.transformer.layers.{bid}.attention.wq", # pixtral
@@ -1490,7 +1490,7 @@ class TensorNameMap:
14901490
"model.vision_tower.encoder.layer.{bid}.attention.k_proj", # Intern-S1
14911491
"vpm.encoder.layers.{bid}.self_attn.k_proj",
14921492
"model.vision_model.encoder.layers.{bid}.self_attn.k_proj", # SmolVLM
1493-
"vit.layers.{bid}.self_attn.k_proj", # HunyuanOCR
1493+
"vit.layers.{bid}.self_attn.k_proj", # HunyuanVL
14941494
"vision_model.model.layers.{bid}.self_attn.k_proj", # llama4
14951495
"vision_tower.transformer.layers.{bid}.attention.k_proj", # pixtral-hf
14961496
"vision_encoder.transformer.layers.{bid}.attention.wk", # pixtral
@@ -1514,7 +1514,7 @@ class TensorNameMap:
15141514
"model.vision_tower.encoder.layer.{bid}.attention.v_proj", # Intern-S1
15151515
"vpm.encoder.layers.{bid}.self_attn.v_proj",
15161516
"model.vision_model.encoder.layers.{bid}.self_attn.v_proj", # SmolVLM
1517-
"vit.layers.{bid}.self_attn.v_proj", # HunyuanOCR
1517+
"vit.layers.{bid}.self_attn.v_proj", # HunyuanVL
15181518
"vision_model.model.layers.{bid}.self_attn.v_proj", # llama4
15191519
"vision_tower.transformer.layers.{bid}.attention.v_proj", # pixtral-hf
15201520
"vision_encoder.transformer.layers.{bid}.attention.wv", # pixtral
@@ -1532,7 +1532,7 @@ class TensorNameMap:
15321532
"model.vision_tower.encoder.layer.{bid}.layernorm_before", # Intern-S1
15331533
"vpm.encoder.layers.{bid}.layer_norm1",
15341534
"model.vision_model.encoder.layers.{bid}.layer_norm1", # SmolVLM
1535-
"vit.layers.{bid}.input_layernorm", # HunyuanOCR
1535+
"vit.layers.{bid}.input_layernorm", # HunyuanVL
15361536
"vision_tower.transformer.layers.{bid}.attention_norm", # pixtral-hf
15371537
"vision_encoder.transformer.layers.{bid}.attention_norm", # pixtral
15381538
"vision_model.model.layers.{bid}.input_layernorm", # llama4, gemma4
@@ -1553,7 +1553,7 @@ class TensorNameMap:
15531553
"model.vision_tower.encoder.layer.{bid}.attention.projection_layer", # Intern-S1
15541554
"vpm.encoder.layers.{bid}.self_attn.out_proj",
15551555
"model.vision_model.encoder.layers.{bid}.self_attn.out_proj", # SmolVLM
1556-
"vit.layers.{bid}.self_attn.o_proj", # HunyuanOCR
1556+
"vit.layers.{bid}.self_attn.o_proj", # HunyuanVL
15571557
"model.vision_model.encoder.layers.{bid}.self_attn.projection_layer", # Janus Pro
15581558
"vision_model.model.layers.{bid}.self_attn.o_proj", # llama4
15591559
"vision_tower.transformer.layers.{bid}.attention.o_proj", # pixtral-hf
@@ -1580,7 +1580,7 @@ class TensorNameMap:
15801580
"model.vision_tower.encoder.layer.{bid}.layernorm_after", # Intern-S1
15811581
"vpm.encoder.layers.{bid}.layer_norm2",
15821582
"model.vision_model.encoder.layers.{bid}.layer_norm2", # SmolVLM
1583-
"vit.layers.{bid}.post_attention_layernorm", # HunyuanOCR
1583+
"vit.layers.{bid}.post_attention_layernorm", # HunyuanVL
15841584
"vision_model.model.layers.{bid}.post_attention_layernorm", # llama4
15851585
"vision_tower.transformer.layers.{bid}.ffn_norm", # pixtral-hf
15861586
"vision_encoder.transformer.layers.{bid}.ffn_norm", # pixtral
@@ -1601,7 +1601,7 @@ class TensorNameMap:
16011601
"model.vision_tower.encoder.layer.{bid}.mlp.fc1", # Intern-S1
16021602
"vpm.encoder.layers.{bid}.mlp.fc1",
16031603
"model.vision_model.encoder.layers.{bid}.mlp.fc1", # SmolVLM, gemma3
1604-
"vit.layers.{bid}.mlp.dense_h_to_4h", # HunyuanOCR
1604+
"vit.layers.{bid}.mlp.dense_h_to_4h", # HunyuanVL
16051605
"vision_tower.transformer.layers.{bid}.feed_forward.up_proj", # pixtral-hf
16061606
"vision_encoder.transformer.layers.{bid}.feed_forward.w3", # pixtral
16071607
"vision_model.model.layers.{bid}.mlp.fc1", # llama4
@@ -1630,7 +1630,7 @@ class TensorNameMap:
16301630
"model.vision_tower.encoder.layer.{bid}.mlp.fc2", # Intern-S1
16311631
"vpm.encoder.layers.{bid}.mlp.fc2",
16321632
"model.vision_model.encoder.layers.{bid}.mlp.fc2", # SmolVLM, gemma3
1633-
"vit.layers.{bid}.mlp.dense_4h_to_h", # HunyuanOCR
1633+
"vit.layers.{bid}.mlp.dense_4h_to_h", # HunyuanVL
16341634
"vision_tower.transformer.layers.{bid}.feed_forward.down_proj", # pixtral-hf
16351635
"vision_encoder.transformer.layers.{bid}.feed_forward.w2", # pixtral
16361636
"vision_model.model.layers.{bid}.mlp.fc2", # llama4
@@ -1694,7 +1694,7 @@ class TensorNameMap:
16941694
MODEL_TENSOR.V_MM_POST_NORM: (
16951695
"visual.merger.post_projection_norm", # glm4v
16961696
"vision_tower.post_trunk_norm", # dots.ocr
1697-
"vit.perceive.after_rms", # HunyuanOCR
1697+
"vit.perceive.after_rms", # HunyuanVL
16981698
),
16991699

17001700
MODEL_TENSOR.V_MM_INP_PROJ: (
@@ -1899,15 +1899,15 @@ class TensorNameMap:
18991899
),
19001900

19011901
MODEL_TENSOR.V_MM_PRE_NORM: (
1902-
"vit.perceive.before_rms", # HunyuanOCR
1902+
"vit.perceive.before_rms", # HunyuanVL
19031903
),
19041904

19051905
MODEL_TENSOR.V_TOK_IMG_BEGIN: (
1906-
"vit.perceive.image_begin", # HunyuanOCR
1906+
"vit.perceive.image_begin", # HunyuanVL
19071907
),
19081908

19091909
MODEL_TENSOR.V_TOK_IMG_END: (
1910-
"vit.perceive.image_end", # HunyuanOCR
1910+
"vit.perceive.image_end", # HunyuanVL
19111911
),
19121912

19131913
MODEL_TENSOR.V_STD_BIAS: (

src/llama-chat.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ static const std::map<std::string, llm_chat_template> LLM_CHAT_TEMPLATES = {
7373
{ "hunyuan-moe", LLM_CHAT_TEMPLATE_HUNYUAN_MOE },
7474
{ "gpt-oss", LLM_CHAT_TEMPLATE_OPENAI_MOE },
7575
{ "hunyuan-dense", LLM_CHAT_TEMPLATE_HUNYUAN_DENSE },
76-
{ "hunyuan-ocr", LLM_CHAT_TEMPLATE_HUNYUAN_OCR },
76+
{ "hunyuan-vl", LLM_CHAT_TEMPLATE_HUNYUAN_VL },
7777
{ "kimi-k2", LLM_CHAT_TEMPLATE_KIMI_K2 },
7878
{ "seed_oss", LLM_CHAT_TEMPLATE_SEED_OSS },
7979
{ "grok-2", LLM_CHAT_TEMPLATE_GROK_2 },
@@ -218,7 +218,7 @@ llm_chat_template llm_chat_detect_template(const std::string & tmpl) {
218218
} else if (tmpl_contains("<|start|>") && tmpl_contains("<|channel|>")) {
219219
return LLM_CHAT_TEMPLATE_OPENAI_MOE;
220220
} else if (tmpl_contains("<|hy_Assistant|>") && tmpl_contains("<|hy_begin▁of▁sentence|>")) {
221-
return LLM_CHAT_TEMPLATE_HUNYUAN_OCR;
221+
return LLM_CHAT_TEMPLATE_HUNYUAN_VL;
222222
} else if (tmpl_contains("<|hy_Assistant|>") && tmpl_contains("<|hy_place▁holder▁no▁3|>")) {
223223
return LLM_CHAT_TEMPLATE_HUNYUAN_DENSE;
224224
} else if (tmpl_contains("<|im_assistant|>assistant<|im_middle|>")) {
@@ -825,8 +825,8 @@ int32_t llm_chat_apply_template(
825825
ss << "<|hy_User|>" << chat[i]->content << "<|hy_Assistant|>";
826826
}
827827
}
828-
} else if (tmpl == LLM_CHAT_TEMPLATE_HUNYUAN_OCR) {
829-
// tencent/HunyuanOCR
828+
} else if (tmpl == LLM_CHAT_TEMPLATE_HUNYUAN_VL) {
829+
// tencent/HunyuanOCR & tencent/HunyuanVL
830830
ss << "<|hy_begin▁of▁sentence|>";
831831
for (size_t i = 0; i < chat.size(); i++) {
832832
std::string role(chat[i]->role);

src/llama-chat.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ enum llm_chat_template {
5353
LLM_CHAT_TEMPLATE_HUNYUAN_MOE,
5454
LLM_CHAT_TEMPLATE_OPENAI_MOE,
5555
LLM_CHAT_TEMPLATE_HUNYUAN_DENSE,
56-
LLM_CHAT_TEMPLATE_HUNYUAN_OCR,
56+
LLM_CHAT_TEMPLATE_HUNYUAN_VL,
5757
LLM_CHAT_TEMPLATE_KIMI_K2,
5858
LLM_CHAT_TEMPLATE_SEED_OSS,
5959
LLM_CHAT_TEMPLATE_GROK_2,

0 commit comments

Comments
 (0)