Skip to content

Commit 9936d66

Browse files
committed
Move the vLLM serving model into keras-hub
1 parent 94b2042 commit 9936d66

3 files changed

Lines changed: 256 additions & 2 deletions

File tree

keras_hub/src/vllm/attention.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ def vllm_paged_attention(
9696
key = ops.reshape(key, (-1, kv_heads * head_dim))
9797
value = ops.reshape(value, (-1, kv_heads * head_dim))
9898

99-
# `paged_attention_func` comes from the serving model in tpu-inference.
99+
# `paged_attention_func` comes from the serving model
100+
# (keras_hub_for_causal_lm.py).
100101
# It takes only what an attention layer knows; engine details (the
101102
# attention metadata, the mesh) are already bound inside it. It returns
102103
# `(new_kv_cache, output)`.

keras_hub/src/vllm/context.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Thread-local context for passing vLLM metadata to Keras layers.
22
3-
The serving model (tpu-inference's ``KerasHubForCausalLM``) publishes this
3+
The serving model (``KerasHubForCausalLM`` in this package, registered
4+
with vLLM by tpu-inference's plugin hook) publishes this
45
context around each forward step with ``vllm_context_scope``, which clears
56
it on exit even when the forward raises, all on one thread. KerasHub
67
attention layers, the paged-attention bridge, and ``PositionEmbedding`` read
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
"""The serving model that runs a KerasHub `CausalLM` on vLLM's TPU backend.
2+
3+
`KerasHubForCausalLM` lives here in keras-hub and is registered with
4+
tpu-inference's model registry (and vLLM's) by tpu-inference's plugin hook,
5+
so the native `flax_nnx` loader resolves it by architecture name like any
6+
other model. It is only ever imported on the serving path, where flax and
7+
tpu-inference are installed.
8+
"""
9+
10+
import jax
11+
from flax import nnx
12+
from jax.sharding import Mesh
13+
from keras import ops
14+
15+
from keras_hub.src.models.causal_lm import CausalLM
16+
from keras_hub.src.vllm.context import get_vllm_context
17+
from keras_hub.src.vllm.context import vllm_context_scope
18+
19+
20+
class KerasHubForCausalLM(nnx.Module):
21+
"""Serves a KerasHub `CausalLM` on tpu-inference's native JAX path.
22+
23+
An adapter, not a conversion: it implements the model interface the
24+
native `flax_nnx` runner drives (resolved by the `KerasHubForCausalLM`
25+
architecture name through the standard model registry), reusing the
26+
preset's existing backbone and weights. Keras's NNX mode
27+
(`KERAS_NNX_ENABLED=true`) makes the backbone's variables nnx state, so
28+
the runner's `nnx.split`/`nnx.merge` machinery carries the weights with
29+
no conversion:
30+
31+
- `__init__` records the preset name and dtype; it builds nothing, so
32+
it is safe under the loader's `nnx.eval_shape` abstract pass.
33+
- `load_weights` builds the model and loads the preset weights with a
34+
single `CausalLM.from_preset` call. The loader always calls it
35+
eagerly, so KerasHub construction and weight IO are unrestricted.
36+
- `__call__` runs one forward step: it publishes the serving context
37+
(the paged-attention kernel, the per-layer paged KV caches, and
38+
vLLM's per-token positions), then delegates to the backbone's own
39+
forward; each attention layer's vLLM route reads that context and
40+
dispatches to the paged-attention kernel.
41+
- `compute_logits` projects hidden states to vocabulary logits through
42+
the tied token embedding.
43+
"""
44+
45+
def __init__(
46+
self, vllm_config, rng_key: jax.Array, mesh: Mesh = None
47+
) -> None:
48+
"""Records what `load_weights` needs; builds nothing.
49+
50+
The loader constructs models under `nnx.eval_shape`, where concrete
51+
tensor work cannot run (KerasHub constructors compute values such
52+
as Gemma's embedding scale). Keeping `__init__` free of any Keras
53+
construction makes it safe to trace; the model itself is built in
54+
`load_weights`, which the loader always calls eagerly.
55+
56+
Args:
57+
vllm_config: The vLLM config; `model_config.hf_config` carries
58+
the `keras_hub_preset` written by `setup_vllm_model`.
59+
rng_key: JAX PRNG key (unused; weights come from the preset).
60+
mesh: JAX device mesh used by the paged-attention kernel.
61+
"""
62+
self.vllm_config = vllm_config
63+
self.mesh = mesh
64+
self.preset_name = vllm_config.model_config.hf_config.keras_hub_preset
65+
# Serving dtype comes from the config vLLM resolved (written as
66+
# torch_dtype by setup_vllm_model), not a hardcoded value.
67+
self._dtype = str(
68+
getattr(
69+
vllm_config.model_config.hf_config,
70+
"torch_dtype",
71+
"bfloat16",
72+
)
73+
)
74+
75+
def load_weights(self, *args, **kwargs) -> None:
76+
"""Builds the KerasHub model and loads the preset weights.
77+
78+
One `CausalLM.from_preset` call, the same as any other KerasHub
79+
usage: it builds the model and loads the preset weights in place.
80+
Only the backbone is kept: serving never calls the task wrapper,
81+
and a compiled task carries a layer-keyed dict (Keras 3.15's
82+
`_compiled_trainable_state`) whose keys nnx's graph flatten cannot
83+
sort.
84+
85+
Args:
86+
*args: Variable length argument list (unused).
87+
**kwargs: Arbitrary keyword arguments (unused).
88+
"""
89+
model = CausalLM.from_preset(self.preset_name, dtype=self._dtype)
90+
self.backbone = model.backbone
91+
92+
def __call__(
93+
self,
94+
kv_caches,
95+
input_ids,
96+
attention_metadata,
97+
*args,
98+
**kwargs,
99+
):
100+
"""Runs one forward step with vLLM's paged KV cache.
101+
102+
Publishes the serving context (the paged-attention kernel, the
103+
per-layer paged KV caches, and vLLM's per-token positions), then
104+
delegates to the KerasHub backbone's own forward, which keeps every
105+
model-specific detail (embedding scaling, learned vs. rotary
106+
positions, per-family norms) in the model. Each attention layer's
107+
vLLM route and KerasHub's `PositionEmbedding` read the context in
108+
place; the scope clears it when the step finishes, even on error.
109+
110+
Returns:
111+
`(updated_kv_caches, hidden_states, None, None)` — the tuple
112+
the native runner expects.
113+
"""
114+
positions = getattr(attention_metadata, "input_positions", None)
115+
if positions is None:
116+
positions = kwargs.get("positions")
117+
118+
token_ids = input_ids
119+
if len(token_ids.shape) == 1:
120+
token_ids = ops.expand_dims(token_ids, axis=-1)
121+
# vLLM presents already-packed tokens; there is no padding to mask.
122+
padding_mask = ops.ones_like(token_ids)
123+
124+
with self._serving_context(kv_caches, attention_metadata, positions):
125+
hidden_states = self.backbone(
126+
{"token_ids": token_ids, "padding_mask": padding_mask},
127+
training=False,
128+
)
129+
# Tokens ride as (num_tokens, 1); drop the seq axis the runner
130+
# does not expect.
131+
if len(hidden_states.shape) == 3 and hidden_states.shape[1] == 1:
132+
hidden_states = ops.squeeze(hidden_states, axis=1)
133+
134+
# The bridge stored each layer's kernel-updated cache here. It
135+
# is None on a cacheless pass (e.g. vLLM's startup profiling
136+
# run).
137+
ctx = get_vllm_context()
138+
updated_kv_caches = (
139+
list(ctx.updated_kv_caches)
140+
if ctx.updated_kv_caches is not None
141+
else None
142+
)
143+
# Every attention layer must have dispatched to the kernel
144+
# exactly once; a mismatch means one silently ran its dense
145+
# path (or ran twice), which would corrupt the paged cache.
146+
caches = ctx.kv_caches
147+
num_caches = len(caches) if caches is not None else None
148+
if num_caches is not None and ctx.layer_index != num_caches:
149+
raise RuntimeError(
150+
f"Paged-attention dispatch ran {ctx.layer_index} "
151+
f"time(s) for {num_caches} transformer layers. An "
152+
"attention layer skipped the vLLM dispatch (or "
153+
"dispatched more than once); serving this model would "
154+
"produce incorrect output."
155+
)
156+
157+
return updated_kv_caches, hidden_states, None, None
158+
159+
def compute_logits(self, hidden_states, *args, **kwargs):
160+
"""Projects hidden states to vocab logits via the tied embedding.
161+
162+
Args:
163+
hidden_states: Tensor. A tensor of hidden states from the
164+
backbone.
165+
*args: Variable length argument list (unused).
166+
**kwargs: Arbitrary keyword arguments (unused).
167+
168+
Returns:
169+
Tensor. A tensor of logits.
170+
"""
171+
return self.backbone.token_embedding(hidden_states, reverse=True)
172+
173+
def _serving_context(self, kv_caches, attention_metadata, positions):
174+
"""Builds the serving-context scope for one forward step.
175+
176+
Returns keras-hub's ``vllm_context_scope`` context manager, which
177+
publishes the thread-local serving context on entry and always
178+
clears it on exit, even when the forward raises.
179+
180+
The published function wraps tpu-inference's ragged-paged-attention
181+
kernel (`_jax_attn_func`) behind a small stable contract carrying
182+
only what a KerasHub attention layer knows::
183+
184+
fn(kv_cache, q, k, v, scale, head_size, num_heads, num_kv_heads,
185+
sliding_window=None, soft_cap=None) -> (new_kv_cache, output)
186+
187+
Engine-side arguments (the attention metadata, the mesh) are closed
188+
over here, so kernel signature changes stay local to this repo. The
189+
per-layer paged KV caches ride in the context too: KerasHub's shared
190+
bridge consumes them in layer-call order, so the layers run their
191+
plain `cache=None` path and need no cache threading.
192+
"""
193+
# tpu-inference is imported here, not at module top: it is an
194+
# optional dependency of keras-hub, and the tpu-inference plugin
195+
# hook imports this module, so a top-level import would be
196+
# circular. A missing kernel fails loudly with the real cause.
197+
try:
198+
from tpu_inference.layers.vllm.backends.flash_attn import (
199+
_jax_attn_func,
200+
)
201+
except ImportError as e:
202+
raise ImportError(
203+
"Serving a KerasHub model requires tpu-inference "
204+
"(vllm-tpu), which provides the paged-attention kernel."
205+
) from e
206+
207+
mesh = self.mesh
208+
209+
def paged_attention(
210+
kv_cache,
211+
q,
212+
k,
213+
v,
214+
scale,
215+
head_size,
216+
num_heads,
217+
num_kv_heads,
218+
sliding_window=None,
219+
soft_cap=None,
220+
):
221+
return _jax_attn_func(
222+
kv_cache=kv_cache,
223+
q=q,
224+
k=k,
225+
v=v,
226+
sinks=None, # KerasHub attention layers have no sinks.
227+
attention_metadata=attention_metadata,
228+
shared_attention_metadata=None, # single-chip serving
229+
mesh=mesh,
230+
scale=scale,
231+
head_size=head_size,
232+
num_heads=num_heads,
233+
num_kv_heads=num_kv_heads,
234+
sliding_window=sliding_window,
235+
soft_cap=soft_cap,
236+
)
237+
238+
block_tables = getattr(attention_metadata, "block_tables", None)
239+
slot_mapping = getattr(
240+
attention_metadata,
241+
"slot_mapping_tensor",
242+
getattr(attention_metadata, "slot_mapping", None),
243+
)
244+
return vllm_context_scope(
245+
block_tables,
246+
slot_mapping,
247+
attention_metadata,
248+
paged_attention,
249+
self.mesh,
250+
positions=positions,
251+
kv_caches=kv_caches,
252+
)

0 commit comments

Comments
 (0)