Skip to content

Commit 698365f

Browse files
justinchubyCopilot
andcommitted
Decouple attention_k_eq_v from num_global_key_value_heads
Decouple two independent Gemma4 features: - num_global_key_value_heads: controls KV head count for full-attention layers. Now used whenever set (not None), regardless of attention_k_eq_v. - attention_k_eq_v: controls V=K sharing (no v_proj). Gated separately. Previously both were coupled: num_global_key_value_heads only took effect when attention_k_eq_v was True. This was incorrect — a model could have different KV head counts for global/local layers without sharing V=K. Changes: - gemma4.py: Use num_global_key_value_heads for full-attention layers whenever it's not None - _gemma4.py: Same decoupling in _make_gemma4_kv_cache_inputs - build_graph_test.py: Add test with k_eq_v=True and num_global_key_value_heads != num_key_value_heads, verifying no v_proj and correct KV cache shapes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Justin Chu <justinchu@microsoft.com>
1 parent 096fc29 commit 698365f

3 files changed

Lines changed: 83 additions & 14 deletions

File tree

src/mobius/models/gemma4.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -607,11 +607,12 @@ def __init__(
607607
self._use_alternative_attention = (
608608
getattr(config, "attention_k_eq_v", False) and not is_sliding
609609
)
610-
self.num_key_value_heads = (
611-
(config.num_global_key_value_heads or config.num_key_value_heads)
612-
if self._use_alternative_attention
613-
else config.num_key_value_heads
614-
)
610+
# Full-attention layers use num_global_key_value_heads when set,
611+
# independent of the k_eq_v flag.
612+
if not is_sliding and config.num_global_key_value_heads is not None:
613+
self.num_key_value_heads = config.num_global_key_value_heads
614+
else:
615+
self.num_key_value_heads = config.num_key_value_heads
615616

616617
# KV sharing: layers >= first_kv_shared_layer_idx borrow K,V from source
617618
self.is_kv_shared_layer = layer_idx >= first_kv_shared_layer_idx > 0

src/mobius/tasks/_gemma4.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ def _make_gemma4_kv_cache_inputs(
5454
``num_hidden_layers - num_kv_shared_layers`` entries are created.
5555
5656
All layers use the original ``num_key_value_heads`` except
57-
alternative-attention (``attention_k_eq_v``) full layers which use
58-
``num_global_key_value_heads`` (fewer KV heads).
57+
full-attention layers when ``num_global_key_value_heads`` is set
58+
(fewer KV heads, independent of the ``attention_k_eq_v`` flag).
5959
The Attention op supports GQA head counts natively. CUDA EP limitations
6060
are tracked in microsoft/onnxruntime#28195 and #28196.
6161
"""
@@ -75,13 +75,13 @@ def _make_gemma4_kv_cache_inputs(
7575
for i in range(num_kv_layers):
7676
layer_type = layer_types[i] if i < len(layer_types) else "sliding_attention"
7777
hd = global_head_dim if layer_type == "full_attention" else local_head_dim
78-
# Alternative attention (k_eq_v) uses fewer KV heads for full_attention
79-
use_alt = getattr(config, "attention_k_eq_v", False) and layer_type == "full_attention"
80-
kv_heads = (
81-
(config.num_global_key_value_heads or config.num_key_value_heads)
82-
if use_alt
83-
else config.num_key_value_heads
84-
)
78+
# Full-attention layers use num_global_key_value_heads when set,
79+
# independent of the k_eq_v flag.
80+
is_full = layer_type == "full_attention"
81+
if is_full and config.num_global_key_value_heads is not None:
82+
kv_heads = config.num_global_key_value_heads
83+
else:
84+
kv_heads = config.num_key_value_heads
8585
past_key = builder.input(
8686
f"past_key_values.{i}.key",
8787
dtype=config.dtype,

tests/build_graph_test.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1415,6 +1415,74 @@ def test_gemma4_kv_shared_layer_tracing(self):
14151415
assert "present.1.key" not in output_names
14161416
assert "present.1.value" not in output_names
14171417

1418+
def test_gemma4_k_eq_v_with_global_kv_heads(self):
1419+
"""Verify attention_k_eq_v removes v_proj and num_global_key_value_heads sets KV cache shapes.
1420+
1421+
Config: attention_k_eq_v=True, num_key_value_heads=4 (sliding),
1422+
num_global_key_value_heads=2 (full). Full-attention layers should:
1423+
- Have no v_proj initializer (V=K)
1424+
- Use num_global_key_value_heads=2 for KV cache shapes
1425+
Sliding layers should use num_key_value_heads=4.
1426+
"""
1427+
from mobius._configs import Gemma4Config
1428+
from mobius.models.gemma4 import Gemma4CausalLMModel
1429+
from mobius.tasks._gemma4 import Gemma4TextCausalLMTask
1430+
1431+
config = Gemma4Config(
1432+
num_hidden_layers=2,
1433+
hidden_size=64,
1434+
intermediate_size=128,
1435+
num_attention_heads=4,
1436+
num_key_value_heads=4,
1437+
head_dim=16,
1438+
vocab_size=256,
1439+
rms_norm_eps=1e-6,
1440+
hidden_act="silu",
1441+
attn_qk_norm=True,
1442+
# Layer 0: sliding, Layer 1: full (k_eq_v + global heads)
1443+
layer_types=["sliding_attention", "full_attention"],
1444+
sliding_window=8,
1445+
global_head_dim=16,
1446+
global_rope_theta=10_000.0,
1447+
global_partial_rotary_factor=0.25,
1448+
final_logit_softcapping=0.0,
1449+
hidden_size_per_layer_input=0,
1450+
image_token_id=255999,
1451+
pad_token_id=0,
1452+
tie_word_embeddings=True,
1453+
attention_k_eq_v=True,
1454+
num_global_key_value_heads=2,
1455+
)
1456+
module = Gemma4CausalLMModel(config)
1457+
task = Gemma4TextCausalLMTask()
1458+
pkg = task.build(module, config)
1459+
decoder = pkg["model"]
1460+
1461+
# Check initializer names: full-attention layer (1) should have no v_proj
1462+
init_names = set(decoder.graph.initializers)
1463+
# Sliding layer 0 has k_proj, v_proj
1464+
assert "model.layers.0.self_attn.k_proj.weight" in init_names
1465+
assert "model.layers.0.self_attn.v_proj.weight" in init_names
1466+
# Full layer 1 has k_proj but NO v_proj (k_eq_v: V=K)
1467+
assert "model.layers.1.self_attn.k_proj.weight" in init_names
1468+
assert "model.layers.1.self_attn.v_proj.weight" not in init_names
1469+
1470+
# KV cache shapes:
1471+
# Layer 0 (sliding): num_key_value_heads=4
1472+
# Layer 1 (full): num_global_key_value_heads=2
1473+
input_shapes = {i.name: list(i.shape) for i in decoder.graph.inputs}
1474+
# Layer 0: kv_heads=4
1475+
layer0_key_shape = input_shapes["past_key_values.0.key"]
1476+
assert layer0_key_shape[1] == 4, (
1477+
f"Sliding layer 0 should have 4 KV heads, got {layer0_key_shape[1]}"
1478+
)
1479+
# Layer 1: kv_heads=2 (num_global_key_value_heads)
1480+
layer1_key_shape = input_shapes["past_key_values.1.key"]
1481+
assert layer1_key_shape[1] == 2, (
1482+
f"Full layer 1 should have 2 KV heads "
1483+
f"(num_global_key_value_heads), got {layer1_key_shape[1]}"
1484+
)
1485+
14181486
def test_blip2_vision_language_graph(self):
14191487
"""Build BLIP-2 with ViT + Q-Former + LLM 3-model split."""
14201488
config = _base_config(

0 commit comments

Comments
 (0)