Skip to content

Commit 7b63b64

Browse files
committed
Address review feedback on the serving context, config, samplers, and guards
1 parent 9936d66 commit 7b63b64

13 files changed

Lines changed: 261 additions & 147 deletions

keras_hub/src/layers/modeling/cached_multi_head_attention.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,16 @@ def call(
9393
"`return_attention_scores=True` is not supported while "
9494
"serving through vLLM paged attention."
9595
)
96-
return self._compute_vllm_attention(query, value, key, cache)
96+
# Self-attention passes the same tensor for query and value;
97+
# cross-attention (e.g. TransformerDecoder's second attention
98+
# block) must not push encoder K/V into the paged cache.
99+
if value is not query:
100+
raise ValueError(
101+
"Cross-attention is not supported under vLLM serving; "
102+
"only decoder self-attention can use the paged KV "
103+
"cache."
104+
)
105+
return self._compute_vllm_attention(query, key, value, cache)
97106

98107
query = self._query_dense(query)
99108

@@ -146,7 +155,7 @@ def call(
146155
else:
147156
return attention_output
148157

149-
def _compute_vllm_attention(self, query, value, key, cache):
158+
def _compute_vllm_attention(self, query, key, value, cache):
150159
"""vLLM paged-attention route (serving on TPU only).
151160
152161
Projects Q/K/V and hands them to the shared paged-attention bridge.

keras_hub/src/layers/modeling/position_embedding.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,16 @@ def _vllm_serving_positions():
147147
layout the serving model feeds in.
148148
"""
149149
ctx = get_vllm_context()
150-
if ctx is None or ctx.positions is None:
150+
if ctx is None:
151+
return None
152+
if ctx.positions is None:
153+
# A kernel with no positions would silently misplace every learned
154+
# embedding; fail loudly instead of falling back to 0..seq_len.
155+
if ctx.paged_attention_func is not None:
156+
raise ValueError(
157+
"vLLM serving context is active but carries no per-token "
158+
"positions; learned position embeddings cannot be "
159+
"computed."
160+
)
151161
return None
152162
return ops.reshape(ctx.positions, (-1, 1))

keras_hub/src/vllm/attention_test.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,7 @@ def __call__(
5656
def activate_serving(kernel, kv_caches=None, positions=None):
5757
"""Publishes a serving context wired to `kernel` for a test."""
5858
vllm_context.set_vllm_context(
59-
block_tables=None,
60-
slot_mapping=None,
61-
attention_metadata="META",
6259
paged_attention_func=kernel,
63-
mesh="MESH",
6460
positions=positions,
6561
kv_caches=kv_caches,
6662
)
@@ -71,6 +67,23 @@ def tearDown(self):
7167
vllm_context.clear_vllm_context()
7268
super().tearDown()
7369

70+
def test_returns_none_when_no_kernel_injected(self):
71+
# Context active, but no paged-attention function published: the
72+
# second half of the entry guard.
73+
activate_serving(None, kv_caches=["C0"])
74+
z = ops.zeros((1, 2, 4, 8))
75+
self.assertIsNone(vllm_paged_attention(z, z, z, 0.125))
76+
77+
def test_raises_when_layers_exceed_caches(self):
78+
# More attention layers than allocated caches is the layer-count
79+
# mismatch a wrong config produces; it must fail loudly.
80+
kernel = RecordingKernel()
81+
activate_serving(kernel, kv_caches=["C0"])
82+
z = ops.zeros((1, 2, 4, 8))
83+
vllm_paged_attention(z, z, z, 0.125)
84+
with self.assertRaisesRegex(ValueError, "caches"):
85+
vllm_paged_attention(z, z, z, 0.125)
86+
7487
def test_returns_none_when_context_inactive(self):
7588
vllm_context.clear_vllm_context()
7689
z = ops.zeros((1, 2, 4, 8))

keras_hub/src/vllm/context.py

Lines changed: 37 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
33
The serving model (``KerasHubForCausalLM`` in this package, registered
44
with vLLM by tpu-inference's plugin hook) publishes this
5-
context around each forward step with ``vllm_context_scope``, which clears
6-
it on exit even when the forward raises, all on one thread. KerasHub
7-
attention layers, the paged-attention bridge, and ``PositionEmbedding`` read
8-
it in-place, so serving metadata never has to thread through layer
9-
signatures.
5+
context around each forward step with ``vllm_context_scope``, which restores
6+
the prior state on exit even when the forward raises, all on one thread.
7+
KerasHub attention layers, the paged-attention bridge, and
8+
``PositionEmbedding`` read it in-place, so serving metadata never has to
9+
thread through layer signatures.
1010
1111
It is thread-local so concurrent requests on separate worker threads never see
1212
each other's caches, positions, or kernel: each thread gets its own inactive
@@ -16,16 +16,31 @@
1616
import contextlib
1717
import threading
1818

19+
# The single enumeration of the context's fields and their inactive values.
20+
# `VLLMContext.__init__`, `clear_vllm_context`, and the scope's save/restore
21+
# all derive from it, so adding a field means touching exactly one place.
22+
_INACTIVE_STATE = {
23+
"paged_attention_func": None,
24+
"positions": None,
25+
"kv_caches": None,
26+
"updated_kv_caches": None,
27+
"layer_index": 0,
28+
"active": False,
29+
}
30+
31+
32+
def _reset(context):
33+
"""Puts ``context`` into the inactive state."""
34+
for name, value in _INACTIVE_STATE.items():
35+
setattr(context, name, value)
36+
1937

2038
class VLLMContext(threading.local):
2139
"""Thread-local context for passing serving metadata to Keras layers.
2240
2341
Attributes:
24-
block_tables: The block tables tensor for paged attention.
25-
slot_mapping: The slot mapping tensor for paged attention.
26-
attention_metadata: The full attention metadata object from vLLM.
27-
paged_attention_func: The compiled paged-attention kernel.
28-
mesh: The JAX device mesh required by the paged attention kernel.
42+
paged_attention_func: The paged-attention closure published by the
43+
serving model; engine details are already bound inside it.
2944
positions: vLLM's per-token absolute position ids (for RoPE models).
3045
kv_caches: Per-layer paged KV caches for the current forward step.
3146
Attention layers consume these in call order via `layer_index`,
@@ -40,69 +55,40 @@ class VLLMContext(threading.local):
4055
def __init__(self):
4156
"""Initializes an empty inactive context."""
4257
super().__init__()
43-
self.block_tables = None
44-
self.slot_mapping = None
45-
self.attention_metadata = None
46-
self.paged_attention_func = None
47-
self.mesh = None
48-
self.positions = None
49-
self.kv_caches = None
50-
self.updated_kv_caches = None
51-
self.layer_index = 0
52-
self.active = False
58+
_reset(self)
5359

5460

5561
_vllm_context = VLLMContext()
5662

5763

5864
def set_vllm_context(
59-
block_tables,
60-
slot_mapping,
61-
attention_metadata=None,
6265
paged_attention_func=None,
63-
mesh=None,
6466
positions=None,
6567
kv_caches=None,
6668
):
6769
"""Sets the thread-local vLLM context parameters.
6870
6971
Args:
70-
block_tables: Array representing memory blocks for key/value caching.
71-
slot_mapping: Array mapping sequence tokens to cache slots.
72-
attention_metadata: Additional hardware/framework specific metadata.
73-
paged_attention_func: The function to use for paged attention.
74-
mesh: The JAX device mesh the paged attention kernel shards across.
72+
paged_attention_func: The paged-attention closure to dispatch to.
7573
positions: vLLM's per-token absolute position ids (used by RoPE models
7674
to apply rotary embeddings at the correct positions under paged /
7775
continuous-batched decode).
7876
kv_caches: Per-layer paged KV caches, in transformer-layer order.
7977
"""
80-
_vllm_context.block_tables = block_tables
81-
_vllm_context.slot_mapping = slot_mapping
82-
_vllm_context.attention_metadata = attention_metadata
8378
_vllm_context.paged_attention_func = paged_attention_func
84-
_vllm_context.mesh = mesh
8579
_vllm_context.positions = positions
8680
_vllm_context.kv_caches = list(kv_caches) if kv_caches is not None else None
8781
_vllm_context.updated_kv_caches = (
8882
list(kv_caches) if kv_caches is not None else None
8983
)
9084
_vllm_context.layer_index = 0
85+
# Set last, so a failure partway through can never look half-active.
9186
_vllm_context.active = True
9287

9388

9489
def clear_vllm_context():
95-
"""Clears the thread-local vLLM context."""
96-
_vllm_context.block_tables = None
97-
_vllm_context.slot_mapping = None
98-
_vllm_context.attention_metadata = None
99-
_vllm_context.paged_attention_func = None
100-
_vllm_context.mesh = None
101-
_vllm_context.positions = None
102-
_vllm_context.kv_caches = None
103-
_vllm_context.updated_kv_caches = None
104-
_vllm_context.layer_index = 0
105-
_vllm_context.active = False
90+
"""Resets the thread-local vLLM context to its inactive state."""
91+
_reset(_vllm_context)
10692

10793

10894
def get_vllm_context():
@@ -116,30 +102,25 @@ def get_vllm_context():
116102

117103
@contextlib.contextmanager
118104
def vllm_context_scope(
119-
block_tables,
120-
slot_mapping,
121-
attention_metadata=None,
122105
paged_attention_func=None,
123-
mesh=None,
124106
positions=None,
125107
kv_caches=None,
126108
):
127109
"""Publishes the serving context for the enclosed forward step.
128110
129-
Sets the thread-local context on entry and always clears it on exit,
130-
including when the forward raises, so a reused worker thread can never
131-
observe a stale context. Arguments match ``set_vllm_context``.
111+
Saves the prior state on entry and always restores it on exit,
112+
including when the forward raises — so a reused worker thread can never
113+
observe a stale context, and nested scopes hand back the outer scope's
114+
state instead of wiping it. Arguments match ``set_vllm_context``.
132115
"""
116+
previous = {name: getattr(_vllm_context, name) for name in _INACTIVE_STATE}
133117
set_vllm_context(
134-
block_tables=block_tables,
135-
slot_mapping=slot_mapping,
136-
attention_metadata=attention_metadata,
137118
paged_attention_func=paged_attention_func,
138-
mesh=mesh,
139119
positions=positions,
140120
kv_caches=kv_caches,
141121
)
142122
try:
143123
yield
144124
finally:
145-
clear_vllm_context()
125+
for name, value in previous.items():
126+
setattr(_vllm_context, name, value)

keras_hub/src/vllm/context_test.py

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,36 +17,33 @@ def test_inactive_by_default(self):
1717

1818
def test_set_get_roundtrip(self):
1919
vllm_context.set_vllm_context(
20-
block_tables="BT",
21-
slot_mapping="SM",
22-
attention_metadata="META",
2320
paged_attention_func="FUNC",
24-
mesh="MESH",
21+
positions="POS",
22+
kv_caches=["C0"],
2523
)
2624
ctx = vllm_context.get_vllm_context()
2725
self.assertIsNotNone(ctx)
28-
self.assertEqual(ctx.block_tables, "BT")
29-
self.assertEqual(ctx.slot_mapping, "SM")
30-
self.assertEqual(ctx.attention_metadata, "META")
3126
self.assertEqual(ctx.paged_attention_func, "FUNC")
32-
self.assertEqual(ctx.mesh, "MESH")
27+
self.assertEqual(ctx.positions, "POS")
28+
self.assertEqual(ctx.kv_caches, ["C0"])
3329
self.assertTrue(ctx.active)
3430

3531
def test_clear_resets_everything(self):
36-
vllm_context.set_vllm_context("BT", "SM", "META", "FUNC", "MESH")
32+
vllm_context.set_vllm_context(
33+
paged_attention_func="FUNC", positions="POS", kv_caches=["C0"]
34+
)
3735
vllm_context.clear_vllm_context()
3836
self.assertIsNone(vllm_context.get_vllm_context())
39-
# The singleton's fields are reset too.
40-
self.assertIsNone(vllm_context._vllm_context.mesh)
41-
self.assertIsNone(vllm_context._vllm_context.paged_attention_func)
42-
self.assertIsNone(vllm_context._vllm_context.kv_caches)
43-
self.assertEqual(vllm_context._vllm_context.layer_index, 0)
37+
# Every field named in _INACTIVE_STATE is back at its inactive
38+
# value, so nothing can survive a "clear".
39+
for name, value in vllm_context._INACTIVE_STATE.items():
40+
self.assertEqual(
41+
getattr(vllm_context._vllm_context, name), value, name
42+
)
4443

4544
def test_kv_caches_copied_and_lifecycle_fields_initialized(self):
4645
caches = ["C0", "C1"]
47-
vllm_context.set_vllm_context(
48-
None, None, kv_caches=caches, positions="POS"
49-
)
46+
vllm_context.set_vllm_context(kv_caches=caches, positions="POS")
5047
ctx = vllm_context.get_vllm_context()
5148
# Layer counter starts fresh; updated caches seed from the input.
5249
self.assertEqual(ctx.layer_index, 0)
@@ -62,24 +59,44 @@ def test_kv_caches_copied_and_lifecycle_fields_initialized(self):
6259
def test_reset_on_new_forward_step(self):
6360
# A second forward step must reset the per-step layer counter, even if
6461
# the previous step was never explicitly cleared.
65-
vllm_context.set_vllm_context(None, None, kv_caches=["C0"])
62+
vllm_context.set_vllm_context(kv_caches=["C0"])
6663
vllm_context.get_vllm_context().layer_index = 5
67-
vllm_context.set_vllm_context(None, None, kv_caches=["C0"])
64+
vllm_context.set_vllm_context(kv_caches=["C0"])
6865
self.assertEqual(vllm_context.get_vllm_context().layer_index, 0)
6966

67+
def test_scope_clears_on_normal_exit(self):
68+
with vllm_context.vllm_context_scope(paged_attention_func="KERNEL"):
69+
self.assertIsNotNone(vllm_context.get_vllm_context())
70+
self.assertIsNone(vllm_context.get_vllm_context())
71+
7072
def test_scope_clears_on_exception(self):
7173
with self.assertRaisesRegex(RuntimeError, "boom"):
72-
with vllm_context.vllm_context_scope(
73-
None, None, paged_attention_func="KERNEL"
74-
):
74+
with vllm_context.vllm_context_scope(paged_attention_func="KERNEL"):
7575
self.assertIsNotNone(vllm_context.get_vllm_context())
7676
raise RuntimeError("boom")
7777
self.assertIsNone(vllm_context.get_vllm_context())
7878

79+
def test_nested_scopes_restore_outer(self):
80+
# The inner scope's exit must hand back the outer scope's state, not
81+
# wipe it — otherwise the rest of the outer forward would silently
82+
# fall back to the dense path.
83+
with vllm_context.vllm_context_scope(
84+
paged_attention_func="OUTER", kv_caches=["O0"]
85+
):
86+
with vllm_context.vllm_context_scope(paged_attention_func="INNER"):
87+
ctx = vllm_context.get_vllm_context()
88+
self.assertEqual(ctx.paged_attention_func, "INNER")
89+
self.assertIsNone(ctx.kv_caches)
90+
ctx = vllm_context.get_vllm_context()
91+
self.assertIsNotNone(ctx)
92+
self.assertEqual(ctx.paged_attention_func, "OUTER")
93+
self.assertEqual(ctx.kv_caches, ["O0"])
94+
self.assertIsNone(vllm_context.get_vllm_context())
95+
7996
def test_context_is_thread_local(self):
8097
# A context set on the main thread must be invisible to another thread,
8198
# so concurrent requests can never see each other's serving state.
82-
vllm_context.set_vllm_context("BT", "SM", "META", "FUNC", "MESH")
99+
vllm_context.set_vllm_context(paged_attention_func="FUNC")
83100
seen = {}
84101

85102
def worker():

0 commit comments

Comments
 (0)