Skip to content

Commit 66cdb0b

Browse files
committed
convert : add support for Gemma4AssistantForCausalLM (MTP drafter)
Register the Gemma4AssistantForCausalLM architecture for GGUF conversion. This is the text-only MTP drafter model used for speculative decoding with Gemma 4 models. Key differences from Gemma4ForConditionalGeneration: - attention_k_eq_v=true with no k_proj/v_proj tensors (k_proj derived by slicing q_proj, v_proj = k_proj) - k_norm derived from q_norm (same head_dim) - Filters out post_projection and pre_projection tensors (drafter-specific projection layers not needed for inference) - Overrides shared_kv_layers to 0 for standalone inference (HF config has 4, meaning all layers share KV with main model during speculative decoding)
1 parent 560304e commit 66cdb0b

2 files changed

Lines changed: 45 additions & 0 deletions

File tree

conversion/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
"Gemma3TextModel": "gemma",
7474
"Gemma3nForCausalLM": "gemma",
7575
"Gemma3nForConditionalGeneration": "gemma",
76+
"Gemma4AssistantForCausalLM": "gemma",
7677
"Gemma4ForConditionalGeneration": "gemma",
7778
"GemmaForCausalLM": "gemma",
7879
"Glm4ForCausalLM": "glm",

conversion/gemma.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,50 @@ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iter
765765
yield from super().modify_tensors(data_torch, name, bid)
766766

767767

768+
@ModelBase.register("Gemma4AssistantForCausalLM")
769+
class Gemma4AssistantModel(Gemma4Model):
770+
"""Text-only Gemma4 assistant model (MTP drafter for Gemma4).
771+
772+
This model has attention_k_eq_v=true and no k_proj/v_proj tensors.
773+
K is derived by slicing q_proj, V=K. K norm = Q norm.
774+
"""
775+
model_arch = gguf.MODEL_ARCH.GEMMA4
776+
777+
@classmethod
778+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
779+
name, gen = item
780+
if name in ("post_projection.weight", "pre_projection.weight"):
781+
return None
782+
return super().filter_tensors((name, gen))
783+
784+
def set_gguf_parameters(self):
785+
super().set_gguf_parameters()
786+
# For standalone inference, all layers need their own KV cache.
787+
# The shared_kv_layers=4 from HF config means the drafter shares KV
788+
# with the main model during speculative decoding.
789+
# We override to 0 so the model works standalone.
790+
self.gguf_writer.add_shared_kv_layers(0)
791+
792+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
793+
# Gemma4Assistant has attention_k_eq_v=true with no k_proj/v_proj tensors.
794+
# Derive k_proj by slicing q_proj to the first n_kv * head_dim rows.
795+
if name.endswith(".self_attn.q_proj.weight"):
796+
is_swa = self.hparams["layer_types"][bid] == "sliding_attention"
797+
head_dim = self.hparams["head_dim"] if is_swa else self.hparams["global_head_dim"]
798+
n_kv = self.hparams["num_key_value_heads"] if is_swa else self.hparams["num_global_key_value_heads"]
799+
k_dim = n_kv * head_dim
800+
k_data = data_torch[:k_dim, :]
801+
k_gguf = self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_K, bid)
802+
yield (k_gguf, k_data)
803+
804+
# Also derive k_norm from q_norm (same head_dim, same normalization)
805+
if name.endswith(".self_attn.q_norm.weight"):
806+
k_norm_gguf = self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_K_NORM, bid)
807+
yield (k_norm_gguf, data_torch)
808+
809+
yield from super().modify_tensors(data_torch, name, bid)
810+
811+
768812
@ModelBase.register("Gemma4ForConditionalGeneration")
769813
class Gemma4VisionAudioModel(MmprojModel):
770814
has_audio_encoder = True

0 commit comments

Comments
 (0)