Repository navigation
update: offer prompt lookup on the server without slowing plain decode - #2262
Merged
Merged
Conversation
The server could not offer prompt lookup: SpeculativeDispatch::resolve returned Disabled before reading --draft-kind when no drafter checkpoint was set. This is stage A of #2255. - SpeculativeDispatch::PromptLookup { config, max_batch } resolves before the draft-model early return; a draft model with it is InvalidKind, --draft-block-size sets max_draft, and is_kind_specific() stays false so the burst path is never entered. prompt_lookup_config() is the scheduler's accessor. - New --prompt-lookup-max-batch on mlxcel serve and mlxcel-server (provisional default 2, DEFAULT_PROMPT_LOOKUP_MAX_BATCH), plumbed into ServerConfig; --spec-type none also drops prompt-lookup; --draft-kind help lists prompt-lookup. - The verify round moves out of DirectEngine::speculate into Engine::verify_round (engine/verify_round.rs), with Engine::verify_rounds_unsupported as the one eligibility rule; mlxcel generate --prompt-lookup now runs through both. Validated: mlxcel-core engine::, speculative::, drafter:: and root server::speculative_dispatch tests pass; clippy clean. Refs #2255
Stages B and C of #2255. Under --draft-kind prompt-lookup an eligible sequence gets a PromptLookupDrafter on its SequenceInfo at prefill completion. Each decode tick asks the gated-in rows for a proposal on their committed tokens: a proposing row runs one Engine::verify_round on its own, the rest take the regular batched step, so concurrent rows keep getting a token every tick. Ineligible requests (multimodal, structured output, logprobs, feedback or extended sampler chains, models a trim cannot roll back) decode plainly with a debug log. Proposal-free ticks stay on the lookahead pipeline: proposals are asked for before the step n+1 prime; a proposal while a step is in flight commits that step with no prime, retracts the asks and makes the next tick synchronous; a prime after a synchronous tick waits until nobody proposed and every gated-in drafter pipelines plain rounds. Verify rounds run only while the active batch is at most --prompt-lookup-max-batch and no unified KV budget is set; gated-out rows call no draft_block, so no governor miss is recorded. The invariants (append ownership, admission, cancellation, errors) are in the scheduler::prompt_lookup module docs, and each new test pins one. Finished requests report prompt-lookup acceptance counters. Validated: scheduler_prompt_lookup_tests (cycle-model Llama: plain-equal output, per-tick progress of a concurrent row, pipelined proposal-free ticks, synchronous switch with no prime, max-batch gate, admission teardown), engine verify_round_tests, server::batch and mlxcel-core engine/speculative/drafter suites; clippy clean. Refs #2255
Stage D of #2255. scripts/bench_serving_concurrency.py gains --prompt-style plain|copy (copy wraps the synthetic paragraph as "Repeat the following paragraph exactly: ...") for the flag-on versus flag-off concurrency measurement. CONTINUOUS_BATCHING.md documents --draft-kind prompt-lookup and --prompt-lookup-max-batch on the servers, speculative-acceptance.md and llama-server-compat.md list the new path and its timings, ADR 0009 records Engine::verify_round and Engine::verify_rounds_unsupported, and CHANGELOG Unreleased gains the entry. Refs #2255
inureyes
marked this pull request as ready for review
October 10, 2026 15:24
The server never ran the verify-width warmup that mlxcel generate --prompt-lookup runs, so the first requests at each block width paid that width's first-use kernel cost mid-decode (on GB10 more than the rest of a short reply). The loop moves from DirectEngine::warm_up_verify_widths to Engine::warm_up_verify_widths, which unwinds a width's appends before returning an error; the CLI delegates to it, and the scheduler runs it once on the first eligible prefill, before a finish at prefill returns, so the one-token startup warmup request covers it. PromptLookupDrafter no longer builds its n-gram index at prefill; the first draft_block builds it, as it already extended it. A row that --prompt-lookup-max-batch keeps out of verify rounds no longer pays an index build over its whole prompt at prefill completion. Proposals are unchanged. Tests: the scheduler warms once (also for a request that finishes at prefill) and leaves the KV offset at the prompt; the drafter builds no index at prefill and proposes what find_draft finds. mlxcel-core engine::, speculative::, drafter::; root server::batch, server::speculative_dispatch, generate commands, cli_help_consistency; clippy -D warnings and fmt pass. Refs #2255
Member
Author
Implementation Review SummaryIntent
Findings Addressed
Remaining Items
Verification
|
A proposal was capped by max_tokens only. With --max-kv-size and context shifting off, a client prompt that ends just under the bound and copies itself made the verify forward append up to max_draft - 1 positions past the KV window the plain step that ends the row would hold; the bound then finished the row and those positions were forwarded only to be discarded. ContextBound::verify_room now gives the room left before the context-bound stop, and proposal_budget takes the smaller of it and the max_tokens room, so every forwarded position is used and the forward never exceeds the plain path's peak. The verify-width warmup also skips a prefill without room for the widest block and leaves the warmup to a later one. ask_prompt_lookup sliced generated_tokens[observed..] unchecked; a row whose drafter ever ran ahead of its tokens would panic the scheduler thread. It now drops the drafter and decodes the row plainly. Tests: proposals_stop_at_the_context_bound, a_drafter_ahead_of_its_row_decodes_plainly. server::batch 524 pass, clippy and fmt clean. Refs #2255
Member
Author
|
Security and performance review (fixes in a64431e). Fixed:
Open:
Verified, no change: every new device read goes through
|
…values Add two scheduler-level tests on the stub cycle model. An EOS inside an accepted verify block ends the row at the stop token, stores nothing after it, and leaves the KV at the prompt plus the stored tokens, matching the plain decode with the same stop id. A cancel set before a synchronous verify tick lets the round run to its end, and finalize_completed releases the row after the tick with the I1 state intact. The cycle model has no EOS, so the stop id rides in the request's SamplingConfig. A verify-eval failure test is not added: the harness has no way to make a device read throw without a new production seam, and the engine-level paths are covered in verify_round_tests. State in the I1 invariant that a stop token inside the block is not stored. Mark DEFAULT_PROMPT_LOOKUP_MAX_BATCH and PROMPT_LOOKUP_PAGED_CONTEXT_LIMIT as provisional pending the end-of-run #2255 measurement, and add the default (2, provisional) to the --prompt-lookup-max-batch help text on mlxcel serve and mlxcel-server. Validation: - cargo test --profile test-fast --features cuda --lib scheduler_prompt_lookup_tests: 11 passed - cargo test --test cli_help_consistency --test dead_doc_pointers: 27 and 2 passed - cargo clippy --lib --tests --bins --examples -D warnings, cargo fmt --check, insert_apache_header.py --check: clean Refs #2255
…ver-prompt-lookup
The new ServerConfig field broke server_config_schema_classifies_all_111_fields. It is read-only at runtime (the worker reads it when it builds its scheduler, like the other speculative knobs) and reported as an integer.
This was referenced Oct 10, 2026
inureyes
added a commit
that referenced
this pull request
Oct 10, 2026
## Summary End-of-run measurement for #2229 (PR #2256) and #2255 (PR #2262) on GB10, and the two values #2255 left provisional, now set from it. Report: `docs/benchmark_results/server-prompt-lookup-gb10-2026-10-11.md`; drivers, raw output and analysis script under `docs/benchmark_results/data/server-prompt-lookup-gb10-2026-10-11/`. - `PROMPT_LOOKUP_PAGED_CONTEXT_LIMIT`: `None` to `Some(4096)`. A paged sequence stops running verify rounds past 4096 tokens of context and decodes plainly. - `DEFAULT_PROMPT_LOOKUP_MAX_BATCH`: stays 2; the doc comment and the `--prompt-lookup-max-batch` help now cite the measurement instead of calling it provisional. - `docs/CONTINUOUS_BATCHING.md` and the CHANGELOG entry state the measured gains and the cost on low-copy chat, and point to #2264. ## Results **#2229, penalty requests on the CLI path** (`--repetition-penalty 1.1`, 5 rounds with a null arm): against main before #2229 the engine client now decodes 23 % (Qwen3-1.7B, 256 tokens), 16 % (Qwen3, 8192), 31 % (Llama-3.2-1B, 256) and 23 % (Llama, 8192) faster, every round above every round. Against the pre-epic `CxxGenerator` (`4c44e317`) it is equal on Qwen3 at 256 tokens (inside the null range) and 1.5 to 2.7 % faster in the other three cells. **#2255, server prompt lookup** (Qwen3-1.7B, 256 tokens): | Criterion | Result | |---|---| | (a) plain chat, B=1, flag on vs off, no worse than -1.0 % | **Not met.** -3.49 % (-3.73..-3.22) over 8 chat-template questions x 5 rounds, 25 to 45 % of proposed tokens accepted. Tracked in #2264. | | (b) copy-heavy, B=1 | +53 to +67 %, 4.2 tokens per verify | | (c) concurrency 4 and 8, default gate | on matches off in every workload; a gate of 8 loses 13 % on the chat-template copy workload at concurrency 4, so the default stays 2 | | (d) paged, long prompts | on / off ratio for copy and open question: 2.69x / 1.27x at 1024, 2.79x / 1.64x at 2048, 1.42x / 1.12x at 4096, 1.15x / 0.62x at 8192; dense at 8192: 2.10x / 0.99x | The flag is opt-in, so decode without it is unchanged. ## Test plan - [x] clippy `--profile test-fast --features cuda --lib --tests --bins -D warnings` - [x] `--lib server::batch` (526), `server::speculative_dispatch` (17), `--test cli_help_consistency` (27), `--test dead_doc_pointers`, Apache header check, all under `gpu-lock` with `--test-threads=1` - [ ] `scripts/ci/check_binary_assets.py` fails on main's two undeclared tarballs from #2250 (`docs/benchmark_results/data/rocm-unified-engine-gfx1151-2026-10-08/`); this PR adds no binary file Refs #2229, #2255, #2264
9 of 14 tasks
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
mlxcel serve/mlxcel-server --draft-kind prompt-lookup(no draft model) run prompt-lookup verify rounds per sequence inside the batch scheduler, at most one per tick, while ticks on which no row proposes stay on the lookahead pipeline exactly as without the flag.SpeculativeDispatch::PromptLookup { config, max_batch }resolves before the draft-model early return; a draft model with it isInvalidKind;--draft-block-sizesetsmax_draft;--spec-type nonedrops it.is_kind_specific()stays false, so the burst path is never entered and the scheduler readsprompt_lookup_config(). New--prompt-lookup-max-batchon both server binaries, classified read-only in the runtime settings schema.mlxcel generate --prompt-lookupand its--draft-kindrejection are unchanged.DirectEngine::speculateintoEngine::verify_round(engine/verify_round.rs), withEngine::verify_rounds_unsupportedas the one eligibility rule andEngine::warm_up_verify_widthsas the one warmup; the CLI and the scheduler both run them. ADR 0009 amended.scheduler/prompt_lookup.rs): drafter onSequenceInfo, primed at prefill completion; proposing rows verify alone, the rest take the batched step. Switch rule: ask before the step n+1 prime; a proposal commits step n (throughtry_eval) with no prime, retracts the asks and makes the next tick synchronous; re-prime only when nobody proposed and every gated-in drafterpipelines_plain_rounds(). The module docs state invariants I1 to I8 (append ownership, admission, cancellation, errors), and each scheduler test pins one.--prompt-lookup-max-batch, no unified KV budget is set, and the proposal fits undermax_tokensand the context-bound stop. A gated-out row calls nodraft_block, so its governor records no miss.debuglog naming why.scripts/bench_serving_concurrency.py --prompt-style plain|copy.Provisional values for the end-of-run measurement
DEFAULT_PROMPT_LOOKUP_MAX_BATCH = 2(server/speculative_dispatch.rs): one extra forward per proposing row at most doubles a step at B <= 2. Measurement (c) sets the final value.PROMPT_LOOKUP_PAGED_CONTEXT_LIMIT = None(scheduler/prompt_lookup.rs), no limit, pending measurement (d).Changes during review
--max-kv-sizecould make a verify forward append up to 63 positions the stop then discarded); the same bound in the warmup; an unchecked slice in the ask path replaced, since a scheduler panic stops all serving.prompt_lookup_max_batchclassified in the runtime settings schema (server_config_schema_classifies_all_112_fields).Test plan
All on GB10 under
gpu-lock,--profile test-fast --features cuda, each CUDA test with-- --test-threads=1.speculative_dispatch_tests(5),engine::verify_round_tests(3),scheduler_prompt_lookup_tests(11, a cycle-model dense Llama, KV offset checked against committed tokens after every tick;proposal_free_ticks_stay_pipelinedfails on main)-D warnings(root and mlxcel-core); mlxcel-coreengine::,speculative::,drafter::,session,sampling,generate::; rootserver::batch,server::speculative_dispatch,server::in_process,server::runtime_settings,engine_probe,backend::,cli_input,commands::,mlxcel-serverbin;cli_help_consistency,dead_doc_pointers,model_loader_stdout_guardmake verify-test-cuda: 13,868 passed; the only failures are the pre-existing SSM bf16 parity tests (chore: post-epic cleanup of compat paged kernels, the model-owned paged flag, and the CUDA SSM bf16 parity failures #2230) and the schema test fixed in 9f5b5a8mlxcel generate --prompt-lookupon Llama-3.2-1B, greedy, 160 tokens: identical to the build before this PRMLXCEL_SDPA_DETERMINISTIC=1,/completiongreedy, flag on vs off: Qwen3-1.7B at 140 tokens (auto, dense, paged) and Llama-3.2-1B at 160 tokens (auto, paged) identical for copy and plain prompts, concurrent copy + plain equal their solo outputs,draft_kindprompt-lookup withdraft_n_accepted > 0;mlxcel serveidentical; Gemma-3-1B identical with prompt lookup declined (non-trimmable sliding-window cache, as on the CLI). At 160 tokens the Qwen3 copy reply diverges once at about token 150 on an exact tie (top two logprobs both -1.625 on the flag-off server)mlxcel-engine-parity -n 400on Qwen3-1.7B and Llama-3.2-1B: unchanged from update: pipeline penalty and DRY requests in the engine's B=1 client #2256Closes #2255