1010from keras_hub .src .utils .keras_utils import gpu_supports_fused_attention_op
1111from keras_hub .src .utils .keras_utils import running_on_gpu
1212from keras_hub .src .utils .keras_utils import running_on_tpu
13+ from keras_hub .src .vllm .attention import vllm_paged_attention
14+ from keras_hub .src .vllm .context import get_vllm_context
1315
1416
1517class CachedGemmaAttention (keras .layers .Layer ):
@@ -97,9 +99,16 @@ def build(self, inputs_shape):
9799
98100 self .built = True
99101
100- def _apply_rope (self , x , start_index ):
101- """Rope rotate q or k."""
102- x = self .rope_layer (x , start_index = start_index )
102+ def _apply_rope (self , x , start_index , positions = None ):
103+ """Rope rotate q or k.
104+
105+ When ``positions`` is given (vLLM serving), rotate at those absolute
106+ per-token positions instead of a contiguous ``start_index`` range.
107+ """
108+ if positions is not None :
109+ x = self .rope_layer (x , positions = positions )
110+ else :
111+ x = self .rope_layer (x , start_index = start_index )
103112 # Gemma uses a different layout for positional embeddings.
104113 # The transformation below ensures the embeddings are numerically
105114 # equivalent to the original gemma implementation.
@@ -226,6 +235,43 @@ def _mask_sliding_window(
226235 sliding_mask = ops .expand_dims (sliding_mask , 0 )
227236 return ops .logical_and (attention_mask , ops .cast (sliding_mask , "bool" ))
228237
238+ def _compute_vllm_attention (self , x , cache , vllm_context ):
239+ """vLLM paged-attention route (serving on TPU only).
240+
241+ Projects Q/K/V, applies Gemma's RoPE (with its split/reshape layout) at
242+ the engine's per-token positions, and hands the unexpanded K/V to the
243+ shared paged-attention bridge. Gemma's softmax scale, sliding window,
244+ and optional logit soft cap (Gemma 2) are all supplied here.
245+ """
246+ positions = ops .reshape (vllm_context .positions , (- 1 , 1 ))
247+ query = self ._apply_rope (self .query_dense (x ), 0 , positions = positions )
248+ key = self ._apply_rope (self .key_dense (x ), 0 , positions = positions )
249+ value = self .value_dense (x )
250+
251+ if self .query_head_dim_normalize :
252+ scale = 1.0 / np .sqrt (self .head_dim )
253+ else :
254+ scale = 1.0 / np .sqrt (self .hidden_dim // self .num_query_heads )
255+ sliding_window = (
256+ self .sliding_window_size
257+ if self .use_sliding_window_attention
258+ else None
259+ )
260+
261+ attention_vec = vllm_paged_attention (
262+ query ,
263+ key ,
264+ value ,
265+ scale ,
266+ num_kv_heads = self .num_key_value_heads ,
267+ soft_cap = self .logit_soft_cap ,
268+ sliding_window = sliding_window ,
269+ )
270+ attention_output = self .output_dense (attention_vec )
271+ if cache is not None :
272+ return attention_output , cache
273+ return attention_output
274+
229275 def call (
230276 self ,
231277 x ,
@@ -234,6 +280,15 @@ def call(
234280 cache_update_index = 0 ,
235281 training = False ,
236282 ):
283+ # Route to vLLM paged attention while serving on TPU; otherwise the
284+ # original dense path below runs unchanged.
285+ vllm_context = get_vllm_context ()
286+ if (
287+ vllm_context is not None
288+ and vllm_context .paged_attention_func is not None
289+ ):
290+ return self ._compute_vllm_attention (x , cache , vllm_context )
291+
237292 query = self .query_dense (x )
238293 query = self ._apply_rope (query , cache_update_index )
239294
0 commit comments