Skip to content

perf(speculative): prompt lookup slows prose decoding 3-21% on GB10 #2091

Description

@inureyes

Problem / Background

Prompt lookup (mlxcel generate --prompt-lookup, PR #2074) speeds up replies that copy their prompt but still costs throughput on replies with little to copy. The PR measured break-even (1.01x) on free writing on an M4 Pro; on GB10 the same kind of reply runs below plain decoding.

Measured 2026-10-02 on GB10 (CUDA), release build of the PR #2074 head after its hardening commit (a1fa9e7); greedy, whole-call tok/s including prefill, median of 3 runs, run-to-run spread mostly under 2%. Speedup over plain decoding:

Model edit_fn edit_json summary write story (800 tok)
Qwen3-1.7B-4bit 1.10x 1.37x 0.79x 1.06x 0.84x
Qwen3-4B-4bit 1.21x 1.55x 1.32x 0.93x 0.87x
Qwen3-8B-4bit 1.36x 1.81x 1.50x 0.97x 0.90x
Llama-3.1-8B-Instruct-4bit 1.51x 1.94x 0.92x 0.96x 0.92x

Current Behavior

The losing rows share few drafted rounds, almost all rejected (story: acceptance 0.07 to 0.18, tokens per forward 1.02 to 1.06; 35 to 73 drafted rounds of 371 to 748). Each drafted round pays three costs in PromptLookupGenerator::generate (src/lib/mlxcel-core/src/speculative/prompt_lookup.rs):

  • A verify block wider than one token, run synchronously: the host reads the argmax before building the next round (prompt_lookup.rs:761-831).
  • A broken pipeline: when a proposal appears while a plain step is in flight, the loop submits nothing new, reads the in-flight token synchronously, emits it, and looks the proposal up again next round (prompt_lookup.rs:724-746).
  • A slow restart: plain_streak resets to 0, and pipelining resumes only once it exceeds PLAIN_ROUNDS_BEFORE_PIPELINE (2), so two synchronous plain rounds follow every drafted round (prompt_lookup.rs:254, prompt_lookup.rs:747-759).

On GB10 a forced-synchronous plain step costs 17% on Qwen3-1.7B (MLXCEL_FORCE_SYNC=1: 180 to 150 tok/s), and the 1.7B story works out to about 2.4 plain steps lost per drafted round. The DraftGovernor constants (MISSES_BEFORE_COOLDOWN 3, cooldown MIN_COOLDOWN 4 doubling to MAX_COOLDOWN 32, budget 2 * ema + 2; prompt_lookup.rs:240-322) and the pipelining threshold were chosen from a 36-point grid on an M4 Pro, where verify and sync are relatively cheaper.

Proposed Solution

Candidate directions, each to be measured on both Metal and CUDA before any default changes:

  • Keep the pipeline running when a proposal appears while a step is in flight: verify from the in-flight token instead of draining it.
  • Require a minimum proposal length or recent acceptance before breaking the pipeline.
  • Backend-specific governor thresholds derived from a measured verify-to-step cost ratio.

Decision criterion: ship the direction (or combination) that meets every acceptance criterion below on both backends; if more than one does, prefer the smaller change to the round loop.

Scope

In scope: the decode round loop and DraftGovernor in src/lib/mlxcel-core/src/speculative/prompt_lookup.rs, its tests in prompt_lookup_tests.rs, and the benchmark harness and results under docs/benchmark_results/.

Out of scope: first-use kernel costs per verify width, which made short replies much worse (0.65x) and were fixed in PR #2074 by warming every verify width before the timed run (warm_up_verify_widths, prompt_lookup.rs:544). Server-side prompt lookup is also out of scope.

Implementation Notes

  • Reuse: the existing in_flight / async_eval pipelining in the same loop and the PromptLookupStats counters; keep MLXCEL_FORCE_SYNC as the kill switch (prompt_lookup.rs:696).
  • Constraints: cache trim accounting (trim_caches with draft.len() - accepted) must stay exact if a verify starts from an in-flight token; samplers that need the emitted history (needs_history) stay synchronous.
  • Edge cases: a proposal on the last allowed token (remaining <= 1), EOS read from the in-flight token, and --prompt-lookup-no-adaptive.

Acceptance Criteria

  • On GB10, the write and story rows above reach at least 0.98x of plain decoding for all four models, with the edit and summary gains preserved within 5%.
  • The same matrix on an Apple Silicon machine shows no regression against PR feat(speculative): add prompt-lookup decoding to mlxcel generate #2074's numbers.
  • Greedy output stays byte-identical to the pre-change prompt-lookup output wherever it was identical to plain decoding.
  • Unit tests in prompt_lookup_tests.rs cover any changed governor or pipelining rule.
  • Measured once at the end on a quiet machine; the harness and raw numbers committed under docs/benchmark_results/.

Verification

make release-cuda          # GB10; `make release` on Apple Silicon
make test-fast-cuda FILTER="-p mlxcel-core --lib prompt_lookup"
./target/release/mlxcel generate --model <model> --temp 0 -n 400 -p "<prompt>"
./target/release/mlxcel generate --model <model> --temp 0 -n 400 -p "<prompt>" --prompt-lookup

Run the five prompts on all four models, median of 3, and diff the greedy outputs against the pre-change prompt-lookup outputs.

Refs #2074

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions