feat(SPEC-DFLASH2): W7 — async scheduling survives an Eagle-type speculator, drafts riding in the output (#1824) - #1835
Merged
Conversation
…ive family (#1824) The engine forces synchronous scheduling whenever a speculator is configured, a deferral SPEC-MTP I5d recorded (mtp-spec-decode.md §2.8's not-ported list names the async draft-in-output path). Upstream keeps async scheduling ON for Eagle-type methods — dflash and every MTP type included — and at c1 spec decode that difference serializes every host-side scheduling cost into each of ~360 steps, the largest named host-side divergence in the #1574 gap. This commits the W7 spec and the issue-index row BEFORE any implementation, so commit order proves the spec came first. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
…ulator, drafts riding in the output (#1824) The engine forced synchronous scheduling under ANY configured speculator, a SPEC-MTP I5d deferral. Upstream keeps async scheduling ON for the Eagle-type family — dflash (which DFlash2 rides) and every MTP type — and at c1 spec decode the forced sync arm serializes every host-side scheduling cost into each of ~360 steps, the largest named host-side divergence in the #1574 gap. This ports the draft-in-output flow at the pin's polarity: the AsyncScheduler ships -1 placeholder drafts the worker fills from its own propose, update_draft_token_ids_in_output rewrites real values into a deferred (structured-output) batch, post_step skips the out-of-band pull under async, the rejection rollback gains the async_tokens_to_discard guard, and preemption clears spec_token_ids. The enable flip routes through SpeculativeConfig::async_scheduling_compatible, refusing host ngram and draft_model exactly as the pin does. Two recorded adaptations: the worker fill is host-side and the computed-token correction is exact-structural, both because our propose and rejection are host-synchronous where upstream's are device-resident (spec D3/D4). The GPU TPOT A/B stays owed (spec A1); no speed number is claimed. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
Brings the branch up to df1ee20 so the trailer and style gates run over a real range and the PR merges clean. The union merge of .agents/issue-index.md kept all 678 main-side lines plus this row's appended #1824 entry (verified line-by-line, 679 total); main's only overlap with this wave's files is an unrelated comment block in model_loader.h's vision-tower region. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
… it does not (#1824) Three record corrections on the W7 spec, found by the fresh review of #1835 and fixed before merge; no production code or test changes. M4 claimed the engine-level stub test reds on values. The reviewer measured that test_engine_core_proc's W7 case stays GREEN under M4, because the stub performs its own fill and never sees the production runner's fill. The guarantee is still gated: the LoadedEngine identity gate reds loudly ("vt: embedding: id out of range", a -1 reaching the embed). The entry now names the identity gate as the discriminator and states that the stub test does not red. Scope item 4 said EngineCore gains an async_scheduling constructor flag. The landed design added a Scheduler::async_scheduling() virtual instead, and EngineCore gained no flag; the sentence now matches the PR body. A new Owed bullet (A4) records that async_tokens_to_discard has no production producer in this tree — upstream sets it in the reset-prefix-cache force-preempt path, which is not ported — so the W7 guard's false branch is production-unreachable and a boundary mutation there (== 0 to <= 1) survives every suite. The porter of that path owns the gate. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
Brings the branch up to origin/main 4f0d44c so the diff-scoped commit-trailer and commit-style gates execute against the current base instead of skipping. The .agents/issue-index.md union merge kept every tail row from both sides (678 base + 1 ours + 3 theirs = 682, each present exactly once). FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
localai-bot
pushed a commit
that referenced
this pull request
Aug 24, 2026
Main moved under the branch while the gates ran (W7 #1835 among the landed work); the merge is clean, the union-appended issue-index rows and the W7/W8 spec entries all survived, and the full gate was rerun green on the merged tree before this push. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
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.
The engine forced synchronous scheduling whenever a speculator was configured
(
model_loader.cppcarried!resolved_spec_config_.has_value() &&on theasync resolution), a deferral SPEC-MTP I5d recorded in
.agents/specs/mtp-spec-decode.md§2.8. Upstream keeps async scheduling ON forthe Eagle-type family —
"dflash"(which DFlash2 rides) and every MTP typeincluded (
vllm/config/vllm.py:1064-1112,speculative.py:60-65@555967922) — and at c1 spec decode (~360 steps for 2048 tokens) the forcedsync arm serializes every host-side scheduling cost into each step: the largest
named host-side divergence in the #1574 speed gap.
This wave (
SPEC-DFLASH2W7, spec.agents/specs/spec-decode-async-scheduling.md,committed first) ports the draft-in-output flow and flips the enable to
upstream's method predicate:
SpeculativeConfig::async_scheduling_compatible()mirrors the pin's familycheck:
mtp/dflash/dspark/eagle/eagle3stay async; hostngramanddraft_model(refused at the pin) force sync.AsyncSchedulertakes theSpeculativeConfigand assignsspec_token_ids = [-1] * num_spec_tokens_to_scheduleafter each schedule(
async_scheduler.py:24-25,43-45);SchedulerOutputgainsnum_spec_tokens_to_scheduleandnum_invalid_spec_tokens.Scheduler::update_draft_token_ids_in_output(scheduler.py:2072-2107)trims the worker's drafts to the scheduled count, pads a short row with
-1,and records the invalid tail;
EngineCore::step_with_batch_queue'sdeferred-grammar branch calls it (
core.py:718-731).EngineCore::post_stepskips the out-of-band pull under async scheduling(
core.py:617), read off a newScheduler::async_scheduling()virtual — theresolution product IS the scheduler class.
(
async_tokens_to_discard,scheduler.py:1670-1675), and preemption clearsspec_token_ids(:1217-1218).the spec (§Design D3/D4): the placeholder fill is host-side (our propose
materializes drafts on the host, so the runner patches a local copy of the
scheduled map from its own
pending_drafts_), and the computed-tokencorrection is exact-structural (
num_tokens_no_spec - 1) where upstream's isoptimistic-with-GPU-correction (
gpu_model_runner.py:1356-1396) — ourrejection result is host-visible in time, upstream's is not.
runner_supports_async()is split fromasync_input_combine_: asyncSCHEDULING capability no longer carries the spec veto, while the
non-draft-aware device input-combine keeps it (I5e). Spec steps keep the
synchronous host sampler.
Red first, all captured before the implementation:
test_mtp_depth"W7 (Speculators force synchronous scheduling; upstream keeps async ON for Eagle-type methods (dflash included) #1824)" cases failed behaviorally on the pre-W7 tree:CHECK(eng.async_scheduling_enabled())readfalsewith an MTP speculator,max_concurrent_batchesread1 == 2(2 cases / 3 assertions red).surface:
Schedulerhas no memberupdate_draft_token_ids_in_output,SchedulerOutputhas no membernum_spec_tokens_to_schedule/num_invalid_spec_tokens, noAsyncSchedulerctor taking aSpeculativeConfig.Green, CPU tier:
test_mtp_depth10/10 (123): a spec engine resolves async ON, ngram stayssync, and sync/async emit IDENTICAL tokens through BOTH production fronts
(depth-1
LLMEngine::stepandAsyncLLM's depth-2step_with_batch_queue)on the synthetic GDN-hybrid model with a real MTP head, with the depth
witnesses asserted in every arm.
test_engine_core_proc15/15 (121): the placeholder / worker-fill contractat the
ModelRunnerBaseseam — under async the engine never pullsout-of-band (
take_calls == 0), the scheduler ships-1placeholders, andthe stub's fill emits the sync flow's exact token streams.
test_async_scheduler9/9 (86) incl. the portedtest_no_placeholder_underflow_on_discarded_spec_frameand a steady-stateplaceholder/rollback arithmetic case over acceptance 0/1/k.
test_scheduler38/38 (459) incl.update_draft_token_ids_in_outputtrim/pad/skip/replace semantics.
test_speculative_mtp_depth5/5 (27) incl. the method truth table.test_cpu_threadpool, is the known parallel-ctest load flake and passesserially (9/9, 19602 assertions).
scripts/agent-preflight.shgreen exceptthe known-flaky
test_cpu_x86_llamacpp_floor(test_cpu_x86_llamacpp_floor: the contended-leg case is load-dependent — at loadavg 63 the harness exits NO_QUIET_WINDOW (4) instead of GIVING_UP (2), so the guarantee goes untested and reads as a failure of whatever diff is in flight #618, the recordedexception).
Mutations (each applied, focused suite red, restored):
!resolved_spec_config_.has_value() &&form —test_mtp_depthred, 2cases / 3 assertions (the production call site IS the flip).
num_output_placeholders -= num_rejected—test_async_schedulerred, 5 assertions (placeholder budget drifts).
async_tokens_to_discard == 0guard — the ported underflowcase red (
num_output_placeholdersread-4 == 1).-1map verbatim) —test_mtp_depthred, 5 cases (every spec engine arm).test_mtp_depthred atexactly the depth-2 arm's token identity (
:876), the arm the correctionexists for; depth-1 stays green because its rollback lands before the next
schedule.
update_draft_token_ids_in_output's trim —test_schedulerred,5 assertions.
test_async_schedulerred (3 assertions) ANDtest_engine_core_procred(2 assertions).
Every mutation compiled clean, was run, and the tree was restored and
verified byte-for-byte against pre-mutation sha256 sums before the next.
Owed, recorded in the spec's
## Owed:VT_ASYNC_SCHED=0, same binary, Qwen3.8-27B three-way at each engine's best: one checkpoint ours/vLLM/SGLang all serve, and three of the four recorded NVFP4 blockers are properties of the unsloth artifact rather than the format #1574workload) — operator-run under an
rclease; no speed number is claimedhere.
steps: device rejection sampling, device-resident drafts, the draft-aware
combine, the optimistic correction).
validate_tokensarm,deferred with the multi-row bitmask).
Closes #1824.
FOLLOWING_AGENTS_PROTOCOL
Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]