Skip to content

feat: add lag confidence to decouple adaptive speculative pruning from critical path. - #2280

Open
weizhehuang0827 wants to merge 2 commits into
xLLM-AI:mainfrom
weizhehuang0827:feat/adaptive-lag-confidence
Open

feat: add lag confidence to decouple adaptive speculative pruning from critical path.#2280
weizhehuang0827 wants to merge 2 commits into
xLLM-AI:mainfrom
weizhehuang0827:feat/adaptive-lag-confidence

Conversation

@weizhehuang0827

Copy link
Copy Markdown
Collaborator

Summary

The adaptive speculative controller (DFlash/DSpark) prunes per-sequence how many draft tokens to validate, based on per-step confidence. Today that decision sits on the serial decode critical path: run_validate reads this step's confidence — produced by the draft forward that just finished — does a blocking D2H, and only then can the target forward proceed.

This PR adds enable_lag_confidence (default false, zero regression). When on, the controller prunes from the previous step's confidence (lag-1), read from the embedding cache. The decision no longer data-depends on this step's draft, so it runs in step_decode while the async draft forward is still in flight — leaving the critical path. This aligns with SGLang's independently-gated ConfidenceRelay channel (needs_confidence_relay), which is a separate relay from the always-on token future map.

What changed

  • EmbeddingCache: DecodeState.confidence stores this step's per-draft confidence per slot at validate end — one D2H past the validate sync, off the critical path. read_lagged_confidence reads it next step, reusing the existing request_id freshness gate (slot reuse / first step -> fall back to full width, no prune).
  • DFlashWorkerImpl: split compute_prefix_lengths_from_probs out of compute_adaptive_prefix_lengths (linear cost model reused verbatim); add decide_lagged_prefix_lengths in step_decode; run_validate consumes the precomputed lagged decision when the flag is on. One site covers both DFlash and DSpark. Signal source mirrors the existing controller: ConfidenceHead output (DSpark) or proposal probs (DFlash).
  • New flag plumbed through speculative_config -> common Options -> runtime Options.

Measurements (Qwen3 DSpark, single die, ShareGPT, conc=32)

Overlap — controller decision cost still on the critical path (inside run_validate), measured with a temporary probe (not shipped):

SL this-step lagged
7 63.85 us/call 0.31 us/call
16 77.62 us/call 0.14 us/call

The ~52 us of real decision work relocates into step_decode, overlapping the in-flight draft.

Throughput / acceptance (32/32 completed, coherent, no crashes):

SL arm out tok/s acc_rate
7 static 613 0.576
7 this-step adaptive 985 0.803
7 lagged 1109 0.780
16 static 349 0.239
16 this-step adaptive 893 0.803
16 lagged 766 0.709

SL7 lagged is a net win (+12.6% over this-step adaptive, acceptance barely moves). SL16 lagged regresses vs this-step (stale confidence prunes less aggressively at the larger block) but still far beats static and stays coherent.

Test plan

  • Builds clean in the CANN container; shipping binary verified free of the temporary probe.
  • DSpark SL7/SL16 x {static, this-step adaptive, lagged}, 32/32 completed, no fatals, coherent output.
  • Overlap probe confirms the decision leaves the critical path (~64-78 us -> ~0.1-0.3 us).
  • Flag off = byte-for-byte current behavior (this-step path unchanged).
  • DFlash lag arm (static/this-step/lagged) not yet benchmarked — DFlash uses proposal probs (weaker signal than a confidence head); worth a separate run to confirm acc_rate/throughput hold.

…m the critical path.

The adaptive speculative controller (DFlash/DSpark) prunes per-sequence how
many draft tokens to validate from per-step confidence. Today that decision
sits on the serial decode critical path: run_validate reads THIS step's
confidence (produced by the draft forward that just finished), does a blocking
D2H, and only then can the target forward proceed.

Add enable_lag_confidence (default false, zero regression): the controller
instead prunes from the PREVIOUS step's confidence (lag-1), read from the
embedding cache. The decision no longer data-depends on this step's draft, so
it runs in step_decode while the async draft forward is still in flight,
leaving the critical path. This aligns with SGLang's independently-gated
ConfidenceRelay channel (needs_confidence_relay).

- EmbeddingCache: store this step's per-draft confidence per slot at validate
  end (one D2H past the validate sync, off the critical path); read it next
  step via read_lagged_confidence with the existing request_id freshness gate
  (slot reuse / first step -> fall back to full width, no prune).
- DFlashWorkerImpl: split compute_prefix_lengths_from_probs out of
  compute_adaptive_prefix_lengths (linear cost model reused verbatim); add
  decide_lagged_prefix_lengths in step_decode; run_validate consumes the
  precomputed lagged decision when the flag is on. One site covers both DFlash
  and DSpark. Signal source mirrors the existing controller: ConfidenceHead
  output (DSpark) or proposal probs (DFlash).

Measured (Qwen3 DSpark, single die): the controller decision on the critical
path drops from ~64-78 us/call to ~0.1-0.3 us/call (the ~52 us of work moves
into step_decode, overlapping the in-flight draft). SL7 lagged throughput
1109 tok/s vs 985 this-step adaptive (+12.6%), acc_rate 0.780 vs 0.803.
@weizhehuang0827 weizhehuang0827 changed the title feat: add lag confidence to decouple adaptive speculative pruning from the critical path feat: add lag confidence to decouple adaptive speculative pruning from critical path. Aug 20, 2026
Under adaptive speculative decode, when the controller prunes, run_validate
rebuilt the target batch as a true varlen [Σ(prefix_i+1)] batch via a host-side
per-seq loop (7 host vectors + heap allocs + H2D copies). That rebuild sat on
the serial decode critical path: ~2.2% of a decode step, growing with the
speculative block size.

Under lag confidence the per-seq prune decision comes from the previous step's
confidence, so it is known BEFORE this step's draft forward and has no data
dependency on it. run_decode_draft already builds a validate batch on the host
in the draft-overlap window, but it built the dense full-width batch that
run_validate then discarded on every prune step. Build the pruned varlen batch
there instead: the whole rebuild now overlaps the in-flight draft and leaves
run_validate's critical path.

- Hoist decide_lagged_prefix_lengths above run_decode_draft in step_decode and
  thread the decision in; the draft launch is async so this costs nothing.
- prepare_overlap_validate_input: shared entry both DFlash and DSpark
  run_decode_draft call; builds the pruned varlen batch (recording the prune
  onto DraftBlock) when the lagged decision prunes, else the dense batch as
  before.
- run_validate consumes the pre-built batch (DraftBlock.varlen_prebuilt) and
  skips its own rebuild; only fill_validate_input_from_draft_outputs_varlen —
  which needs this step's draft tokens — stays on the critical path.
- Factor prefix_lengths_to_val_tokens out of the old apply_per_seq_varlen_prune,
  shared by the overlap path and the legacy this-step fallback. Derive
  effective_prefix from per_seq_val_tokens so it is correct on both paths.

Flag off (default) is byte-for-byte the previous ordering: dense prep in
run_decode_draft, this-step decision + rebuild in run_validate.

Critical-path probe (DSpark, single die): host prune-block cost on run_validate's
path drops SL7 873->285 us/step, SL16 1045->285 us/step — the metadata rebuild
leaves the critical path and the residue (fixed ~285 us, the draft-token fill)
is decoupled from block size. acc_rate unchanged (reorder is behavior-preserving).
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