64 query heads share 8 KV heads (d_h=64) — full attention quality with a 8× smaller KV cache.
Standard causal self-attention with grouped queries: the 64 query heads
are split into 8 groups, and each group shares one K/V head. Per token
the KV cache stores 8 × 64 keys and values instead of
64 × 64 — at inference, KV-cache size (not FLOPs) is what limits
batch size and context, so this is nearly free quality-wise and hugely valuable.
The spectrum: MHA (kvHeads = heads, original), GQA (1 < kvHeads < heads),
MQA (kvHeads = 1, maximum sharing, some quality cost).
In this model:
- 64 query heads, 8 KV heads, d_h=64
- RoPE base θ=150000 with YaRN ×32 context extension
- no Q/K normalization
- projection biases enabled (uncommon in modern decoders)
- learned sink logits: each head has a trainable logit competing in the softmax, letting heads attend to 'nothing' instead of being forced to distribute weight over real tokens — replaces the first-token 'attention sink' hack
class GQA(nn.Module):
def __init__(self, d_model=2880, n_heads=64,
n_kv_heads=8, head_dim=64):
super().__init__()
self.q = nn.Linear(d_model, n_heads * head_dim, bias=True)
self.k = nn.Linear(d_model, n_kv_heads * head_dim, bias=True)
self.v = nn.Linear(d_model, n_kv_heads * head_dim, bias=True)
self.o = nn.Linear(n_heads * head_dim, d_model, bias=False)
self.groups = n_heads // n_kv_heads
self.sinks = nn.Parameter(torch.zeros(n_heads)) # learned sink logits
def forward(self, x, pos):
B, T, _ = x.shape
q = split_heads(self.q(x), 64) # [B, 64, T, 64]
k = split_heads(self.k(x), 8) # [B, 8, T, 64]
v = split_heads(self.v(x), 8)
q, k = apply_rope(q, pos), apply_rope(k, pos)
k, v = repeat_kv(k, self.groups), repeat_kv(v, self.groups)
out = F.scaled_dot_product_attention(q, k, v, is_causal=True)
return self.o(merge_heads(out)) # [B, T, 2880]