inference/research
verifiedhigh · 0.93blockerupdated 2026-10-05

A hybrid model's KV cost depends entirely on how many layers are full attention, and when config gives a GROUP SIZE rather than an index list, reading the group size as the layer count inflates the KV figure several-fold

[blocker] config

Fields

id
gotchas/mdl-gotcha-hybrid-attention-layer-count-from-a-rule-not-the-name
Class
config
Severity
blocker
Concepts
kv-cachehybrid-attentionssmmambakdalinear-attentionmlalong-contextmemory-planning
Symptom
ling-3-0-flash recorded kv_cache_bytes_per_token 23040, overstating the real figure 8064 by 2.86x (20 attention layers used where 7 are real). The record had attention_layer_indices and layer_types left null, which is the visible symptom: an author could not find the layer list in config and substituted a number that looked plausible. The consequence was not cosmetic. Ling-3.0-Flash advertises max_position_embeddings 262144; at 23040 B/token one 256k sequence needs 5.60 GiB of KV, at 8064 it needs 2.00 GiB. The old figure is the difference between a 256k request fitting alongside the weights on a single 80GB-class card and not fitting at all, so the error inverted the answer to the only question this field exists to answer.
Root cause
config.json for this family does not enumerate the attention layers. It publishes num_hidden_layers 42 and layer_group_size 6, and the split is implied by a rule implemented in modeling_bailing_moe_v3.py at the layer-construction site: attention_layer_type = 'attention' if (layer_idx + 1) % layer_group_size == 0 or layer_idx >= num_hidden_layers // layer_group_size * layer_group_size else 'linear_attention'. Over 42 layers at group size 6 that yields attention at 0-based indices 5, 11, 17, 23, 29, 35, 41 - seven layers. The recorded 20 is a different config field entirely: max_window_layers is 20, which is a WINDOW cap on the attention layers, not a count of them. So the failure is a field-semantics mix-up between two integers in the same file, both small and both plausible as a layer count. The same trap is waiting in every hybrid in this repo: config publishes at least three integers that look like layer counts (num_hidden_layers, max_window_layers, and a group size or interval), and only one of them is the answer.
Workaround
When config gives a group size, interval or pattern instead of an index list, derive the index list from the rule in the modeling code and RECORD it in attention_layer_indices and layer_types rather than leaving them null. A null split on a hybrid record is the tell that the KV figure was not derived. Cross-check against a sibling: kimi-linear-48b-a3b is the same KDA+MLA design and its config DOES enumerate linear_attn_config.full_attn_layers [4,8,12,16,20,24,27] = 7 of 27 layers, giving 7 x 576 x 2 = 8,064 B/token - identical to the corrected Ling figure, which is the consistency check that identified 23,040 as underivable. For weight-level confirmation without modeling code, count layers carrying q_proj in model.safetensors.index.json.
notes
CORRECTED ARITHMETIC for ling-3-0-flash: 7 attention layers x (kv_lora_rank 512 + qk_rope_head_dim 64) x 2 bytes (bf16) = 7 x 576 x 2 = 8,064 B/token (7.9 KiB/token). attention_layer_indices [5,11,17,23,29,35,41] and a 42-entry layer_types are now recorded, where both were previously null. FAMILY-WIDE CHECK, every hybrid in this repo re-derived from its own config: kimi-k3 (linear_attn_config.full_attn_layers = 24 of 93) 24 x 576 x 2 = 27,648 CORRECT as recorded; kimi-linear-48b-a3b 7 of 27 = 8,064 CORRECT; granite-4-0-h-tiny (layer_types, attention at 5,15,25,35 = 4 of 40) 4 x 4 x 128 x 2 x 2 = 8,192 CORRECT as recorded, and this record is the counter-example that proves the head-dim lesson was already learned once: its head_dim 128 comes from hidden_size 1536 / num_attention_heads 12 = 128, not from the string 'h-tiny' in the name; nemotron-h-8b-base-8k 4 of 52 = 16,384 CORRECT, with the split confirmed against the safetensors index (backbone.layers.7/18/29/40 carry mixer.q_proj, all 52 carry mixer.*) because that config's hybrid_override_pattern string is TRUNCATED at 25 characters and does not itself cover 52 layers; lfm2-1-2b and lfm2-5-1-2b-instruct (full_attn_idxs [2,5,8,10,12,14] = 6 of 16) correctly record 0 for a bounded-not-growing cache with the 12,288 B/token attention slice spelled out in notes; zamba-7b (layers_block_type 'hybrid' at 2,7,13,19,25,31,37,43,49,55,61,67,73 = 13 of 76) 2 x 13 x 16 x 232 x 2 = 193,024 CORRECT as recorded; falcon-h1-7b-base is the deliberate exception - config sets attn_layer_indices null and the safetensors index shows every one of 44 layers carrying BOTH self_attn and mamba tensors, so attention runs in PARALLEL with the SSM on every layer, attention_layer_indices [0..43] is correct, and 2 x 44 x 2 x 128 x 2 = 45,056 stands. That last one is the mirror image of this bug: for a parallel hybrid the KV cache does NOT shrink, and a reader who assumed 'hybrid implies few attention layers' would under-budget it by the full 44/4 factor. THE TWO CONVENTIONS IN THIS REPO, WHICH LOOK LIKE AN INCONSISTENCY BUT ARE NOT, AND WHICH AN AUDITOR MUST KNOW BEFORE RECOMPUTING ANY OF THESE FIGURES. Class 1 - linear/SSM hybrids (Gated DeltaNet, KDA, Mamba): the non-attention layers hold a FIXED-SIZE recurrent state and no growing KV cache at all, so the per-token figure counts ONLY the attention layers. Every record in this class does this: kimi-k3 24 of 93 = 27,648; kimi-linear 7 of 27 = 8,064; qwen3-5-35b-a3b 10 of 40 = 20,480; qwen3-next-80b-a3b-instruct 12 of 48 = 24,576; granite-4-0-h-tiny 4 of 40 = 8,192; nemotron-h 4 of 52 = 16,384; and the corrected ling-3-0-flash 7 of 42 = 8,064. Class 2 - sliding-window hybrids where EVERY layer still carries a KV cache (Gemma 2, Gemma 3n, GPT-OSS, OLMo 3, Ministral): the per-token figure counts ALL layers, i.e. the unbounded worst case, and each of those records states the sliding-window caveat in its notes so a reader can discount it. gemma-2-9b 42 x 8 x 256 x 2 x 2 = 344,064 rather than 172,032; gpt-oss-20b 24 rather than 12 layers; olmo-3-7b 32 rather than 8; ministral-8b 36 rather than 9; gemma-3n-e2b 30 rather than 6. A recomputation that silently applies Class 1 logic to a Class 2 model will report a 2x-5x 'mismatch' that is really just the two conventions meeting, and the correct response is to check the notes rather than to change the number. Class 2 is the right choice there because the window is a runtime setting an operator may widen or disable, so the per-token figure should describe the ceiling. The genuinely wrong records are the ones where the count is not derivable from config under EITHER convention - ling-3-0-flash's 20 came from max_window_layers, a third field entirely, and matched neither.

