Skip to content

feat(bench): add reproducible serving performance harness - #341

Open
tuxevil wants to merge 1 commit into
FlashML-org:mainfrom
tuxevil:feat/serving-performance-harness
Open

tuxevil wants to merge 1 commit into
FlashML-org:mainfrom
tuxevil:feat/serving-performance-harness

Conversation

@tuxevil

@tuxevil tuxevil commented Sep 2, 2026

Copy link
Copy Markdown

Summary

  • Add benchmarks/bench_serving.py, a reproducible client-wall performance harness for an already-running FreeToken server.
  • Calibrate prompt sizes through /v1/messages/count_tokens.
  • Measure streamed /v1/chat/completions TTFT, decode throughput, effective prefill throughput, and total latency.
  • Capture model, server, hardware, KV/MoE/Mamba cache geometry, MoE residency, and cache budget.
  • Benchmark fresh, identical-prefix, and small-suffix prefix-cache behavior.
  • Add CPU-only unit tests and usage documentation.

Motivation

FreeToken currently has serving benchmarks, but no small, generic harness that clearly separates client-observed performance from engine-internal telemetry.

This harness records measurements at the HTTP client boundary. Fields prefixed with client_wall_ include queueing, prefill, scheduling, detokenization, and SSE delivery.

Design

The harness:

  1. Validates that the server is ready.
  2. Collects metadata from /health, /v1/models, /v1/cache/status, and /v1/stats.
  3. Performs configurable warmup runs.
  4. Calibrates deterministic prompts to approximately 512, 1024, 2048, and 4096 tokens.
  5. Measures fresh prefill performance.
  6. Measures steady-state single-stream decode performance.
  7. Measures prefix-cache reuse through an ordered fresh → identical → small_suffix sequence.
  8. Writes machine-readable schema version 1 JSON and prints a human-readable summary.

The benchmark uses explicit greedy sampling with thinking disabled so runtime measurements are not affected by reasoning nondeterminism.

Decode throughput follows the existing serving benchmark convention:

(completion_tokens - 1) / (last_token_time - first_token_time)

Scope

This first version intentionally:

  • connects to an existing server;
  • measures one serving configuration at a time;
  • uses standard-library HTTP/SSE handling;
  • does not add new server endpoints;
  • does not start or stop the server;
  • does not run concurrency or backend sweeps;
  • does not parse engine logs;
  • does not perform behavioral evaluation.

These capabilities can build on the schema in later PRs.

Validation

  • PYTHONPATH=python:. uv run --no-project --with pytest pytest -q tests/benchmarks/test_bench_serving.py

    • 13 tests passed
  • uv run --no-project --with ruff ruff check benchmarks/bench_serving.py tests/benchmarks/test_bench_serving.py

  • uv run --no-project --with ruff ruff format --check benchmarks/bench_serving.py tests/benchmarks/test_bench_serving.py

  • python -m py_compile benchmarks/bench_serving.py

  • git diff --check

The full repository test suite was not collected in this environment because FreeToken's runtime dependencies, including PyTorch, are not installed.

MT-z added a commit to MT-z/FreeToken that referenced this pull request Sep 16, 2026
…red)

Two upstream PRs taken into the daily branch, both clean merges:

  FlashML-org#278 bernimccoy  fix(moe): match the benchbw verdict to the served model's expert size
  FlashML-org#341 tuxevil     feat(bench): add reproducible serving performance harness

Chosen on one criterion: does it still hold when the hardware changes.

FlashML-org#278 is the one that bites. `load_backend_recommendation` keyed the `--moe-backend auto`
verdict on the expert FORMAT alone, and `ft bench bw --dtype` measures one geometry per
format (nvfp4: 8.0 MB experts). The CPU MoE kernel has a per-expert fixed cost, so that
verdict overstates the CPU side for a small-expert model -- 90 GB/s at the dtype geometry
vs 32 GB/s at Qwen3.6-35B-A3B's 1.7 MB experts on the same box, and auto then picks hybrid
where offload decodes 3x faster (27 vs 84 tok/s on a 4070 SUPER). The PR adds
`_comparable()` so a dtype verdict is only reused when the benched expert size is within a
factor of the served model's. Ornith-1.5-35B-A3B is on the small-expert side, so this box
is in the affected class; the practical effect here is not yet measured.

