Benchmarking plain and sealed serving
The benchmarks/ directory of the repository holds one script per published table. Each
script seals the model under a fresh key unless a sealed copy exists, serves the plain and
sealed variants, and writes everything it measured to an output directory. This page
describes how to run the scripts on other hardware, which script produced which published
table, and the full LLM tables.
Note
kLLM has no benchmark command. Benchmarks are the shell and Python scripts described here; they are not part of the installed package or the container image, so run them from a source checkout.
Measurement conditions
Unless a table states otherwise, the published results were measured as follows:
one NVIDIA A40 48 GB per server (two for tensor parallelism 2);
SGLang 0.5.21 and vLLM 0.31.0, bf16 unless the checkpoint is FP8;
random token ids, 1024 input and 128 output tokens; concurrency 1 with 10 requests and concurrency 16 with 128 requests;
variants served one after another on the same GPU, with
--no-recipe;a GPU shared with another tenant’s light job;
single runs; no variance is reported.
Report new numbers with the GPU model, the engine version and these settings. Differences of a few percent between single runs on a shared host are not significant.
Prerequisites
A source checkout with kLLM installed with
sealand the engine extra,serveorvllm.Local checkpoint directories; the scripts take paths, not Hub ids.
For perplexity: the GPL-3, GFDL-1.3 and Apache-2.0 license texts under
/usr/share/common-licenses, as shipped by Debian and Ubuntu.For
eval.sh:pip install 'lm_eval[api]' transformers.For the H3 scripts: an SGLang build with diffusion support and PyAV; for
h3_int8.shalso theint8extra.For
zimage_client.py: theimageextra.
The scripts create a key file in the output directory when none is given, with the
passphrase from KLLM_PASSPHRASE (default kllm-bench). Use such keys for benchmarking
only.
LLM latency, throughput and perplexity
$ benchmarks/llm.sh ~/models/Qwen3-8B # SGLang, GPU 0
$ ENGINE=vllm GPU=2 benchmarks/llm.sh ~/models/Qwen3-8B # vLLM, GPU 2
$ python benchmarks/report.py out/Qwen3-8B-sglang
llm.sh serves plain, then each transform in TRANSFORMS. Each variant is scored by
tools/ppl.py on the three license texts (the plain run is saved, and the sealed runs are
compared with it token by token), then load-tested with sglang.bench_serving on random
token ids when SGLang is importable, or with vllm bench serve otherwise. Engine flags are
--mem-fraction-static 0.55 for SGLang, and --gpu-memory-utilization 0.55
--max-model-len 8192 --no-enable-prefix-caching for vLLM. report.py prints a Markdown
table of median TTFT and TPOT at concurrency 1, output throughput and median TPOT at
concurrency 16, perplexity and mean |Δ log-prob| against plain; it orders the sealed
columns alphabetically.
Variable |
Default |
Meaning |
|---|---|---|
|
|
Value of |
|
|
Engine port. |
|
|
|
|
|
Sealed variants. Use |
|
|
Key file; created if missing. |
|
|
Output directory. |
|
empty |
Further engine flags, for example tensor-parallel options. |
For tensor parallelism, select several GPUs with GPU, for example
GPU=0,1 ENGINE=vllm. kllm serve sets the tensor-parallel size to the number of
visible GPUs when --tp is not given.
Standard evaluations
$ benchmarks/eval.sh ~/models/Qwen3-1.7B # GSM8K and MMLU, vLLM, GPU 0
$ TASKS=gsm8k LIMIT=200 ENGINE=sglang benchmarks/eval.sh ~/models/Qwen3-1.7B
eval.sh runs lm-evaluation-harness against the OpenAI-compatible endpoint of each
variant. The harness sends text and, for a sealed model, counts tokens with the tokenizer
that kllm unlock decrypts, which is the one the server uses: an evaluator that holds the
key is inside the trusted domain. Settings: GPU (0), PORT (31050), ENGINE
(vllm), TRANSFORMS (perm orth), TASKS (gsm8k,mmlu), LIMIT, KEY,
OUT (out/eval-<model>-<engine>) and EXTRA.
Client-keyed serving
$ benchmarks/client.sh ~/models/Qwen3-1.7B # SGLang, GPU 0
$ ENGINE=vllm GPU=3 benchmarks/client.sh ~/models/Qwen3-1.7B
client.sh seals with --client (perm by default), then serves the sealed model
once with --key and once without a key from its -cloud directory behind
kllm client proxy, one after the other on one GPU. Both runs are scored by
tools/ppl.py (the client run against the keyed one), load-tested through the proxy, and
asked one chat prompt with request logging at its most verbose. The script then searches
both engine logs for three-word phrases of the prompt and the reply. Settings: GPU,
PORT (31060; the proxy uses PORT+2), ENGINE, TRANSFORM, KEY, OUT
and EXTRA. The results are in Client-keyed mode.
For Z-Image, start the latent server separately and run the comparison script:
$ kllm seal ~/models/Z-Image -o ~/models/Z-Image-client --client
$ CUDA_VISIBLE_DEVICES=0 kllm serve ~/models/Z-Image-client-cloud --client --host 127.0.0.1 --port 30010
$ CUDA_VISIBLE_DEVICES=1 python benchmarks/zimage_client.py ~/models/Z-Image ~/models/Z-Image-client \
--key ~/.kllm/keys/default.key --url http://127.0.0.1:30010 --seeds 0 1
For three fixed prompts per seed, from the same noise, it runs the plain pipeline, the plain
pipeline with the DiT’s attention on PyTorch’s math kernel (the noise floor), and the client
through the keyless server, and prints latent error and PSNR against plain and the
end-to-end times. Options: -o/--out (out/zimage-client), --size (1024),
--steps (28), --guidance-scale (4.0), --seeds (0). The results are in
Generating images in client-keyed mode.
MiniMax H3
$ benchmarks/h3.sh ~/models/MiniMax-H3/FL2VA # 49 steps, GPUs 0 and 1
$ STEPS=8 GPUS=2,3 benchmarks/h3.sh ~/models/MiniMax-H3/FL2VA
h3.sh runs plain and sealed FL2VA at the same time on two GPUs of one host, so that both
see the same contention. The sealed server draws the plain server’s noise, permuted into the
sealed latent basis, and both save their final latents. The script prints the end-to-end
times, the latent drift and the frame PSNR and audio SNR, and writes a side-by-side still
to OUT/grid.png. For the bf16 noise floor, run the plain model against itself with
--attention-backend torch_sdpa on one side. Settings: GPUS (0,1), PORTS
(30021,30022), TRANSFORM (perm), STEPS (49), SEED (7), KEY,
SEALED and OUT.
h3_int8.sh measures the INT8 DiT kernels on plain H3; see Running the H3 diffusion transformer on INT8 kernels. Settings:
GPU (0), PORT (30031), STEPS (49), SEED (7), VARIANTS
(plain int8 sdpa), INT8 (all, attention or linear) and OUT. The
results of both scripts are in Sealing and serving MiniMax H3 and Running the H3 diffusion transformer on INT8 kernels.
Attacks
$ python tools/attacks/weights.py --base ~/models/Qwen3-8B --transform perm
$ python tools/attacks/tokens.py --help
See Attacks for the procedure and the results.
Which script produced which result
Published result |
Command |
Notes |
|---|---|---|
LLM latency, throughput and perplexity, SGLang and vLLM |
|
One model and engine per run; tables combined by hand. |
fp16 log-probability comparison (0.053, 0.008, 0.032) |
|
Not scripted as such; serve in fp16 through the engine’s dtype option in |
MMLU and GSM8K |
|
|
Client-keyed LLM table and log check |
|
One engine per run. |
Client-keyed Z-Image fidelity |
|
Server started separately. |
H3 end-to-end time, latent drift, PSNR |
|
One step count per run; the SDPA row needs a manual run. |
INT8 time per step and fidelity |
|
Reports total denoising time. |
Weight recovery and token attacks |
|
|
INT8 per-layer timings, Z-Image VAE decode times, sealing times |
none |
Measured outside the scripts. |
Results: SGLang
One NVIDIA A40 48 GB, SGLang 0.5.21, bf16, under the conditions above
(benchmarks/llm.sh):
Qwen3-1.7B plain |
|
|
Qwen3-8B plain |
|
|
|
|---|---|---|---|---|---|---|
Median TTFT, concurrency 1 (ms) |
98.0 |
105.2 |
104.3 |
199.5 |
200.1 |
200.4 |
Median TPOT, concurrency 1 (ms) |
12.44 |
12.54 |
12.32 |
25.12 |
25.12 |
25.16 |
Output throughput, concurrency 16 (tok/s) |
563 |
577 |
580 |
333 |
333 |
332 |
Median TPOT, concurrency 16 (ms) |
21.5 |
22.7 |
22.4 |
35.3 |
36.2 |
37.3 |
Perplexity |
5.4991 |
5.5057 |
5.5315 |
1.9050 |
1.9036 |
1.9063 |
Mean |Δ log-prob| against plain |
0.069 |
0.098 |
0.031 |
0.045 |
Perplexity is over 14690 tokens of the GPL-3, GFDL-1.3 and Apache-2.0 license texts, which
Qwen3-8B has largely memorized. For scale, serving plain Qwen3-1.7B in fp16 instead of bf16
moves log-probabilities by 0.053 on average; served in fp16, perm moves them by 0.008
and orth by 0.032. The vLLM table for Qwen3-1.7B is in Serving with vLLM.
Results: other families
Same texts and load. Each cell gives output throughput at concurrency 16, perplexity, and for sealed variants the mean |Δ log-prob| against plain.
Model |
Engine |
plain |
|
|
|---|---|---|---|---|
Qwen2.5-1.5B-Instruct (Q/K/V biases) |
SGLang |
1443 · 1.4067 |
1446 · 1.4067 · 0.014 |
1442 · 1.4081 · 0.021 |
Llama-3.2-1B-Instruct (RoPE scaling, tied embeddings) |
SGLang |
1630 · 8.5347 |
1835 · 8.5313 · 0.043 |
1839 · 8.5007 · 0.061 |
Qwen3-1.7B-FP8 (128x128 blocks) |
SGLang |
1055 · 5.7789 |
1055 · 5.7752 · 0.064 |
|
Qwen3-1.7B-FP8 (128x128 blocks) |
vLLM |
605 · 5.7770 |
628 · 5.7815 · 0.058 |
|
Qwen3-30B-A3B-FP8 (MoE, 128 experts), TP=2 |
vLLM |
359 · 1.3424 |
378 · 1.3428 · 0.033 |
Median TPOT of sealed and plain variants agreed within 0.5 ms in every row. In every measured keyed configuration, sealed output throughput at concurrency 16 was at most 0.3% below plain, and in some rows higher; the higher figures are attributed to noise on the shared host (the plain Llama run came first). Before weight locking was added, sealing took 14 to 20 s for the 1 to 2B models and 12 minutes for Qwen3-30B-A3B-FP8 on CPU.
Stopping servers
Each script stops the server processes it started, by their process IDs, when it exits.
kllm serve replaces itself with the engine, so the recorded ID is the engine’s main
process; whether its worker processes exit with it depends on the engine. h3_int8.sh
starts each server in its own process group and stops that group. After an interrupted run,
check nvidia-smi and stop any remaining server by its own process ID before the next
run.
See also
Claims for how the measurements may be quoted.
Supported models for the measured families.
Contributing a serving recipe for contributing a measured recipe.