Skip to content

fix(gemma4-gguf): accept scalar attention.head_count_kv - #190

Open
qxZap wants to merge 1 commit into
FlashML-org:mainfrom
qxZap:fix/gemma4-gguf-scalar-kv
Open

fix(gemma4-gguf): accept scalar attention.head_count_kv#190
qxZap wants to merge 1 commit into
FlashML-org:mainfrom
qxZap:fix/gemma4-gguf-scalar-kv

Conversation

@qxZap

@qxZap qxZap commented Aug 25, 2026

Copy link
Copy Markdown

Problem

parse_gguf_config treats gemma4.attention.head_count_kv as a per-layer array
unconditionally, but llama.cpp writes it as a scalar when every layer shares a
KV head count, emitting an array only when they differ. Any such checkpoint dies
at config-parse time:

File "freetoken/models/gemma4/gguf.py", line 73, in parse_gguf_config
    swa_kv = int(kv_per_layer[swa_layer_ids[0]]) if swa_layer_ids else int(kv_per_layer[0])
             ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
TypeError: 'int' object is not subscriptable

The backend worker then exits and the server shuts down, so the user sees only
{"error":"server unavailable: maintenance failed (restart required)"}.

Reproducer

lmstudio-community/gemma-4-E2B-it-GGUF. Dumping the metadata the parser reads:

gemma4.attention.head_count_kv       int=1            <- scalar
gemma4.attention.sliding_window_pattern  list(len=35) <- genuinely a list
gemma4.block_count                   int=35

So the file mixes both forms, and only head_count_kv was mis-typed.

Fix

Normalise to a per-layer list at the read site, leaving both consumers below
unchanged. hasattr(_kv, "__len__") rather than isinstance(..., list) so numpy
arrays from the reader are handled too.

Scope — please read

This fixes the crash, not dense gemma-4 GGUF support. Immediately after, the
same function hits:

intermediate_size=int(g("feed_forward_length")),
TypeError: int() argument must be ... not 'list'

because that key is per-layer on E2B (MatFormer). And beyond that,
parse_gguf_config hardcodes moe_enabled=True and requires expert_count,
expert_used_count and expert_feed_forward_length, which dense checkpoints do
not carry at all.

Auditing every g(...) call site against a real file:

key parser wants actual (E2B)
attention.head_count_kv list int=1this PR
feed_forward_length int list(len=35)
expert_count int missing
expert_used_count int missing
expert_feed_forward_length int missing

Full dense support looks like a feature, not a bug fix — ModelConfig takes a
single intermediate_size, so per-layer FFN cannot be expressed without a schema
change, and the downstream model code assumes experts. I have kept this PR to the
one unambiguous type bug and am happy to close it if you would rather address the
whole dense path in one go.

Testing

Verified the parser gets past this line on the reproducer above (it then fails at
feed_forward_length, as described). No behaviour change for array-valued
checkpoints: an input that was already a list takes the same path as before.

🤖 Generated with Claude Code

llama.cpp writes gemma4.attention.head_count_kv as a SCALAR when every layer
shares a KV head count, and only as a per-layer array when they differ. The
parser assumed the array form unconditionally, so any such checkpoint died at
config-parse time:

  swa_kv = int(kv_per_layer[swa_layer_ids[0]]) ...
  TypeError: 'int' object is not subscriptable

Reproduced with lmstudio-community/gemma-4-E2B-it-GGUF, whose metadata carries
head_count_kv=1 (scalar) alongside a genuine 35-element sliding_window_pattern.

Normalise to a per-layer list at the read site so both call sites below are
unchanged.

Note this fixes the crash, not dense gemma-4 support: parse_gguf_config still
hardcodes moe_enabled=True and requires expert_count / expert_used_count /
expert_feed_forward_length, which dense checkpoints do not carry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant