@@ -11855,7 +11855,7 @@ def prepare_tensors(self):
1185511855 raise ValueError(f"Unprocessed experts: {experts}")
1185611856
1185711857
11858- @ModelBase.register("HunYuanDenseV1ForCausalLM", "HunYuanVLForConditionalGeneration" )
11858+ @ModelBase.register("HunYuanDenseV1ForCausalLM")
1185911859class HunYuanModel(TextModel):
1186011860 model_arch = gguf.MODEL_ARCH.HUNYUAN_DENSE
1186111861
@@ -11994,40 +11994,125 @@ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iter
1199411994
1199511995
1199611996@ModelBase.register("HunYuanVLForConditionalGeneration")
11997- class HunyuanOCRVisionModel(MmprojModel):
11997+ class HunyuanVLVisionModel(MmprojModel):
11998+ # Handles both HunyuanOCR and HunyuanVL, which share the HF architecture name
11999+ # "HunYuanVLForConditionalGeneration" and the `vit.perceive.*` vision layout.
12000+ # Each variant maps to a different projector type in clip.cpp so image
12001+ # preprocessing follows the correct code path.
12002+
1199812003 def __init__(self, *args, **kwargs):
1199912004 super().__init__(*args, **kwargs)
1200012005 assert self.hparams_vision is not None
12001- # HunyuanOCR uses max_image_size instead of image_size
12006+ # HunyuanOCR / HunyuanVL uses max_image_size instead of image_size
1200212007 if "image_size" not in self.hparams_vision:
1200312008 self.hparams_vision["image_size"] = self.hparams_vision.get("max_image_size", 2048)
1200412009
12010+ @staticmethod
12011+ def is_ocr_variant(hparams: dict) -> bool:
12012+ """Return True for HunyuanOCR, False for HunyuanVL.
12013+
12014+ The projector's output dim must equal the text model's hidden_size by
12015+ construction (that's what "projector" means). HunyuanOCR pairs a 1B text
12016+ backbone (hidden=1024); HunyuanVL pairs a 4B one (hidden=3072). So the
12017+ ViT -> LLM projection dim is a hard architectural signature, not a
12018+ magic number.
12019+ """
12020+ vision_out = int((hparams.get("vision_config") or {}).get("out_hidden_size", 0))
12021+ return vision_out == 1024
12022+
1200512023 def set_gguf_parameters(self):
1200612024 super().set_gguf_parameters()
1200712025 assert self.hparams_vision is not None
12008- hparams = self.hparams_vision
12009- self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.HUNYUANOCR)
12010- self.gguf_writer.add_vision_use_gelu(True)
12011- self.gguf_writer.add_vision_attention_layernorm_eps(hparams.get("rms_norm_eps", 1e-5))
12012- self.gguf_writer.add_vision_spatial_merge_size(hparams.get("spatial_merge_size", 2))
12013- self.gguf_writer.add_vision_min_pixels(self.preprocessor_config["min_pixels"])
12014- self.gguf_writer.add_vision_max_pixels(self.preprocessor_config["max_pixels"])
12026+ vcfg = self.hparams_vision
12027+
12028+ if self.is_ocr_variant(self.global_config):
12029+ # --- HunyuanOCR ---
12030+ self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.HUNYUANOCR)
12031+ self.gguf_writer.add_vision_use_gelu(True)
12032+ self.gguf_writer.add_vision_attention_layernorm_eps(vcfg.get("rms_norm_eps", 1e-5))
12033+ self.gguf_writer.add_vision_spatial_merge_size(vcfg.get("spatial_merge_size", 2))
12034+ self.gguf_writer.add_vision_min_pixels(self.preprocessor_config["min_pixels"])
12035+ self.gguf_writer.add_vision_max_pixels(self.preprocessor_config["max_pixels"])
12036+ return
12037+
12038+ # --- HunyuanVL ---
12039+ self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.HUNYUANVL)
12040+ self.gguf_writer.add_vision_use_gelu(str(vcfg["hidden_act"]).lower() == "gelu")
12041+ self.gguf_writer.add_vision_attention_layernorm_eps(float(vcfg["rms_norm_eps"]))
12042+ self.gguf_writer.add_vision_spatial_merge_size(int(vcfg["spatial_merge_size"]))
12043+ self.gguf_writer.add_vision_min_pixels(int(self.preprocessor_config["min_pixels"]))
12044+ self.gguf_writer.add_vision_max_pixels(int(self.preprocessor_config["max_pixels"]))
1201512045
1201612046 def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
1201712047 if not name.startswith("vit."):
12018- return # skip text tensors
12048+ return
1201912049 # strip CLS token (row 0) from position embeddings so resize_position_embeddings works
1202012050 if "position_embedding" in name:
1202112051 data_torch = data_torch[1:] # [n_patches+1, n_embd] -> [n_patches, n_embd]
1202212052 yield from super().modify_tensors(data_torch, name, bid)
1202312053
1202412054 def tensor_force_quant(self, name, new_name, bid, n_dims):
1202512055 # force conv weights to F32 or F16 to avoid BF16 IM2COL issues on Metal
12056+ # Both HunyuanOCR and HunyuanVL emit the ViT -> LLM projection as mm.0/mm.2.
1202612057 if ("mm.0." in new_name or "mm.2." in new_name) and new_name.endswith(".weight"):
1202712058 return gguf.GGMLQuantizationType.F16 if self.ftype == gguf.LlamaFileType.MOSTLY_F16 else gguf.GGMLQuantizationType.F32
1202812059 return super().tensor_force_quant(name, new_name, bid, n_dims)
1202912060
1203012061
12062+ @ModelBase.register("HunYuanVLForConditionalGeneration")
12063+ class HunyuanVLTextModel(HunYuanModel):
12064+ # The "HunYuanVLForConditionalGeneration" HF architecture covers both HunyuanOCR
12065+ # and HunyuanVL. HunyuanOCR reuses the HunYuan-Dense text backbone (standard RoPE),
12066+ # while HunyuanVL introduces a new LLM arch with XD-RoPE. Detect the variant from
12067+ # the config and pick the matching GGUF architecture.
12068+ model_arch = gguf.MODEL_ARCH.HUNYUAN_VL
12069+
12070+ @staticmethod
12071+ def _is_ocr_config(hparams: dict) -> bool:
12072+ # OCR pairs a 1B text backbone (hidden=1024) with a ViT projector that
12073+ # outputs 1024-d; HunyuanVL uses 3072-d. Keep in sync with
12074+ # HunyuanVLVisionModel.is_ocr_variant.
12075+ return int((hparams.get("vision_config") or {}).get("out_hidden_size", 0)) == 1024
12076+
12077+ def __init__(self, dir_model: Path, *args, **kwargs):
12078+ raw_hparams = kwargs.get("hparams") or ModelBase.load_hparams(dir_model, is_mistral_format=False)
12079+ if self._is_ocr_config(raw_hparams):
12080+ self.model_arch = gguf.MODEL_ARCH.HUNYUAN_DENSE
12081+ else:
12082+ self.model_arch = gguf.MODEL_ARCH.HUNYUAN_VL
12083+ super().__init__(dir_model, *args, **kwargs)
12084+
12085+ def set_gguf_parameters(self):
12086+ super().set_gguf_parameters()
12087+
12088+ # Only emit XD-RoPE metadata for the HunyuanVL backbone; HunyuanOCR uses
12089+ # the HunYuan-Dense arch which already handles standard rope in super().
12090+ if self.model_arch != gguf.MODEL_ARCH.HUNYUAN_VL:
12091+ return
12092+
12093+ if self.rope_parameters.get("rope_type") != "xdrope":
12094+ return
12095+
12096+ # defaults for HunyuanVL. The C++ side later computes:
12097+ # freq_base = rope_theta * alpha ** (head_dim / (head_dim - 2))
12098+ self.gguf_writer.add_rope_freq_base(float(self.rope_parameters["rope_theta"]))
12099+ self.gguf_writer.add_rope_scaling_alpha(float(self.rope_parameters["alpha"]))
12100+ self.gguf_writer.add_rope_scaling_type(gguf.RopeScalingType.NONE)
12101+ self.gguf_writer.add_rope_scaling_factor(float(self.rope_parameters.get("factor", 1)))
12102+
12103+ ctx_len = int(self.hparams["max_position_embeddings"])
12104+ self.gguf_writer.add_rope_scaling_orig_ctx_len(ctx_len)
12105+ self.gguf_writer.add_context_length(ctx_len)
12106+
12107+ self.gguf_writer.add_rope_dimension_sections(list(self.rope_parameters["xdrope_section"]))
12108+
12109+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
12110+ # Skip vision tensors — they are written by HunyuanVLVisionModel
12111+ if name.startswith("vit."):
12112+ return
12113+ yield from super().modify_tensors(data_torch, name, bid)
12114+
12115+
1203112116@ModelBase.register("SmolLM3ForCausalLM")
1203212117class SmolLM3Model(LlamaModel):
1203312118 model_arch = gguf.MODEL_ARCH.SMOLLM3
0 commit comments