-
Notifications
You must be signed in to change notification settings - Fork 19.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add GroupedQueryAttention
layer
#18488
Conversation
Codecov ReportAttention:
Additional details and impacted files@@ Coverage Diff @@
## master #18488 +/- ##
==========================================
+ Coverage 77.40% 78.36% +0.95%
==========================================
Files 331 335 +4
Lines 31972 32600 +628
Branches 6241 6355 +114
==========================================
+ Hits 24749 25548 +799
+ Misses 5646 5486 -160
+ Partials 1577 1566 -11
Flags with carried forward coverage won't be shown. Click here to find out more.
☔ View full report in Codecov by Sentry. |
Hello @mattdangerw, I would appreciate your valuable feedback on the implementation. I used llamav2 as a reference for this layer but also used I did observe some key differences between the llamav2 implementation and the Keras
Additionally, I haven't yet included the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR! Left some initial high level comments.
Similar to multi-head attention
The code / API looks good to me (would like @mattdangerw to LGTM it though). Please add unit tests! |
mqa -> multi query attention gqa -> grouped query attention mha -> multi head attention
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the delay! Overall this looks good to me, just a few last comments.
Do we know of a reference layer somewhere in a torch or jax lib we could use to sanity check our numerics?
What happens if `num_query_heads` is not divisible by `num_key_value_heads`?
use different letters to denote `num_query_heads` vs `num_key_value_heads`
I tried to match numeric result of PyTorch (simple ver. of llama-v2) with this impl. but I am having trouble initialing them with same weights which I think is essential for testing. Probably it's due to Here is the pytorch code I'm using, which is simpler ver. of llama-v2: import numpy as np
import torch
import math
import torch.nn.functional as F
class GroupedQueryAttentionTorch(nn.Module):
"""Multi-head attention module."""
def __init__(self, dim, head_dim, n_kv_heads, n_q_heads):
"""Initialize the Attention module.
Args:
dim (int): Hidden dimension size
n_kv_heads (int): Number of key and value heads
n_q_heads (int): Number of query heads
"""
super().__init__()
self.n_kv_heads = n_kv_heads
self.n_q_heads = n_q_heads
self.n_rep = self.n_q_heads // self.n_kv_heads
self.dim = dim
self.head_dim = head_dim
self.wq = nn.Linear(dim, n_q_heads * self.head_dim,
bias=False,)
self.wk = nn.Linear(dim, n_kv_heads * self.head_dim,
bias=False)
self.wv = nn.Linear(dim, n_kv_heads * self.head_dim,
bias=False)
self.wo = nn.Linear(n_q_heads * self.head_dim, dim,
bias=False)
def forward(
self,
x,
mask,
):
bsz, seqlen, _ = x.shape
xq, xk, xv = self.wq(x), self.wk(x), self.wv(x)
xq = xq.view(bsz, seqlen, self.n_q_heads, self.head_dim)
xk = xk.view(bsz, seqlen, self.n_kv_heads, self.head_dim)
xv = xv.view(bsz, seqlen, self.n_kv_heads, self.head_dim)
# repeat k/v heads if n_kv_heads < n_q_heads
key = self.repeat_kv(xk, self.n_rep) # (bs, seqlen, n_q_heads, head_dim)
value = self.repeat_kv(xv, self.n_rep) # (bs, seqlen, n_q_heads, head_dim)
query = xq.transpose(1, 2) / math.sqrt(self.head_dim) # (bs, n_q_heads, seqlen, head_dim)
key = key.transpose(1, 2)
value = value.transpose(1, 2)
scores = torch.matmul(query, key.transpose(2, 3))
if mask is not None:
scores = scores + mask # (bs, n_q_heads, seqlen, cache_len + seqlen)
scores = F.softmax(scores.float(), dim=-1).type_as(xq)
output = torch.matmul(scores, value) # (bs, n_q_heads, seqlen, head_dim)
output = output.transpose(1, 2).contiguous().view(bsz, seqlen, -1)
return self.wo(output)
def repeat_kv(self, x: torch.Tensor, n_rep: int) -> torch.Tensor:
"""torch.repeat_interleave(x, dim=2, repeats=n_rep)"""
bs, slen, n_kv_heads, head_dim = x.shape
if n_rep == 1:
return x
return (
x[:, :, :, None, :]
.expand(bs, slen, n_kv_heads, n_rep, head_dim)
.reshape(bs, slen, n_kv_heads * n_rep, head_dim)
) |
Do you mean the weight layout is different? What are the different weight matrixes and their shapes? |
@fchollet Thanks or your help with this query! After some digging, I noticed an interesting difference in the weight shapes between After that, I ran a numeric comparison of GQA between Keras (TensorFlow backend) and PyTorch version (simpler version of llama-v2; code above) and it matched up perfectly. I used the following code to test, # Define parameters and data
head_dim = 2
n_q_heads = 3 * 2
n_kv_heads = 3
batch_size = 2
seq_len = 4
dim = 16
# Instantiate the torch GQA
gqa_torch = GroupedQueryAttentionTorch(dim=dim,
head_dim=head_dim,
n_kv_heads=n_kv_heads,
n_q_heads=n_q_heads)
# Manually initialize PyTorch model weights to known values
init_tensor1 = torch.rand(n_q_heads * head_dim, dim)
init_tensor2 = torch.rand(n_kv_heads * head_dim, dim)
init_tensor3 = torch.rand(dim, n_q_heads * head_dim)
gqa_torch.wq.weight = nn.Parameter(init_tensor1.clone())
gqa_torch.wk.weight = nn.Parameter(init_tensor2.clone())
gqa_torch.wv.weight = nn.Parameter(init_tensor2.clone())
gqa_torch.wo.weight = nn.Parameter(init_tensor3.clone())
# Generate some example input data
inputs = torch.randn(batch_size, seq_len, dim)
mask = None # You can add a mask tensor here if needed
# Forward Pass
output_torch = gqa_torch(inputs, mask).detach().numpy()
print(output_torch.shape)
# Instantiate the keras GQA
gqa_keras = GroupedQueryAttentionKeras(head_dim=head_dim,
num_query_heads=n_q_heads,
num_key_value_heads=n_kv_heads)
gqa_keras.build((batch_size, seq_len, dim), (batch_size, seq_len, dim))
# Manually initialize Keras model weights to same known values
gqa_keras._query_dense.kernel = tf.Variable(init_tensor1.numpy().T.reshape(dim, n_q_heads, head_dim))
gqa_keras._key_dense.kernel = tf.Variable(init_tensor2.numpy().T.reshape(dim, n_kv_heads, head_dim))
gqa_keras._value_dense.kernel = tf.Variable(init_tensor2.numpy().T.reshape(dim, n_kv_heads, head_dim))
gqa_keras._output_dense.kernel = tf.Variable(init_tensor3.numpy().T.reshape(n_q_heads, head_dim, dim))
# Generate inputs
inputs = tf.convert_to_tensor(inputs.numpy())
mask = None # You can add a mask tensor here if needed
# Forward Pass
output_keras = gqa_keras(inputs, inputs, attention_mask=mask).numpy()
print(output_keras.shape)
# Test if torch keras result match
np.testing.assert_allclose(output_keras, output_torch, rtol=1e-5, atol=1e-5) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM -- thank you for the great contribution! 👍
Thanks for your feedbacks @mattdangerw and @fchollet. I hope this layer helps the community to build LLMs more easily. |
This PR corresponds to Issue: #18402.