Repository navigation
feat: add ADR 0007, CLI/server parity harness and B=1 decode bench - #2194
Merged
Merged
Conversation
Phase 0 of epic #2166. Before the CLI and server decode paths merge into one engine, record the target design, the divergence we start from, and the single-stream throughput to protect. - ADR 0007 names the engine API (Engine, KvStore with dense and paged storage, PrefillPlan, RowSampler, FinishStep), maps phases #2168 to #2176 onto it, supersedes ADR 0004's KV/scheduler deferral, defines parity on ROCm and Metal, and holds the CLI-vs-server defaults table. Semantic rows are decided with reasons; the prefill chunk and single-sequence storage rows and the regression threshold list candidates and the deciding measurement, marked TBD for the measured baseline. - mlxcel-engine-parity (make engine-parity) runs one prompt and SamplingConfig through CxxGenerator and the in-process server scheduler at B=1, dense and paged, plus a prompt-cache miss vs hit, and prints the first divergent token per pair; --expect-identical exits 1 on divergence. - mlxcel-bench-engine (make bench-engine) times bench_decode's CxxGenerator call next to the server worker driven in-process, with --prefill-chunk and --decode-storage knobs; scripts/engine_bench_rounds.py runs interleaved A/B rounds with a null arm. - src/server/engine_probe builds the server exactly as start_server does; the prompt-cache store setup is extracted into resolve_prompt_cache_store for that, and the long-prompt corpus moves out of bench_decode so both benches share it. No decode behavior changes. Verified on qwen3-0.6b-4bit, llama-3.2-1b-instruct-4bit and qwen3.5-0.8b-4bit (functional runs only); engine_probe and server::startup unit tests, clippy and fmt pass. Refs #2167
The parity harness and the B=1 bench labelled a server run "Paged" whenever the scheduler resolved paged storage. Model-owned KV families (Gemma 3, Llama 4, Qwen 3.5) resolve to paged storage but decode a lone sequence through their dense caches, with only the block table paged, so their (c) row silently compared dense attention. On gemma-3-1b-it-4bit the paged run launched zero paged kernels; on qwen3-0.6b-4bit it launched 476. ServerEngineRun now carries paged_decode_launches, the delta of mlxcel_core's process-wide paged decode counters across the request. The harness appends it to the (c) row and marks a zero as dense attention; the bench records it per server row and warns when a paged row ran dense. ServerEngine::start also runs the main-thread CUDA graph hoist start_server runs (Gemma 4, #688). The rounds script rotates the arm order per round so a drift across a round does not land on one arm. ADR 0007 states the paged-kernel caveat, the full auto-to-paged condition and the per-row seed scope (first token only) as the code has them. Validated with clippy and engine_probe tests (12) on the cuda feature, and functional runs of the harness on qwen3-0.6b-4bit and gemma-3-1b-it-4bit and the rounds script on the server path. Refs #2167
Member
Author
Implementation Review SummaryIntent
Findings Addressed
Remaining Items
Verification
|
Member
Author
Security and performance reviewNo CRITICAL, HIGH or MEDIUM findings, so nothing was committed.
LOW, left open:
Checks run: |
engine_bench_rounds.py ran every benchmark process with no time limit, so a hung model load or decode stalled a round indefinitely. Each process now runs under --run-timeout seconds (default 3600, 0 disables) and a timeout fails the round with the captured output and a pointer to the flag. A user arm named like a generated null arm (for example dense-null next to dense) shared its records and summary row with it and corrupted the noise floor, so arm names ending in -null are now rejected before any output file is opened. Adds tests/test_engine_bench_rounds.py covering arm name checks, timeout, failure and missing-record handling, and a rotating-schedule run against a stand-in binary. docs/benchmarks.md and ADR 0007 mention the new flag and the reserved suffix. Validation: - python3 -m unittest tests/test_engine_bench_rounds.py: 12 tests pass Refs #2167
mlxcel-engine-parity built a zero-length priming request for the prompt-cache hit case when the rendered prompt was a single token, and failed deep inside the scheduler. An empty --prompt is now rejected before any model loads, and a rendered prompt shorter than two tokens (one when --no-prompt-cache-case is given) is rejected right after tokenization with the reason and the remedy. The pure logic in the new binaries is now unit tested: argument defaults and parsing, case construction, the --expect-identical determinism check and exit gate, divergence rows including n/a pairs and token-0 divergence, the text report and JSON output, benchmark measurement records, the side-by-side summary, CSV header and quoting, and the MLXCEL_PREFILL_CHUNK conflict check (which also rejects --prefill-chunk 0). The report and summary printers now build their lines in a function the tests call. Validation: - cargo fmt --all -- --check: clean - cargo clippy --profile test-fast --features cuda --lib --tests --bins -- -D warnings: clean - cargo test --profile test-fast --features cuda --lib server::engine_probe: 14 pass - cargo test --profile test-fast --features cuda --bin mlxcel-engine-parity: 18 pass - cargo test --profile test-fast --features cuda --bin mlxcel-bench-engine: 11 pass Refs #2167
The baseline run on GB10 (5 interleaved rounds, null arms, quiet-host gate) fills the rows ADR 0007 decides by measurement: - Prefill chunk: 2048. At 8192 tokens a 512 chunk raised TTFT by 8 to 29 percent on both paths and both models; decode did not move. - Single-sequence KV storage: dense. Paged decode at B=1 was 6 to 23 percent slower in every cell, with the paged kernel confirmed running; paged stays the batched backend. - Regression threshold: 1.0 percent, from the widest CLI null-arm decode delta (0.92 percent). The baseline also shows today's server single-stream decode 8 to 23 percent below the CLI, almost all of it from paged storage at B=1, and the parity harness finds the CLI, server-dense and server-paged paths identical under MLXCEL_SDPA_DETERMINISTIC=1 for Qwen3-1.7B and Llama-3.2-1B over 400 tokens, with only the prompt-cache hit diverging.
inureyes
added a commit
that referenced
this pull request
Oct 7, 2026
The test file added by #2195 (`src/execution/memory_estimate_inflight_tests.rs`) lacks the project license header, so the `license headers` job fails on `main` (run 37569698228) and on every PR CI run merged with it, for example #2194. This inserts the standard header with `python3 scripts/insert_apache_header.py`; `--check` now passes. No code change. The other failure on recent `main` runs, `WebUI installed artifact`, is unrelated: the GB10 runner could not obtain the host `gpu-lock` within 600 seconds because development benchmarks on that host held it (`issue-2190-bench-31b-b2`, `issue-2191-throughput`).
This was referenced Oct 7, 2026
inureyes
added a commit
that referenced
this pull request
Oct 8, 2026
## Summary End-of-run measurement for epic #2166 (ADR 0007's regression threshold and the speed questions the phases left open), on GB10 against two release builds: the pre-epic `CxxGenerator` build (4c44e31, PR #2194) and the final engine build (30ceea2, PR #2225). Adds `docs/benchmark_results/unified-engine-final-gb10-2026-10-08.md`, its raw records and drivers under `docs/benchmark_results/data/unified-engine-final-gb10-2026-10-08/`, and an index entry in `docs/benchmarks.md`. No code changes. ## Results - `mlxcel generate` (the engine's `DirectEngine` client) decodes within the 1.0 percent threshold in every cell: +0.82 and +0.31 percent on Qwen3-1.7B (256 and 8192-token prompts), +0.51 and +0.19 percent on Llama-3.2-1B. TTFT at 256 tokens is about 1 ms higher (+4.4 percent unresolved on Qwen3, +9.0 percent on Llama). - The server engine at B=1 with dense storage, the path `mlxcel run` and chat take since #2173, is 1.14 to 2.05 percent below the pre-epic CLI and misses the threshold in three of four cells. Phase 0 measured the same scheduler overhead before the epic, but `run` used `CxxGenerator` then, so `run` users see a 1 to 2 percent decode regression. - The server's default single-stream configuration (paged at `--parallel 4`) decodes as before; its 8192-token TTFT dropped 19 percent (Qwen3) and 12 percent (Llama) with the 2048-token chunk. - Batched serving: unchanged on Qwen3 defaults; Llama with Turbo4 KV is 16 and 18 percent higher at concurrency 4 and 8. - Prefill chunk under live decode (four streams, one 8192-token admission, Llama): 2048 gives about 2.0 s admitted TTFT and 128 ms live-stream p95 ITL; 512 gives about 3.9 s and 56 ms. A policy trade, not a speed win. - Gemma 3 paged vs dense under concurrency: dense 1 to 2 percent higher with overlapping ranges, unresolved; default unchanged. ## Test plan - [x] Single-stream: `scripts/engine_bench_rounds.py`, 5 interleaved rounds with a null arm and the quiet-host gate, both builds through `pick.sh`, under the host `gpu-lock` with no other GPU work - [x] Serving: `scripts/bench_serving_concurrency.py` and `scripts/bench_mixed_step_admission.py`, 3 alternating rounds per arm Part of #2166
This was referenced Oct 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 0 of epic #2166. ADR 0007 records the batch-native engine design and the CLI-vs-server defaults table, two tools measure where the two decode paths stand today, and a baseline run on GB10 fills the rows the ADR decides by measurement. No decode behavior changes.
What is in it
docs/adr/0007-unified-batch-native-engine.md, indexed indocs/adr/README.md):Engine,KvStoreover dense and paged storage,PrefillPlan,RowSampler,FinishStep) and maps refactor: one post-sample finish step for every CLI and server decode site #2168 to refactor: run mlxcel generate on the engine and retire CxxGenerator #2176 onto it.llama_decode, onecommon_sampler,llama-clias a client ofllama-server).repeat_last_n64, DRY window and the b10621 breakers, loop detection in the single finish step, server sampler defaults exposed to every client, greedy-by-default CLI clients, the per-slotmax_tokensrule, per-sequence seeds, prompt cache on for chat clients.make engine-parity(mlxcel-engine-parity) runs one prompt andSamplingConfigthroughCxxGeneratorand through the in-process server scheduler at B=1, dense and paged.identicalor the first divergent index and both token ids, plus the paged kernel launch count on the paged row.--expect-identicalexits 1 on divergence, and the tool refuses to run withoutMLXCEL_SDPA_DETERMINISTIC=1.make bench-engine(mlxcel-bench-engine) timesbench_decode'sCxxGeneratorcall next to the server worker driven in-process.--prefill-chunk(both paths) and--decode-storage(server).scripts/engine_bench_rounds.pyruns interleaved rounds with a rotating order, null arms, a quiet-host gate and a per-run timeout.src/server/engine_probebuilds the server the waystart_serverdoes. The prompt-cache store setup moved verbatim intoresolve_prompt_cache_storeso both share it, and the long-prompt corpus moved out ofbench_decodeso both benches synthesize the same prompt.Baseline (GB10,
docs/benchmark_results/unified-engine-baseline-gb10-2026-10-07.md)MLXCEL_SDPA_DETERMINISTIC=1, CLI, server-dense and server-paged are identical over 400 tokens for both models, greedy and seeded with penalties and DRY. Only the prompt-cache hit diverges: on Qwen3 greedy at token 0, and seeded at token 11 on Qwen3 and token 25 on Llama. That divergence is refactor: one prefill planner and one pad-trim hook for CLI and server #2170's first target.Changes during review
start_serverapplies.-nullsuffix guard were added.Test plan
cargo test --profile test-fast --features cuda --lib server::engine_probe(14). Bin tests:engine_parity18,bench_engine11.tests/test_engine_bench_rounds.py12.cargo clippy --profile test-fast --features cuda --lib --tests --bins -- -D warningsandcargo fmt --checkmake engine-parityon qwen3-0.6b, qwen3-1.7b, llama-3.2-1b and gemma-3-1b (the last confirming the dense-attention annotation).gpu-lock, raw records committed underdocs/benchmark_results/data/unified-engine-baseline-gb10-2026-10-07/Closes #2167