Skip to content

Commit ed17b60

Browse files
authored
Merge branch 'keras-team:master' into port_Gemma3n_AudioConverter
2 parents c949d72 + c66cdc1 commit ed17b60

6 files changed

Lines changed: 1471 additions & 2 deletions

File tree

benchmarks/vllm_serving.py

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
"""Benchmark KerasHub presets served through vLLM.
2+
3+
Measures output token throughput for one of three configurations, all serving
4+
the same weights:
5+
6+
keras_hub `CausalLM.generate()`, static full-batch decoding.
7+
vllm_native vLLM's own model implementation, loaded from the
8+
equivalent Hugging Face checkpoint.
9+
keras_hub_vllm The KerasHub integration, `keras_hub.vllm.KerasHubLLM`.
10+
11+
Keras reads its backend at import, so set these before running. The last one
12+
is required: vLLM otherwise forks a worker process, which deadlocks against an
13+
already-initialized JAX.
14+
15+
```
16+
export KERAS_BACKEND=jax
17+
export KERAS_NNX_ENABLED=true
18+
export VLLM_ENABLE_V1_MULTIPROCESSING=0
19+
```
20+
21+
Run one configuration per invocation:
22+
23+
```
24+
python3 benchmarks/vllm_serving.py \
25+
--config keras_hub_vllm \
26+
--preset gemma3_instruct_1b
27+
```
28+
29+
One config per run. A vLLM engine holds its memory until the process exits.
30+
31+
Runs 32 and 512 word prompts at 1, 32 and 64 concurrent requests, 128
32+
generated tokens each, greedy. One warmup pass then 20 timed passes per cell.
33+
Throughput counts generated tokens only.
34+
"""
35+
36+
import time
37+
38+
from absl import app
39+
from absl import flags
40+
41+
import keras_hub
42+
43+
# The presets served by the merged integration, mapped to the Hugging Face
44+
# checkpoint holding the same weights for the vllm_native configuration.
45+
PRESETS = {
46+
"gpt2_base_en": "openai-community/gpt2",
47+
"gpt2_large_en": "openai-community/gpt2-large",
48+
"qwen2.5_coder_0.5b": "Qwen/Qwen2.5-Coder-0.5B",
49+
"llama3.2_instruct_1b": "meta-llama/Llama-3.2-1B-Instruct",
50+
"gemma_2b_en": "google/gemma-2b",
51+
"gemma2_2b_en": "google/gemma-2-2b",
52+
"gemma3_instruct_1b": "google/gemma-3-1b-it",
53+
}
54+
55+
CONFIGS = ("keras_hub", "vllm_native", "keras_hub_vllm")
56+
57+
INPUT_WORDS = (32, 512)
58+
CONCURRENCY = (1, 32, 64)
59+
OUTPUT_TOKENS = 128
60+
WARMUP_RUNS = 1
61+
TIMED_RUNS = 20
62+
MAX_MODEL_LEN = 1024
63+
# Caps how much prefill the engine batches, and so how many shapes it compiles
64+
# at startup.
65+
MAX_NUM_BATCHED_TOKENS = 512
66+
# Matches the highest concurrency measured, so no request waits on a slot.
67+
MAX_NUM_SEQS = 64
68+
DTYPE = "bfloat16"
69+
70+
FLAGS = flags.FLAGS
71+
72+
flags.DEFINE_enum(
73+
"config",
74+
"keras_hub_vllm",
75+
CONFIGS,
76+
"Which configuration to measure.",
77+
)
78+
flags.DEFINE_enum(
79+
"preset",
80+
"gemma3_instruct_1b",
81+
list(PRESETS),
82+
"Which preset to measure.",
83+
)
84+
flags.DEFINE_string(
85+
"output",
86+
None,
87+
"CSV output path. Defaults to <config>_<preset>.csv.",
88+
)
89+
90+
91+
def build_prompt(word_count):
92+
"""Returns a prompt of the requested length in words."""
93+
words = "The future of artificial intelligence is".split()
94+
return " ".join((words * (word_count // len(words) + 1))[:word_count])
95+
96+
97+
def keras_hub_generate(preset):
98+
"""Returns generate and count functions for `CausalLM.generate()`."""
99+
from keras import ops
100+
101+
model = keras_hub.models.CausalLM.from_preset(preset, dtype=DTYPE)
102+
# `CausalLM.compile` defaults to top_k, which is stochastic. The vLLM
103+
# configurations use temperature=0.0, so this has to be greedy to match.
104+
model.compile(sampler="greedy")
105+
tokenizer = model.preprocessor.tokenizer
106+
107+
# The prompt length comes from generate_preprocess, which counts the <bos>
108+
# the tokenizer alone would miss.
109+
max_lengths = {}
110+
for word_count in INPUT_WORDS:
111+
prompt = build_prompt(word_count)
112+
preprocessed = model.preprocessor.generate_preprocess([prompt])
113+
prompt_tokens = int(ops.sum(preprocessed["padding_mask"][0]))
114+
max_lengths[prompt] = prompt_tokens + OUTPUT_TOKENS
115+
116+
def generate(prompts):
117+
return model.generate(
118+
prompts,
119+
max_length=max_lengths[prompts[0]],
120+
strip_prompt=True,
121+
)
122+
123+
def count(outputs):
124+
return sum(len(tokenizer(output)) for output in outputs)
125+
126+
return generate, count
127+
128+
129+
def vllm_generate(model, keras_hub_integration):
130+
"""Returns generate and count functions for a vLLM engine."""
131+
from vllm import SamplingParams
132+
133+
if keras_hub_integration:
134+
from keras_hub.vllm import KerasHubLLM
135+
136+
engine = KerasHubLLM(
137+
f"keras_hub:{model}",
138+
dtype=DTYPE,
139+
max_model_len=MAX_MODEL_LEN,
140+
max_num_batched_tokens=MAX_NUM_BATCHED_TOKENS,
141+
max_num_seqs=MAX_NUM_SEQS,
142+
)
143+
else:
144+
from vllm import LLM
145+
146+
engine = LLM(
147+
model=model,
148+
dtype=DTYPE,
149+
max_model_len=MAX_MODEL_LEN,
150+
max_num_batched_tokens=MAX_NUM_BATCHED_TOKENS,
151+
max_num_seqs=MAX_NUM_SEQS,
152+
)
153+
154+
params = SamplingParams(temperature=0.0, max_tokens=OUTPUT_TOKENS)
155+
156+
def generate(prompts):
157+
return engine.generate(prompts, params)
158+
159+
def count(outputs):
160+
return sum(len(output.outputs[0].token_ids) for output in outputs)
161+
162+
return generate, count
163+
164+
165+
def measure(generate, count, prompts):
166+
"""Returns tokens generated over time taken. Only generation is timed."""
167+
for _ in range(WARMUP_RUNS):
168+
generate(prompts)
169+
170+
total_tokens = 0
171+
total_time = 0.0
172+
for _ in range(TIMED_RUNS):
173+
start = time.perf_counter()
174+
outputs = generate(prompts)
175+
total_time += time.perf_counter() - start
176+
total_tokens += count(outputs)
177+
return total_tokens / total_time
178+
179+
180+
def main(_):
181+
preset = FLAGS.preset
182+
config = FLAGS.config
183+
184+
if config == "keras_hub":
185+
generate, count = keras_hub_generate(preset)
186+
elif config == "vllm_native":
187+
generate, count = vllm_generate(
188+
PRESETS[preset], keras_hub_integration=False
189+
)
190+
else:
191+
generate, count = vllm_generate(preset, keras_hub_integration=True)
192+
193+
path = FLAGS.output or f"{config}_{preset}.csv"
194+
with open(path, "w") as results:
195+
results.write(
196+
"preset,config,input_words,concurrency,tokens_per_second\n"
197+
)
198+
for input_words in INPUT_WORDS:
199+
prompt = build_prompt(input_words)
200+
for concurrency in CONCURRENCY:
201+
throughput = measure(generate, count, [prompt] * concurrency)
202+
print(
203+
f"{preset} {config} input_words={input_words} "
204+
f"concurrency={concurrency} "
205+
f"{throughput:.1f} tokens/s"
206+
)
207+
results.write(
208+
f"{preset},{config},{input_words},{concurrency},"
209+
f"{throughput:.2f}\n"
210+
)
211+
results.flush()
212+
213+
print(f"Wrote {path}")
214+
215+
216+
if __name__ == "__main__":
217+
app.run(main)

0 commit comments

Comments
 (0)