docs(benchmarks): price the qmv_wide narrow pin's collateral - #1278
Conversation
Issue #1261 asks for the collateral cost of the process-wide qmv_wide narrow pin as a number, measured on a generation 15+ host under the project's ABBA protocol. No existing harness could produce it: the B-sweep needs both arms pinned explicitly across server boots, and the mixed arm needs one MTP stream holding the speculative slot while concurrent classic streams are measured. scripts/bench_qmv_wide_pin.sh (new) drives both arms: `sweep` boots the server without a drafter, alternating MLXCEL_QMV_WIDE=1 and =0 boots in balanced ABBA blocks with discarded warm-up passes (any value of MLXCEL_QMV_WIDE is an operator pin, so the exactness gate's retry can never flip an arm mid-run); `mixed` boots a target with its drafter under MLXCEL_MTP_SLICE_GRANT_ROUNDS=0 so the first stream holds the tick-slice slot for its whole generation and every concurrent eligible request falls back to classic decode, then greps the gate log lines so each boot's arm identity is evidenced rather than assumed. Raw runs land in bench-results/ (gitignored); curated results belong in docs/benchmark_results/. scripts/bench_qmv_pin_mixed.py (new) is the mixed-arm client: one long MTP stream, then N concurrent classic streams whose decode rates are the reported quantity, with a window declared invalid unless the MTP stream decoded through at least 95% of it. scripts/bench_serving_concurrency.py now counts `reasoning_content` deltas alongside `content`: a reasoning model that spends its whole budget thinking previously produced no TTFT and no decode rate at all, which issue #1261 hit on Qwen 3.8. Validated by the measurement runs recorded in docs/benchmark_results/qmv-wide-pin-tax-m3ultra-2026-08-22.md (committed separately). Refs #1261
Records the issue #1261 Step 1 measurement on M3 Ultra (generation 15): the batched-decode tax of the narrow pin at B = 2/4/8 is at most 1% on Gemma 4 31B and 0.2% on Qwen 3.8 27B, spreads at or under 1%, because both MTP families decode batches as per-row M = 1 forwards that never dispatch the kernel the pin disables. The collateral cost that is real is one prompt-cache-adopted suffix prefill per request, +15.4 ms (Gemma) / +12.6 ms (Qwen), read as +33% / +28% on cache-hit TTFT, with uncached and chunked prefill measured as controls. The mixed arm (one MTP stream plus four classic streams on the Qwen pairing) bounds the bystander cost at 0.1 to 2.6%, tracking the MTP stream's own verify slowdown occupying the shared worker. Per the issue's exit condition, Step 2 (scoping the exact kernel to the verify forward) is documented as not built; the boundary that would reopen it, a real joint batched decode for an MTP family, is named. The measurement also settled a stale recipe: since #1199 the exactness gate retries without qmv_wide before MLXCEL_MTP_ALLOW_INEXACT is consulted, so on generation 15+ hosts where the narrow retry passes the override alone is inert (verified live: byte-identical output with and without it, 117 vs 139 tok/s against the pinned-wide recipe on the 12B pairing). docs/environment-variables.md corrects the MLXCEL_MTP_ALLOW_INEXACT row, gains the missing MLXCEL_QMV_WIDE row, and docs/benchmarks.md now states the working fast-row recipe (MLXCEL_QMV_WIDE=1 plus MLXCEL_MTP_ALLOW_INEXACT=1); the M5 Max drafter-step profile gets a dated note that its recipe predates the retry. Also recorded: on current main the 31B + bf16 assistant pairing probes non-identical under BOTH kernels on this host, so the default-env gate declines the burst #1217 enabled; filed for follow-up rather than resolved here. Refs #1261
…runs The qmv_wide pin record's central structural claim, that neither MTP family dispatches an `M = B` projection on batched decode, was evidenced for Gemma 4 by "does not override `forward_batched`, so it inherits the trait default". That is true of `src/models/gemma4.rs` but describes a route the measurement never took. `models/gemma-4-31b-it-4bit` carries `embed_vision.*` weights, so `gemma4_has_vision_weights` routes it to `LoadedModel::Gemma4VLM`, and `execute_batched_decode` calls `forward_batched_with_context_and_ids` with the batch's sequence ids, which `Gemma4VLModel` overrides (`src/vision/gemma4_vl.rs:687`) in favour of `forward_batched_with_seq_ids_dispatch`. The conclusion is unchanged, and in fact rests on firmer ground: that dispatch helper is itself an explicit per-row loop over `forward_with_sequence_id`, so `M = 1` per forward on the VLM route and on the text-only route alike. Only the citation was wrong, and a reader re-verifying it would have found the override and concluded the claim was false. Two quantities are also named more precisely. The Gemma verify-forward figure now says "verify forward" the way the Qwen one already did, because `docs/benchmarks.md` carries a second, unrelated 23% for end-to-end decode on the M5 Max code row. The mixed-workload conclusion quoted a composite "17 to 23%" range while measuring the Qwen pairing alone; it now quotes that pairing's own 17 to 20%. The suffix-forward fit tolerance reads 0.2 ms rather than 0.1 ms: 4.5 x 15.4 is 69.3 against a measured 69.5 on the B = 8 row. Refs #1261
Implementation Review SummaryIntentMeasure what the rest of a server process pays for the MTP exactness gate's process-wide The two load-bearing code claims, verified against sourceThe whole argument rests on neither MTP family dispatching an Qwen 3.5: correct. Gemma 4: off-path, corrected in 5c5fa1f. The record said Gemma 4 "does not override The conclusion survives, and on firmer ground: that helper is itself an explicit per-row loop over Findings addressed
Verified accurate, no change needed
Remaining items
Verification
A bilingual technical report is committed at |
Security and lifecycle review of #1278. No runtime code path is involved, but both scripts cost the machine they run on when they go wrong, and two of the failures were reproduced on the measurement host itself. `scripts/bench_qmv_wide_pin.sh` could not be interrupted. `trap cleanup EXIT INT TERM` runs the handler and then resumes the script where the signal landed, so a Ctrl-C part way through an ABBA run killed the current server and booted the next arm, ending with status 0. Verified against a replica: the old form ran all three iterations and exited 0, the new form aborts mid-iteration with 130. The signal handlers now exit, HUP is trapped so an SSH drop cannot orphan a 31B server, shutdown escalates SIGTERM to SIGKILL after SHUTDOWN_GRACE_S instead of waiting forever on a wedged server, and SERVER_PID is cleared before the helper returns so a second handler pass cannot signal a recycled pid. The same driver would have measured the wrong server. `wait_ready` probed `/v1/models` before checking that the boot it launched was still alive, so a server left on port 8113 by an earlier aborted run answers, every subsequent launch dies on EADDRINUSE, and all arms report success while running one kernel. That is the only thing the ABBA design establishes. There is now a preflight that refuses to start when the port already answers, the liveness check runs first inside the loop, and an inherited `MLXCEL_QMV_WIDE` or `MLXCEL_MTP_ALLOW_INEXACT` is rejected up front because those two variables are what define the arms. `env.txt` records the inherited `MLXCEL_`/`MLX_`/`LLAMA_ARG_` environment, the binary and output directory are preflighted, arm tokens are validated before they reach an output path, and a python harness that exits non-zero now leaves a `HARNESS-FAILED` marker plus a run-level warning rather than silently costing one arm a sample. `scripts/with_indexers_paused.sh` had two holes in the resume paths its own header calls non-negotiable. `INDEXER_RESUME_DEADLINE` was spliced into a detached `nohup bash -c` string, so `INDEXER_RESUME_DEADLINE='0; touch /tmp/pwned'` executed the injected command in a process that outlives the wrapper; confirmed, and now rejected by an integer check with the deadline and pid list passed as positional arguments. An unchecked `mktemp` left PIDFILE empty, after which daemons were SIGSTOPped with their pids recorded nowhere and neither the trap nor the backstop could reach them; the failure now refuses to suspend anything, and pids are recorded before the STOP rather than after. QUIT joins the trap list, and the backstop is retired on any ending that reaches the trap: eight of them were resident on this host, one per run, the oldest 2h41m old, and two runs had left a populated pid list behind, meaning their trap never fired and only the two-hour deadline stood between the machine and permanently suspended Spotlight. `scripts/bench_qmv_pin_mixed.py` treats an MTP stream that did not drain as an invalid window, because a stream still holding the tick-slice slot makes the next window's MTP stream a classic one under the same label, which is the shape change the overlap check exists to catch. A malformed `usage` block no longer kills a reader thread, and whatever exception ended a stop-induced read is recorded instead of discarded. `scripts/bench_serving_concurrency.py` had the same `int(usage["completion_tokens"])` hazard, and there it was worse: TypeError is not in the surrounding except tuple, so it propagated out of the executor through the gather in `run_level` and aborted every remaining concurrency level. Reproduced against a stub, pre-fix exit 1 with a traceback, post-fix the level completes on the delta count. Refs #1278, #1261
Security and performance reviewScope: the three harness scripts. No Rust changed, so there is no production code path to price here, and no runtime-performance findings are claimed against benchmark scripts. What matters in a harness is cleanup and whether a failure is loud, and that is where every finding below sits. Two of them were reproduced on this host, using the leftovers of this PR's own measurement runs as evidence. Fixed in d749ad5. HIGHThe driver could not be interrupted. A stale server on the port was measured as if it were the freshly booted arm.
MEDIUM
Shutdown was SIGTERM-only with an unbounded Inherited Python harness exit status was discarded. A
A leaked MTP stream could silently change the next window's shape. The backstop leaked one process per run. Eight LOWArm tokens flowed unvalidated into output paths ( Field evidence worth keepingTwo of the wrapper's leftover pid files on this host still held pids, from the 02:09 and 02:56 runs, which means Looked at, not changedThe byte-at-a-time SSE read loop (
The Verification
|
The MLXCEL_QMV_WIDE + MLXCEL_MTP_ALLOW_INEXACT insertion above it left "Keeping byte-identity on the" and "code row, by dropping" as two short orphaned lines instead of continuing this file's ~80-column hard wrap. Markdown renders it the same either way, but every other paragraph in this file wraps at the convention, so the ragged lines stood out on a source read. Refs #1278, #1261
PR Finalization CompleteSummary
What was checkedDocumentation completeness. Both new harness scripts ( Bilingual docs. Checked Lint/format. Test coverage. Judged not warranted. The one precedent in this repo for testing bench-script internals, Results doc consistency. Confirmed the filename is All checks passing. Ready for review. |
Summary
Issue #1261 gated any scoping of the qmv_wide narrow pin on first measuring what the rest of the process pays for it, with an explicit exit condition: if the tax is small, document the number and stop. Measured on the required generation 15 host (M3 Ultra, 512 GB): the B = 2/4/8 batched-decode tax is at most 1% on Gemma 4 31B and at most 0.2% on Qwen 3.8 27B (8 and 4 samples per arm per cell, every spread at or under 1%, both arms pinned explicitly via
MLXCEL_QMV_WIDE). The reason is structural rather than numerical: both families the pin can fire on decode batches as per-sequence M = 1 forwards (Gemma 4 dispatches one row per forward throughforward_with_sequence_id: on the VLM route the measured checkpoint takes, viaGemma4VLModel'sforward_batched_with_context_and_idsoverride andforward_batched_with_seq_ids_dispatch; on the text-only route, via the trait default's per-row loop. Qwen 3.5's override takes its per-row branch for every single-token input), so batched decode never dispatches the kernel the pin disables. The collateral cost that is real is one prompt-cache-adopted suffix prefill per request, whose M lands in the qmv window: +15.4 ms per forward on Gemma (+33% on cache-hit TTFT), +12.6 ms on Qwen (+28%), each fitting a single per-forward constant across all concurrency levels, with uncached and chunked prefill measured as controls (+0.2 to +1.1%). The mixed arm (one MTP stream holding the speculative slot plus four classic streams, Qwen pairing) bounds the bystander cost at 0.1 to 2.6%, and the loss tracks the MTP stream's own narrow verify occupying the shared worker, not the classic streams' kernels. Per the issue's exit condition, Step 2 is documented as not built; the boundary that would reopen it (a real joint batched decode for an MTP family, as Gemma 3 and Llama 4 already have) is named in the record.The measurement also settled the stale
MLXCEL_MTP_ALLOW_INEXACTrecipe. Since #1199 the gate retries without qmv_wide before the override is consulted, so on generation 15+ hosts where the narrow retry passes the override alone is inert: verified live with all four env recipes on the pairing #1258 measured, with the log lines, byte-identical output between the default env and the override-alone run, and the 117 vs 139 tok/s throughput split as evidence. The fast-kernel recipe isMLXCEL_QMV_WIDE=1 MLXCEL_MTP_ALLOW_INEXACT=1. Note the issue body's own description of its mixed arm ("MLXCEL_MTP_ALLOW_INEXACT=1with the switch left wide") needs the same correction; the measured arm B used the two-variable recipe. The docs are corrected accordingly, and the merged PR descriptions of #1199 and #1258 carry a prepended dated note pointing here.Found on the way and recorded for follow-up: on current
mainthe 31B + bf16 assistant pairing probes non-identical under BOTH kernels on this host ("Disabling qmv_wide did not make it exact either"), so the default-env gate declines the batch-capable burst #1217 enabled; #1217's 1.95x to 2.65x rows were measured at9e2c6675, which predates #1258's Gemma probe.What changed
scripts/bench_qmv_wide_pin.sh(new): ABBA boot driver for both measurement arms (sweep: pinned wide/narrow B-sweep without a drafter;mixed: one MTP stream plus N classic streams, with each boot's arm identity grepped from the gate log lines).scripts/bench_qmv_pin_mixed.py(new): mixed-workload client; classic decode rates are the reported quantity, and a window is invalid unless the MTP stream decoded through at least 95% of it.scripts/bench_serving_concurrency.py: countreasoning_contentdeltas alongsidecontent; a reasoning model that spends its budget thinking previously reported no TTFT and no decode rate at all.docs/benchmark_results/qmv-wide-pin-tax-m3ultra-2026-08-22.md(new): the full measurement record, including the contaminated long-context cell reported as text-divergence-contaminated rather than averaged in.docs/benchmarks.md: link the record from the pin discussion; correct the fast-row reproduction recipe and the "declining probe" sentence for the post-feat(speculative): let the MTP gate buy back exactness by dropping qmv_wide #1199 retry ordering.docs/environment-variables.md: correct theMLXCEL_MTP_ALLOW_INEXACTrow and add the missingMLXCEL_QMV_WIDErow.docs/benchmark_results/mtp-drafter-step-profile-m5max-2026-08-17.md: dated note that its reproduction recipe predates the feat(speculative): let the MTP gate buy back exactness by dropping qmv_wide #1199 retry..gitignore: ignore the rawbench-results/run directories the driver writes.scripts/with_indexers_paused.sh: refuse to suspend anything without a resume list, validateINDEXER_RESUME_DEADLINEas an integer instead of splicing it into a detachedbash -cstring, and record each pid before it is stopped.TECHNICAL_REPORTS/1278-qmv-wide-pin-tax-measurement-20260822.{en,ko}.md(new): the bilingual technical report for this measurement.Changes during review
The measurement and its conclusion are unchanged; the review cycles hardened the harnesses that produced it and corrected one code citation.
embed_vision.*weights, so it routes throughGemma4VLModel, which does overrideforward_batched_with_context_and_ids. That override delegates toforward_batched_with_seq_ids_dispatch, itself an explicit per-row loop, so the per-sequenceM = 1conclusion holds on the route the run actually took. The Summary above cites both routes.traphandler that returned resumed the loop, so an interrupt mid-run killed one server and booted the next arm, completing all boots and exiting 0. Handlers now exit, andHUPwas added so a dropped connection cannot orphan a 31B server.wait_readyprobed the health endpoint before checking that the launched child was alive, so a server already holding the port would answer and every boot would measure one kernel while reporting success. Arm identity is the only property the ABBA design establishes, so this is now a port preflight followed by a liveness check.MLXCEL_*export could silently collapse the two arms of the mixed run. It is now rejected up front, and the run records the inherited environment.TypeErrorfrom a malformedusagefield was outside the surrounding except tuple inbench_serving_concurrency.pyand propagated through the gather, killing every remaining concurrency level. It is now handled per level.Test plan
scripts/with_indexers_paused.sh, Time Machine off; per-stream generation lengths verified equal across arms (59 tokens) viausagecounts.gemma-4-12b-it-4bit+ assistant; log lines, byte-identity diffs, and throughput recorded.python3 -m py_compileon both Python harnesses;bash -non the driver. No Rust source changed.bash -non both shell scripts,python3 -m py_compileon both Python harnesses, andcargo fmt --checkall clean. Interrupt handling, port preflight, and the mktemp failure path each verified against a replica or stub.Closes #1261