# Bug report draft — `num_kv_shared_layers == 0` silently disables hybrid cache layer typing **Repo:** huggingface/transformers · **Type:** bug (silent correctness/perf regression) ## Title `DynamicCache(config=...)` (and `get_head_shapes`) drop all typed layers when `num_kv_shared_layers == 0` — sliding-window layers stop windowing ## Summary When a decoder config exposes `num_kv_shared_layers` **set to 0** (e.g. `google/gemma-4-12B-it`), `DynamicCache.__init__` computes `layer_types[: -num_kv_shared_layers]`, which for `n == 0` is `layer_types[:-0]` → **an empty list**. The per-layer type loop then runs zero times, so no `DynamicSlidingWindowLayer`s are created; every layer lazily falls back to a plain full-storage `DynamicLayer`. The **sliding-attention layers silently stop windowing**, so KV memory grows with the full sequence length instead of being capped at the window. For gemma-4-12B (40 sliding layers, window 1024, at 256k context) this is the difference between **~5 GB and ~90 GB** of KV cache — i.e. it silently makes long-context generation OOM on hardware that should handle it. The identical slicing pattern appears in `transformers/integrations/executorch.py::get_head_shapes`, where it yields empty `num_heads`/`head_dim` lists and raises `ValueError: num_head was provided as a list of length 0, but the Cache currently has N layers`. ## Offending code `src/transformers/cache_utils.py`, `DynamicCache.__init__`: ```python # Some models have shared layers thus no cache is needed for them (e.g. Gemma3n) if hasattr(decoder_config, "num_kv_shared_layers"): layer_types = layer_types[: -decoder_config.num_kv_shared_layers] # n == 0 -> [] (empty!) ``` `src/transformers/integrations/executorch.py`, `get_head_shapes`: ```python head_dim = [... for layer in config.layer_types[: -config.num_kv_shared_layers]] # same bug num_heads = [... for layer in config.layer_types[: -config.num_kv_shared_layers]] ``` ## Minimal repro ```python from transformers import AutoConfig from transformers.cache_utils import DynamicCache, DynamicSlidingWindowLayer cfg = AutoConfig.from_pretrained("google/gemma-4-12B-it").get_text_config() print("num_kv_shared_layers:", cfg.num_kv_shared_layers) # -> 0 print("layer_types:", len(cfg.layer_types), cfg.layer_types[:2]) # -> 48 ['sliding_attention', ...] cache = DynamicCache(config=cfg) print("typed layers built from config:", len(cache.layers)) # -> 0 (BUG; expected 48) # after a forward, every layer lazily becomes a plain DynamicLayer, so sliding layers never window: # none are DynamicSlidingWindowLayer, and KV for sliding layers grows unbounded with seq len. ``` Expected: 48 typed layers, with `sliding_attention` entries → `DynamicSlidingWindowLayer` (windowed). Actual: 0 typed layers built from config → all lazy `DynamicLayer` (full storage, no windowing). ## Suggested fix Guard the slice on a positive value (a config can legitimately carry `num_kv_shared_layers = 0`): ```python if getattr(decoder_config, "num_kv_shared_layers", 0): layer_types = layer_types[: -decoder_config.num_kv_shared_layers] ``` Apply the same guard in `get_head_shapes` (and anywhere else the `[:-num_kv_shared_layers]` idiom is used). A quick `grep -rn "num_kv_shared_layers\]" src/transformers` finds the sites. ## Environment Present on `main` (verified) and transformers 5.x. Triggered by any hybrid-attention model whose config sets `num_kv_shared_layers = 0` while defining `layer_types` (gemma-4-12B is one released example).