Symptom
vllm:spec_decode_num_drafts_total / num_draft_tokens_total / num_accepted_tokens_total stay at 0 in production even when DFlash/DSpark speculative decoding is actively running (visible per step in the debug log as Qwen3 DFlash request=... accepted_draft=N).
Root cause
#787 wires SpecDecodeCounters from Qwen3Executor::execute_speculative_verify_impl into LoadSnapshot.spec_decode, and the legacy handle bridge forwards per-interval deltas via publish_scheduler_stats (pegainfer-frontend/src/vllm/bridge.rs). But every served model line launches as LaunchedEngine::Stepped (e.g. qwen3 at pegainfer-qwen3/src/model_line.rs), and SteppedEngineBridge:
- passes
load_watch = None, so publish_scheduler_stats never spawns, and
- stamps stats with a bare
scheduler_stats_from(load) per output batch (bridge/stepped.rs, two sites), where ..SchedulerStats::default() leaves spec_decoding_stats = None forever.
The sim frontend e2e covers only the handle path, so the gap has no test net.
Why it matters
Upstream vllm-bench (now maintained in vllm-project/vllm at rust/src/bench) scrapes the server's /metrics before and after a run and auto-reports a Speculative Decoding section (acceptance rate/length, per-position rates) plus spec_decode_* JSON fields — no flags needed. With the counters stuck at 0 that whole surface is unusable for spec-decode A/B work against pegainfer.
Suggested fix
Give SteppedEngineBridge its own last_spec: SpecDecodeCounters state and diff the cumulative snapshot with the same spec_decode_delta the legacy publisher uses, attaching spec_decoding_stats only on intervals that drafted (delta.num_drafts > 0). Minimal sketch:
// in SteppedEngineBridge, one fn used by both stats-stamping sites
let snapshot = self.scheduler.load();
let mut stats = scheduler_stats_from(&snapshot);
if let Some(cur) = &snapshot.spec_decode {
let delta = spec_decode_delta(&self.last_spec, cur);
self.last_spec = *cur;
stats.spec_decoding_stats = (delta.num_drafts > 0).then_some(delta);
}
Local verification of the patch
Qwen3-4B + dspark_qwen3_4b_block7, vllm-bench on openai-chat, c4, 20 prompts each:
| dataset |
acceptance rate |
acceptance length |
| random |
14.2% |
1.97 |
| sonnet |
28.9% |
2.99 |
| sharegpt |
20.5% |
2.40 |
All in the documented DFlash/DSpark ranges (see docs/models/qwen3/dspark-integration.md); result JSON carries all spec_decode_* fields.
Follow-up edge case (from review)
With the state held in the bridge, a step whose request updates are all dropped (e.g. an abort arriving while a speculative step is in flight — dispatch_step returns at outputs.is_empty()) never stamps stats, so the last cumulative increment could stay unpublished while the engine idles. The final version should also emit/refresh stats for steps with no surviving request output, or snapshot on every received step.
Symptom
vllm:spec_decode_num_drafts_total/num_draft_tokens_total/num_accepted_tokens_totalstay at 0 in production even when DFlash/DSpark speculative decoding is actively running (visible per step in the debug log asQwen3 DFlash request=... accepted_draft=N).Root cause
#787 wires
SpecDecodeCountersfromQwen3Executor::execute_speculative_verify_implintoLoadSnapshot.spec_decode, and the legacy handle bridge forwards per-interval deltas viapublish_scheduler_stats(pegainfer-frontend/src/vllm/bridge.rs). But every served model line launches asLaunchedEngine::Stepped(e.g. qwen3 atpegainfer-qwen3/src/model_line.rs), andSteppedEngineBridge:load_watch = None, sopublish_scheduler_statsnever spawns, andscheduler_stats_from(load)per output batch (bridge/stepped.rs, two sites), where..SchedulerStats::default()leavesspec_decoding_stats = Noneforever.The sim frontend e2e covers only the handle path, so the gap has no test net.
Why it matters
Upstream
vllm-bench(now maintained invllm-project/vllmatrust/src/bench) scrapes the server's/metricsbefore and after a run and auto-reports a Speculative Decoding section (acceptance rate/length, per-position rates) plusspec_decode_*JSON fields — no flags needed. With the counters stuck at 0 that whole surface is unusable for spec-decode A/B work against pegainfer.Suggested fix
Give
SteppedEngineBridgeits ownlast_spec: SpecDecodeCountersstate and diff the cumulative snapshot with the samespec_decode_deltathe legacy publisher uses, attachingspec_decoding_statsonly on intervals that drafted (delta.num_drafts > 0). Minimal sketch:Local verification of the patch
Qwen3-4B +
dspark_qwen3_4b_block7, vllm-bench onopenai-chat, c4, 20 prompts each:All in the documented DFlash/DSpark ranges (see
docs/models/qwen3/dspark-integration.md); result JSON carries allspec_decode_*fields.Follow-up edge case (from review)
With the state held in the bridge, a step whose request updates are all dropped (e.g. an abort arriving while a speculative step is in flight —
dispatch_stepreturns atoutputs.is_empty()) never stamps stats, so the last cumulative increment could stay unpublished while the engine idles. The final version should also emit/refresh stats for steps with no surviving request output, or snapshot on every received step.