Notes

CORRECTED ARITHMETIC for ling-3-0-flash: 7 attention layers x (kv_lora_rank 512 + qk_rope_head_dim 64) x 2 bytes (bf16) = 7 x 576 x 2 = 8,064 B/token (7.9 KiB/token). attention_layer_indices [5,11,17,23,29,35,41] and a 42-entry layer_types are now recorded, where both were previously null. FAMILY-WIDE CHECK, every hybrid in this repo re-derived from its own config: kimi-k3 (linear_attn_config.full_attn_layers = 24 of 93) 24 x 576 x 2 = 27,648 CORRECT as recorded; kimi-linear-48b-a3b 7 of 27 = 8,064 CORRECT; granite-4-0-h-tiny (layer_types, attention at 5,15,25,35 = 4 of 40) 4 x 4 x 128 x 2 x 2 = 8,192 CORRECT as recorded, and this record is the counter-example that proves the head-dim lesson was already learned once: its head_dim 128 comes from hidden_size 1536 / num_attention_heads 12 = 128, not from the string 'h-tiny' in the name; nemotron-h-8b-base-8k 4 of 52 = 16,384 CORRECT, with the split confirmed against the safetensors index (backbone.layers.7/18/29/40 carry mixer.q_proj, all 52 carry mixer.*) because that config's hybrid_override_pattern string is TRUNCATED at 25 characters and does not itself cover 52 layers; lfm2-1-2b and lfm2-5-1-2b-instruct (full_attn_idxs [2,5,8,10,12,14] = 6 of 16) correctly record 0 for a bounded-not-growing cache with the 12,288 B/token attention slice spelled out in notes; zamba-7b (layers_block_type 'hybrid' at 2,7,13,19,25,31,37,43,49,55,61,67,73 = 13 of 76) 2 x 13 x 16 x 232 x 2 = 193,024 CORRECT as recorded; falcon-h1-7b-base is the deliberate exception - config sets attn_layer_indices null and the safetensors index shows every one of 44 layers carrying BOTH self_attn and mamba tensors, so attention runs in PARALLEL with the SSM on every layer, attention_layer_indices [0..43] is correct, and 2 x 44 x 2 x 128 x 2 = 45,056 stands. That last one is the mirror image of this bug: for a parallel hybrid the KV cache does NOT shrink, and a reader who assumed 'hybrid implies few attention layers' would under-budget it by the full 44/4 factor.