FlashML-org#341 is a pure addition (+958/-0): benchmarks/bench_serving.py measures TTFT, decode,
effective prefill and total latency against a running server, and records model, server,
hardware, KV/MoE/Mamba geometry, MoE residency and the cache budget with the numbers. It
is the instrument for comparing a box before and after a change, which is why it is here
rather than in the "nice to have" pile.

Full suite, one process: 12 failed, 1812 passed, 57 skipped. try/all at c9242db fails the
same 12 by name (diff of the sorted FAILED lists is empty) with 1798 passed, so this adds
14 passing tests and breaks none.

Assisted-by: Claude Opus 5
@earlvanze

Copy link
Copy Markdown
Contributor

We exercised this benchmark shape on RadixArk/Qwen3.8-Flash-Next-NVFP4 and found a useful follow-up for the harness: a concurrency sweep that makes admission and queueing explicit.

In our RTX 5090 run, c8 delivered 115.249 aggregate output tok/s and 32.428 median steady decode tok/s/request. A separate c16 run with both --max-running-requests=16 and --cuda-graph-max-bs=16 admitted all 16 requests and reached 123.079 aggregate output tok/s, but median steady decode fell to 11.614 tok/s/request. Without recording the active-request limit, it is easy to confuse a true c16 run with c16 offered to an eight-slot worker, where half the requests queue. Aggregate throughput alone also hides the per-session latency collapse.

Would you be open to an extension that adds configurable concurrency levels and records, per level:

  • offered concurrency and configured active-request/CUDA-graph limits
  • admitted versus queued requests, where observable
  • total prompt and completion tokens plus batch wall time
  • client-wall effective prefill throughput
  • aggregate delivered output throughput
  • median/p95 TTFT and per-request steady decode throughput
  • machine-readable JSON results using the existing schema conventions

We can prepare the patch against this branch if that scope fits your intent for #341. Published evidence and the exact c1/c2/c4/c8/c16 records are here:
https://tekiz.ai/benchmark-data/qwen38-flash-next-rtx5090-nj-20260920.json

The c16 prefill figure in that artifact is explicitly client-wall/effective rather than engine-internal telemetry, consistent with this PR's measurement boundary.

@tuxevil

tuxevil commented Sep 20, 2026

Copy link
Copy Markdown
Author

Thanks — this is exactly the kind of follow-up the schema was intended to support.

The c8/c16 results make the case clearly: aggregate throughput alone hides the per-session latency collapse, and without recording the active-request and CUDA graph limits it is easy to mislabel an offered concurrency level as an admitted one.

I would welcome a focused concurrency extension built on top of this harness. The fields you listed fit well with the current measurement boundary, especially:

  • offered versus admitted concurrency;
  • max_running_requests and cuda_graph_max_bs;
  • queued requests where the server exposes enough information to distinguish them;
  • batch wall time and total prompt/completion tokens;
  • aggregate client-wall output throughput;
  • per-request TTFT p50/p95 and steady-state decode throughput.

My preference would be to keep #341 focused on the single-stream serving protocol and schema foundation, then implement the concurrency sweep as a follow-up PR based on this branch. That should let us settle the admission/queue semantics explicitly instead of making them implicit in the first version.

The RTX 5090 data is a very useful reference case. In particular, the contrast between c8 and the fully admitted c16 run is exactly the kind of result this harness should make easy to reproduce and compare.

KarrAcaRn pushed a commit to KarrAcaRn/FreeToken-ByAI that referenced this pull request Oct 3, 2026
…ing performance harness

Assisted-by: Claude Opus 5.5
KarrAcaRn pushed a commit to KarrAcaRn/FreeToken-ByAI that referenced this pull request Oct 3, 2026
… bearer header

next serves an --api-key (FlashML-org#305) that answers 401 to unauthenticated requests,
so the harness could not measure such a server. Take --api-key (default
$FREETOKEN_API_KEY, like ft serve) and send it on every request.

Assisted-by: Claude Opus 5.5
KarrAcaRn pushed a commit to KarrAcaRn/FreeToken-ByAI that referenced this pull request Oct 3, 2026
…report as a miss when reporting is on

The server omits prompt_tokens_details for a zero prefix-cache hit (sglang
convention), so every fresh prompt looked like a server without
--enable-cache-report and the fresh-prefill rate, the harness's main number,
was always n/a. Probe once whether a repeated prompt reports a hit; when it
does, an absent object is a miss. Without reporting the hit columns stay n/a.

Assisted-by: Claude Opus 5.5

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants