Skip to content

Fix DefaultKeyValueCache: per-layer num_kv_heads for Gemma 4 dual/MQA attention - #2214

Merged
kunal-vaishnavi merged 2 commits into
microsoft:mainfrom
shreyshah-microsoft:fix/kv-cache-per-layer-num-kv-heads
Jun 18, 2026
Merged

Fix DefaultKeyValueCache: per-layer num_kv_heads for Gemma 4 dual/MQA attention#2214
kunal-vaishnavi merged 2 commits into
microsoft:mainfrom
shreyshah-microsoft:fix/kv-cache-per-layer-num-kv-heads

Conversation

@shreyshah-microsoft

Copy link
Copy Markdown
Contributor

Problem

Loading and running Gemma 4 (gemma4 / gemma4_unified, e.g. the 12B) on the CPU EP fails at prefill:

RuntimeError: Got invalid dimensions for input: past_key_values.5.key
 index: 1 Got: 8 Expected: 1

Root cause

DefaultKeyValueCache already auto-detects a per-layer head_dim (shape index 3) from each
past_key_values.* input shape (added with Gemma 4 support for its dual head_dim). But it keeps a
single uniform num_key_value_heads (shape index 1) for every layer. Gemma 4 uses a dual attention
pattern:

  • sliding-window layers: GQA — num_key_value_heads = 8, head_dim = 256
  • global/full-attention layers: MQA — num_key_value_heads = 1, head_dim = 512

So layer_shapes_ ends up with the right per-layer head_dim but the uniform KV-head count. The
per-layer empty/present KV tensors for the global layers are then allocated as [B, 8, 0, 512] while
the decoder graph expects [B, 1, 0, 512] — the index-1 mismatch above.

Fix

Extend the existing per-layer detection to also read num_kv_heads (input shape index 1), and trigger
per-layer allocation when either num_kv_heads or head_dim varies (set layer_shapes_[i][1]).
The change propagates through empty_pasts_, presents_, Add() and Update(). Uniform-KV models
are unaffected — with no variation, has_varying_shape stays false and the original single-shape path
is used. Guarded with input_shape.size() >= 4.

Testing

  • Gemma 4 12B (gemma4_unified) now loads and decodes on the CPU EP (previously failed at prefill).
  • An fp32 ONNX export of the model matches HuggingFace transformers exactly on a 3-prompt suite:
    last-position logit cosine 1.0000, top-1 match, greedy 12/12 identical tokens.
  • Uniform-KV architectures (Llama / Gemma 3 / etc.) are unchanged — the new branch only activates when
    per-layer KV shapes actually differ.

Note: a regression pass over the RewindTo / past_present_share_buffer / sampling paths is
recommended before merge; this change was validated on the standard prefill + greedy-decode path.

@shreyshah-microsoft
shreyshah-microsoft marked this pull request as ready for review June 11, 2026 01:11
@shreyshah-microsoft
shreyshah-microsoft requested a review from a team as a code owner June 11, 2026 01:11
Copilot AI review requested due to automatic review settings June 11, 2026 01:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Updates KV-cache initialization to detect and support per-layer variation in KV tensor shapes (both num_kv_heads and head_dim) based on ONNX session input shapes, fixing model variants where global-attention layers use MQA (single KV head).

Changes:

  • Detect per-layer num_kv_heads and head_dim from ONNX input shapes instead of only head_dim.
  • Populate layer_shapes_ with per-layer KV shape overrides and create per-layer empty past tensors accordingly.
  • Improve logging and inline documentation to reflect the expanded shape handling.
Show a summary per file
File Description
src/models/kv_cache.cpp Extends per-layer KV cache shape detection to include both KV head count and head dimension.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 2

Comment thread src/models/kv_cache.cpp Outdated
Comment thread src/models/kv_cache.cpp Outdated
@shreyshah-microsoft
shreyshah-microsoft force-pushed the fix/kv-cache-per-layer-num-kv-heads branch from 765ee7e to 3b43e0e Compare June 11, 2026 01:19
Comment thread src/models/kv_cache.cpp Outdated
Comment thread src/models/kv_cache.cpp Outdated
@shreyshah-microsoft
shreyshah-microsoft force-pushed the fix/kv-cache-per-layer-num-kv-heads branch from 5e03fd8 to 6188342 Compare June 11, 2026 22:12
@kunal-vaishnavi

Copy link
Copy Markdown
Contributor

/azp run Integration Tests

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@shreyshah-microsoft
shreyshah-microsoft force-pushed the fix/kv-cache-per-layer-num-kv-heads branch from 6188342 to ddf2aab Compare June 15, 2026 20:20
…/MQA attention)

DefaultKeyValueCache auto-detected a per-layer head_dim (shape index 3) from the
past_key_values.* input shapes, but kept a single uniform num_key_value_heads
(shape index 1) for every layer. Gemma 4 uses a dual attention pattern: the
global/full-attention layers are MQA (num_key_value_heads=1, head_dim=512) while
the sliding-window layers use GQA (num_key_value_heads=8, head_dim=256). With a
uniform KV-head count, the per-layer empty/present KV tensors for the global
layers were allocated with the wrong head count, so prefill failed:

  RuntimeError: Got invalid dimensions for input: past_key_values.5.key
   index: 1 Got: 8 Expected: 1

Extend the existing per-layer detection to also read num_kv_heads (input shape
index 1), and trigger per-layer allocation when either num_kv_heads or head_dim
varies. The change propagates through empty_pasts_, presents_, Add() and
Update(); uniform-KV models are unaffected (no variation falls back to the
original single-shape path). Guarded with input_shape.size() >= 4.

Validated on google/gemma-4-12B (gemma4_unified): with this fix the model loads
and decodes on the CPU EP, and an fp32 export matches HuggingFace transformers
exactly (last-position logit cosine 1.0000, greedy 12/12) on a 3-prompt suite.
Per review feedback: num_kv_heads/head_dim are static per-layer model
properties known ahead of time, and shape_ already holds the config defaults
(decoder.num_key_value_heads / .head_size), so the per-axis distinct-value
tracking is unnecessary. Collapse it into a single has_per_layer_variation
flag that compares each layer's KV shape to shape_. Also tighten the KV-rank
guard from `>= 4` to `== 4` (KV tensors are always rank-4
[batch, num_kv_heads, seq, head_dim]).

No functional change: parity on Gemma-4 12B INT4 vs the HF instruct oracle is
byte-identical to the prior commit (cosine 0.987/0.974/0.987, top-1 all,
greedy 9/9, 12/12, 8/12).
@shreyshah-microsoft
shreyshah-microsoft force-pushed the fix/kv-cache-per-layer-num-kv-heads branch from ddf2aab to ecc3e5f Compare June 16, 2026 18:29
@shreyshah-microsoft

shreyshah-microsoft commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

shreyshah-microsoft please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.

@microsoft-github-policy-service agree [company="{your company}"]

Options:

  • (default - no company specified) I have sole ownership of intellectual property rights to my Submissions and I am not making Submissions in the course of work for my employer.
@microsoft-github-policy-service agree
  • (when company given) I am making Submissions in the course of work for my employer (or my employer has intellectual property rights in my Submissions by contract or applicable law). I have permission from my employer to make Submissions and enter into this Agreement on behalf of my employer. By signing below, the defined term “You” includes me and my employer.
@microsoft-github-policy-service agree company="Microsoft"

Contributor License Agreement

@microsoft-github-policy-service agree company="Microsoft"

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.

3 participants