THE TWO CONVENTIONS IN THIS REPO, WHICH LOOK LIKE AN INCONSISTENCY BUT ARE NOT, AND WHICH AN AUDITOR MUST KNOW BEFORE RECOMPUTING ANY OF THESE FIGURES. Class 1 - linear/SSM hybrids (Gated DeltaNet, KDA, Mamba): the non-attention layers hold a FIXED-SIZE recurrent state and no growing KV cache at all, so the per-token figure counts ONLY the attention layers. Every record in this class does this: kimi-k3 24 of 93 = 27,648; kimi-linear 7 of 27 = 8,064; qwen3-5-35b-a3b 10 of 40 = 20,480; qwen3-next-80b-a3b-instruct 12 of 48 = 24,576; granite-4-0-h-tiny 4 of 40 = 8,192; nemotron-h 4 of 52 = 16,384; and the corrected ling-3-0-flash 7 of 42 = 8,064. Class 2 - sliding-window hybrids where EVERY layer still carries a KV cache (Gemma 2, Gemma 3n, GPT-OSS, OLMo 3, Ministral): the per-token figure counts ALL layers, i.e. the unbounded worst case, and each of those records states the sliding-window caveat in its notes so a reader can discount it. gemma-2-9b 42 x 8 x 256 x 2 x 2 = 344,064 rather than 172,032; gpt-oss-20b 24 rather than 12 layers; olmo-3-7b 32 rather than 8; ministral-8b 36 rather than 9; gemma-3n-e2b 30 rather than 6. A recomputation that silently applies Class 1 logic to a Class 2 model will report a 2x-5x 'mismatch' that is really just the two conventions meeting, and the correct response is to check the notes rather than to change the number. Class 2 is the right choice there because the window is a runtime setting an operator may widen or disable, so the per-token figure should describe the ceiling. The genuinely wrong records are the ones where the count is not derivable from config under EITHER convention - ling-3-0-flash's 20 came from max_window_layers, a third field entirely, and matched neither.

Cross-references

This record references no other records — it is a leaf. It is cited by 0.