Skip to content

Commit 4136e38

Browse files
Fix attention score returns in CachedMultiHeadAttention (#2799)
* Fix attention score returns in cached MHA * Address cached attention test feedback
1 parent 8375767 commit 4136e38

2 files changed

Lines changed: 137 additions & 31 deletions

File tree

keras_hub/src/layers/modeling/cached_multi_head_attention.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,17 @@ class CachedMultiHeadAttention(keras.layers.MultiHeadAttention):
5252
`cache` (usually the index of the current token being processed
5353
when running generation). If `cache_update_index=None` while `cache`
5454
is set, the cache will not be updated.
55+
return_attention_scores: a boolean indicating whether the output
56+
should include attention scores.
5557
training: a boolean indicating whether the layer should behave in
5658
training mode or in inference mode.
5759
5860
Returns:
59-
An `(attention_output, cache)` tuple. `attention_output` is the result
60-
of the computation, of shape `(B, T, dim)`, where `T` is for target
61-
sequence shapes and `dim` is the query input last dimension if
62-
`output_shape` is `None`. Otherwise, the multi-head outputs are
63-
projected to the shape specified by `output_shape`. `cache` is the
64-
updated cache.
61+
One of the following:
62+
- `attention_output`
63+
- `(attention_output, attention_scores)`
64+
- `(attention_output, cache)`
65+
- `(attention_output, attention_scores, cache)`
6566
"""
6667

6768
def call(
@@ -72,6 +73,7 @@ def call(
7273
attention_mask=None,
7374
cache=None,
7475
cache_update_index=None,
76+
return_attention_scores=False,
7577
training=None,
7678
):
7779
if key is None:
@@ -113,11 +115,17 @@ def call(
113115
key=key,
114116
value=value,
115117
attention_mask=attention_mask,
118+
return_attention_scores=return_attention_scores,
116119
training=training,
117120
)
118121

119122
attention_output = self._output_dense(attention_output)
120123

121-
if cache is not None:
124+
if return_attention_scores and cache is not None:
125+
return attention_output, attention_scores, cache
126+
elif return_attention_scores:
127+
return attention_output, attention_scores
128+
elif cache is not None:
122129
return attention_output, cache
123-
return attention_output
130+
else:
131+
return attention_output

keras_hub/src/layers/modeling/cached_multi_head_attention_test.py

Lines changed: 121 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@
88

99

1010
class CachedMultiHeadAttentionTest(TestCase):
11+
def setUp(self):
12+
super().setUp()
13+
self.batch_size = 2
14+
self.seq_len = 5
15+
self.num_heads = 2
16+
self.key_dim = 4
17+
self.hidden_dim = self.num_heads * self.key_dim
18+
1119
def test_layer_behaviors(self):
1220
self.run_layer_test(
1321
cls=CachedMultiHeadAttention,
@@ -26,20 +34,25 @@ def test_layer_behaviors(self):
2634
)
2735

2836
def test_cache_call_is_correct(self):
29-
batch_size = 2
30-
seq_len = 5
31-
num_heads = 2
32-
key_dim = 4
33-
hidden_dim = num_heads * key_dim
34-
35-
input_shape = (batch_size, seq_len, hidden_dim)
37+
input_shape = (self.batch_size, self.seq_len, self.hidden_dim)
3638
x = random.uniform(shape=input_shape)
37-
input_cache = ops.zeros((batch_size, 2, seq_len, num_heads, key_dim))
39+
input_cache = ops.zeros(
40+
(
41+
self.batch_size,
42+
2,
43+
self.seq_len,
44+
self.num_heads,
45+
self.key_dim,
46+
)
47+
)
3848
# Use a causal mask.
39-
mask = ops.tril(ops.ones((seq_len, seq_len)))
49+
mask = ops.tril(ops.ones((self.seq_len, self.seq_len)))
4050
outputs = ops.zeros_like(x)
4151

42-
layer = CachedMultiHeadAttention(num_heads=num_heads, key_dim=key_dim)
52+
layer = CachedMultiHeadAttention(
53+
num_heads=self.num_heads,
54+
key_dim=self.key_dim,
55+
)
4356
no_loop_outputs, no_loop_cache = layer(
4457
x,
4558
x,
@@ -50,8 +63,12 @@ def test_cache_call_is_correct(self):
5063

5164
def loop_body(i, outputs, cache):
5265
# Compute the rest tokens.
53-
next_input = ops.slice(x, (0, i, 0), (batch_size, 1, hidden_dim))
54-
next_mask = ops.slice(mask, (i, 0), (1, seq_len))
66+
next_input = ops.slice(
67+
x,
68+
(0, i, 0),
69+
(self.batch_size, 1, self.hidden_dim),
70+
)
71+
next_mask = ops.slice(mask, (i, 0), (1, self.seq_len))
5572
next_output, cache = layer(
5673
query=next_input,
5774
value=next_input,
@@ -64,7 +81,7 @@ def loop_body(i, outputs, cache):
6481

6582
def call(outputs, cache):
6683
_, outputs, cache = ops.while_loop(
67-
cond=lambda i, outputs, cache: i < seq_len,
84+
cond=lambda i, outputs, cache: i < self.seq_len,
6885
body=loop_body,
6986
loop_vars=[0, outputs, cache],
7087
)
@@ -75,26 +92,107 @@ def call(outputs, cache):
7592
self.assertAllClose(output, no_loop_outputs)
7693
self.assertAllClose(output_cache, no_loop_cache)
7794

78-
def test_training_propagation(self):
79-
batch_size = 2
80-
seq_len = 5
81-
num_heads = 2
82-
key_dim = 4
83-
hidden_dim = num_heads * key_dim
95+
def test_return_attention_scores(self):
96+
x = random.uniform(
97+
shape=(self.batch_size, self.seq_len, self.hidden_dim)
98+
)
99+
100+
layer = CachedMultiHeadAttention(
101+
num_heads=self.num_heads,
102+
key_dim=self.key_dim,
103+
)
104+
output, attention_scores = layer(
105+
x,
106+
x,
107+
return_attention_scores=True,
108+
)
109+
110+
self.assertIsNotNone(attention_scores)
111+
self.assertAllEqual(
112+
output.shape,
113+
[self.batch_size, self.seq_len, self.hidden_dim],
114+
)
115+
self.assertAllEqual(
116+
attention_scores.shape,
117+
[
118+
self.batch_size,
119+
self.num_heads,
120+
self.seq_len,
121+
self.seq_len,
122+
],
123+
)
124+
score_sums = ops.sum(attention_scores, axis=-1)
125+
self.assertAllClose(
126+
score_sums,
127+
ops.ones_like(score_sums),
128+
atol=1e-5,
129+
)
84130

85-
input_shape = (batch_size, seq_len, hidden_dim)
131+
def test_return_attention_scores_with_cache(self):
132+
x = random.uniform(
133+
shape=(self.batch_size, self.seq_len, self.hidden_dim)
134+
)
135+
input_cache = ops.zeros(
136+
(
137+
self.batch_size,
138+
2,
139+
self.seq_len,
140+
self.num_heads,
141+
self.key_dim,
142+
)
143+
)
144+
mask = ops.tril(ops.ones((self.seq_len, self.seq_len)))
145+
146+
layer = CachedMultiHeadAttention(
147+
num_heads=self.num_heads,
148+
key_dim=self.key_dim,
149+
)
150+
output, attention_scores, output_cache = layer(
151+
x,
152+
x,
153+
cache=input_cache,
154+
cache_update_index=0,
155+
attention_mask=mask,
156+
return_attention_scores=True,
157+
)
158+
159+
self.assertIsNotNone(attention_scores)
160+
self.assertAllEqual(
161+
output.shape,
162+
[self.batch_size, self.seq_len, self.hidden_dim],
163+
)
164+
self.assertAllEqual(
165+
attention_scores.shape,
166+
[
167+
self.batch_size,
168+
self.num_heads,
169+
self.seq_len,
170+
self.seq_len,
171+
],
172+
)
173+
self.assertAllEqual(output_cache.shape, input_cache.shape)
174+
175+
def test_training_propagation(self):
176+
input_shape = (self.batch_size, self.seq_len, self.hidden_dim)
86177
x = random.uniform(shape=input_shape)
87178

88179
layer = CachedMultiHeadAttention(
89-
num_heads=num_heads,
90-
key_dim=key_dim,
180+
num_heads=self.num_heads,
181+
key_dim=self.key_dim,
91182
dropout=0.99999, # Zeros out the outputs after the dropout layer
92183
)
93184
outputs = layer(x, x, training=True)
94185

95186
# Custom computation with dropout rate sets to about 1.0
96187
value = layer._value_dense(x)
97-
attention_scores = ops.zeros((batch_size, num_heads, seq_len, seq_len))
188+
attention_scores = ops.zeros(
189+
(
190+
self.batch_size,
191+
self.num_heads,
192+
self.seq_len,
193+
self.seq_len,
194+
)
195+
)
98196
attention_output = ops.einsum(
99197
layer._combine_equation, attention_scores, value
100198
)

0 commit comments

Comments
 (0)