Fix DefaultKeyValueCache: per-layer num_kv_heads for Gemma 4 dual/MQA attention - #2214
Merged
kunal-vaishnavi merged 2 commits intoJun 18, 2026
Conversation
Contributor
There was a problem hiding this comment.
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_headsandhead_dimfrom ONNX input shapes instead of onlyhead_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
shreyshah-microsoft
force-pushed
the
fix/kv-cache-per-layer-num-kv-heads
branch
from
June 11, 2026 01:19
765ee7e to
3b43e0e
Compare
shreyshah-microsoft
force-pushed
the
fix/kv-cache-per-layer-num-kv-heads
branch
from
June 11, 2026 22:12
5e03fd8 to
6188342
Compare
Contributor
|
/azp run Integration Tests |
|
Azure Pipelines successfully started running 1 pipeline(s). |
shreyshah-microsoft
force-pushed
the
fix/kv-cache-per-layer-num-kv-heads
branch
from
June 15, 2026 20:20
6188342 to
ddf2aab
Compare
…/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
force-pushed
the
fix/kv-cache-per-layer-num-kv-heads
branch
from
June 16, 2026 18:29
ddf2aab to
ecc3e5f
Compare
kunal-vaishnavi
approved these changes
Jun 18, 2026
Contributor
Author
@microsoft-github-policy-service agree company="Microsoft" |
This was referenced Aug 1, 2026
Bump Microsoft.ML.OnnxRuntimeGenAI and Microsoft.ML.OnnxRuntimeGenAI.Managed
trackdubllc/Trackdub#53
Merged
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Loading and running Gemma 4 (
gemma4/gemma4_unified, e.g. the 12B) on the CPU EP fails at prefill:Root cause
DefaultKeyValueCachealready auto-detects a per-layer head_dim (shape index 3) from eachpast_key_values.*input shape (added with Gemma 4 support for its dual head_dim). But it keeps asingle uniform
num_key_value_heads(shape index 1) for every layer. Gemma 4 uses a dual attentionpattern:
num_key_value_heads = 8,head_dim = 256num_key_value_heads = 1,head_dim = 512So
layer_shapes_ends up with the right per-layer head_dim but the uniform KV-head count. Theper-layer empty/present KV tensors for the global layers are then allocated as
[B, 8, 0, 512]whilethe 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 triggerper-layer allocation when either
num_kv_headsorhead_dimvaries (setlayer_shapes_[i][1]).The change propagates through
empty_pasts_,presents_,Add()andUpdate(). Uniform-KV modelsare unaffected — with no variation,
has_varying_shapestays false and the original single-shape pathis used. Guarded with
input_shape.size() >= 4.Testing
gemma4_unified) now loads and decodes on the CPU EP (previously failed at prefill).transformersexactly on a 3-prompt suite:last-position logit cosine 1.0000, top-1 match, greedy 12/12 identical tokens.
per-layer KV shapes actually differ.
Note: a regression pass over the
RewindTo/past_present_share_buffer/ sampling paths isrecommended before merge; this change was validated on the standard prefill + greedy-decode path.