Skip to content

spec: build every draft worker from a draft ServerArgs copy - #33335

Merged
ch-wan merged 1 commit into
mainfrom
cheng/gc-wb-2-draft-copy
Aug 3, 2026
Merged

ch-wan merged 1 commit into
mainfrom
cheng/gc-wb-2-draft-copy

Conversation

@ch-wan

@ch-wan ch-wan commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Part 2 of five, all based on main and meant to merge in order.
Applies on top of part 1. Until part 1 lands, this PR's diff includes it.

Replaces #33239, which was closed unmerged: GitHub treated the previous
chained-base series as a stack, which blocks both base retargeting and
every merge path except the async endpoint. The review discussion and the
triage of each round of comments is on #33239; the code here is identical to
that PR's final revision.

The problem

EAGLEWorkerV2, StandaloneWorkerV2, MultiLayerEagleWorkerV2 and
FrozenKVMTPWorkerV2 wrote the draft's context_length onto the ServerArgs
instance they share with the target worker, and the scheduler wrote the draft's
load_format onto that same object just before constructing them:

# EAGLEWorkerV2.__init__ — server_args is the scheduler's instance
server_args.override(
    "spec_worker.match_target_context_length",
    context_length=target_worker.model_runner.model_config.context_len,
)
self._draft_worker = EagleDraftWorker(server_args, ...)

Neither the worker nor EagleDraftWorker copies, so the draft's TpModelWorker
reads the values from the shared object — which then carries draft values for the
rest of the process. dflash and dspark never had this: build_draft_tp_worker
deepcopies first.

The change

draft_server_args_copy(server_args, target_model_config) gives the four workers
the same treatment: deepcopy, then apply the per-draft values through the audited
mutation point. ModelConfig.from_server_args and build_load_config both read the
instance they are handed, so the values land exactly where they did before, while
the target keeps what the launcher resolved.

load_format moves into both draft-copy paths. build_draft_tp_worker's override
set needs it too: the copy those workers make used to inherit the scheduler's write,
so removing that write without this would silently drop
--speculative-draft-load-format for dflash and dspark.

The EAGLE hot-token-map write is deleted, not moved

init_token_map set json_model_override_args to {"hot_vocab_size": N}, but it
runs from alloc_memory_pool — long after EagleDraftWorker.__init__ built the
draft's TpModelWorker and with it the ModelConfig. hot_vocab_size is only ever
read off model_config.hf_config (llama_eagle.py, the two draft-extend graph
runners), and json_model_override_args reaches hf_config only at ModelConfig
construction. The write therefore could not affect the draft model; only the shared
instance saw it. hot_token_id is unchanged, so a draft checkpoint that declares
hot_vocab_size in its own config behaves exactly as before.

If the intent was to trim the draft vocabulary at load time, that needs the token map
resolved before the draft model is built — a behavioural change that deserves its
own PR and an end-to-end run with a token-map checkpoint.

Validation

  • test_draft_server_args_copy.py: the copy carries context_length /
    load_format, the target instance is untouched, and the build_draft_tp_worker
    override set carries the draft load format.
  • test_spec_worker_draft_isolation.py: the scheduler handoff — the factory and the
    worker both receive the copy, the copy is the published config while the draft is
    built, and the target's config is back in the slot afterwards. (It stubs the
    worker factory rather than constructing the four workers; the copy is made once,
    before create_worker, so that is where the contract lives.)
  • test/registered/unit/{spec,model_executor} and the config ratchets pass; full
    registered CPU battery shows no new failures against the base commit.
  • Not covered locally: no speculative-decoding checkpoint is available on the
    machine this was written on, so no EAGLE/MTP end-to-end run was made. CI's
    speculative suites are the gate — please look at them before merging.

Writer ratchet 31 → 26.


CI States

Latest PR Test (Base): 🚫 Run #30784258435
Latest PR Test (Extra): ❌ Run #30784258312

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Aug 3, 2026
EAGLEWorkerV2, StandaloneWorkerV2, MultiLayerEagleWorkerV2 and
FrozenKVMTPWorkerV2 wrote the draft's context_length onto the ServerArgs
instance they share with the target worker, and the scheduler wrote the draft's
load_format onto that same object just before creating them. The target's config
carried draft values from then on, and anything constructed later in the process
inherited them.

Scheduler.maybe_init_draft_worker now makes one draft copy through
draft_server_args_copy() and hands it to both the worker factory and the worker,
so every algorithm gets it — the four built-ins, dflash/dspark (which deepcopy
it again inside build_draft_tp_worker), and anything registered through
SpeculativeAlgorithm.register. The copy starts from the config the process
resolved, not from the pristine seed, so load-time overrides made before this
point (the chunked-prefix gate, the SM100 GDN prefill default) are part of what
the draft sees; context_length and load_format are applied on top.

The construction runs under a preserved publish of that copy, the shape
build_draft_tp_worker already used. Weight loading reads the bags rather than the
instance it was handed — Inkling's ModelOpt scale normalization keys on
load_format — so the draft has to be built with its own config published, and
the target's is back in the slot when construction returns.

The EAGLE hot-token-map write is deleted, not moved. init_token_map runs from
alloc_memory_pool, long after the draft's TpModelWorker built its ModelConfig,
and hot_vocab_size is only ever read off model_config.hf_config, which
json_model_override_args reaches at ModelConfig construction. The write could not
affect the draft model; only the shared instance saw it. hot_token_id is
unchanged, so a draft checkpoint that declares hot_vocab_size behaves as before.

Tests: draft_server_args_copy carries the target context_length, a configured
draft load_format and any load-time override while leaving the target's instance
alone; and the scheduler handoff pins that the factory and the worker both
receive the copy, that the copy is the published config during construction, and
that the target's is restored afterwards.

Writer ratchet 31 -> 26.
@ch-wan
ch-wan force-pushed the cheng/gc-wb-2-draft-copy branch from 9b99592 to e70fd46 Compare August 3, 2026 04:22
@ch-wan
ch-wan merged commit 9bc8848 into main Aug 3, 2026
25 of 32 checks passed
@ch-wan
ch-wan deleted the cheng/gc-wb-2-draft-copy branch August 3, 2026 04:22
kfhfar pushed a commit to kfhfar/sglang that referenced this pull request Aug 7, 2026
…ect#33335)

EAGLEWorkerV2, StandaloneWorkerV2, MultiLayerEagleWorkerV2 and
FrozenKVMTPWorkerV2 wrote the draft's context_length onto the ServerArgs
instance they share with the target worker, and the scheduler wrote the draft's
load_format onto that same object just before creating them. The target's config
carried draft values from then on, and anything constructed later in the process
inherited them.

Scheduler.maybe_init_draft_worker now makes one draft copy through
draft_server_args_copy() and hands it to both the worker factory and the worker,
so every algorithm gets it — the four built-ins, dflash/dspark (which deepcopy
it again inside build_draft_tp_worker), and anything registered through
SpeculativeAlgorithm.register. The copy starts from the config the process
resolved, not from the pristine seed, so load-time overrides made before this
point (the chunked-prefix gate, the SM100 GDN prefill default) are part of what
the draft sees; context_length and load_format are applied on top.

The construction runs under a preserved publish of that copy, the shape
build_draft_tp_worker already used. Weight loading reads the bags rather than the
instance it was handed — Inkling's ModelOpt scale normalization keys on
load_format — so the draft has to be built with its own config published, and
the target's is back in the slot when construction returns.

The EAGLE hot-token-map write is deleted, not moved. init_token_map runs from
alloc_memory_pool, long after the draft's TpModelWorker built its ModelConfig,
and hot_vocab_size is only ever read off model_config.hf_config, which
json_model_override_args reaches at ModelConfig construction. The write could not
affect the draft model; only the shared instance saw it. hot_token_id is
unchanged, so a draft checkpoint that declares hot_vocab_size behaves as before.

Tests: draft_server_args_copy carries the target context_length, a configured
draft load_format and any load-time override while leaving the target's instance
alone; and the scheduler handoff pins that the factory and the worker both
receive the copy, that the copy is the published config during construction, and
that the target's is restored afterwards.

Writer ratchet 31 -> 26.
saturn-acc pushed a commit to saturn-acc/sglang that referenced this pull request Aug 16, 2026
…ect#33335)

EAGLEWorkerV2, StandaloneWorkerV2, MultiLayerEagleWorkerV2 and
FrozenKVMTPWorkerV2 wrote the draft's context_length onto the ServerArgs
instance they share with the target worker, and the scheduler wrote the draft's
load_format onto that same object just before creating them. The target's config
carried draft values from then on, and anything constructed later in the process
inherited them.

Scheduler.maybe_init_draft_worker now makes one draft copy through
draft_server_args_copy() and hands it to both the worker factory and the worker,
so every algorithm gets it — the four built-ins, dflash/dspark (which deepcopy
it again inside build_draft_tp_worker), and anything registered through
SpeculativeAlgorithm.register. The copy starts from the config the process
resolved, not from the pristine seed, so load-time overrides made before this
point (the chunked-prefix gate, the SM100 GDN prefill default) are part of what
the draft sees; context_length and load_format are applied on top.

The construction runs under a preserved publish of that copy, the shape
build_draft_tp_worker already used. Weight loading reads the bags rather than the
instance it was handed — Inkling's ModelOpt scale normalization keys on
load_format — so the draft has to be built with its own config published, and
the target's is back in the slot when construction returns.

The EAGLE hot-token-map write is deleted, not moved. init_token_map runs from
alloc_memory_pool, long after the draft's TpModelWorker built its ModelConfig,
and hot_vocab_size is only ever read off model_config.hf_config, which
json_model_override_args reaches at ModelConfig construction. The write could not
affect the draft model; only the shared instance saw it. hot_token_id is
unchanged, so a draft checkpoint that declares hot_vocab_size behaves as before.

Tests: draft_server_args_copy carries the target context_length, a configured
draft load_format and any load-time override while leaving the target's instance
alone; and the scheduler handoff pins that the factory and the worker both
receive the copy, that the copy is the published config during construction, and
that the target's is restored afterwards.

Writer ratchet 31 -> 26.
jakki-amd pushed a commit to jakki-amd/sglang that referenced this pull request Sep 9, 2026
…ect#33335)

EAGLEWorkerV2, StandaloneWorkerV2, MultiLayerEagleWorkerV2 and
FrozenKVMTPWorkerV2 wrote the draft's context_length onto the ServerArgs
instance they share with the target worker, and the scheduler wrote the draft's
load_format onto that same object just before creating them. The target's config
carried draft values from then on, and anything constructed later in the process
inherited them.

Scheduler.maybe_init_draft_worker now makes one draft copy through
draft_server_args_copy() and hands it to both the worker factory and the worker,
so every algorithm gets it — the four built-ins, dflash/dspark (which deepcopy
it again inside build_draft_tp_worker), and anything registered through
SpeculativeAlgorithm.register. The copy starts from the config the process
resolved, not from the pristine seed, so load-time overrides made before this
point (the chunked-prefix gate, the SM100 GDN prefill default) are part of what
the draft sees; context_length and load_format are applied on top.

The construction runs under a preserved publish of that copy, the shape
build_draft_tp_worker already used. Weight loading reads the bags rather than the
instance it was handed — Inkling's ModelOpt scale normalization keys on
load_format — so the draft has to be built with its own config published, and
the target's is back in the slot when construction returns.

The EAGLE hot-token-map write is deleted, not moved. init_token_map runs from
alloc_memory_pool, long after the draft's TpModelWorker built its ModelConfig,
and hot_vocab_size is only ever read off model_config.hf_config, which
json_model_override_args reaches at ModelConfig construction. The write could not
affect the draft model; only the shared instance saw it. hot_token_id is
unchanged, so a draft checkpoint that declares hot_vocab_size behaves as before.

Tests: draft_server_args_copy carries the target context_length, a configured
draft load_format and any load-time override while leaving the target's instance
alone; and the scheduler handoff pins that the factory and the worker both
receive the copy, that the copy is the published config during construction, and
that the target's is restored afterwards.

Writer ratchet 31 -> 26.
Atituiset pushed a commit to Atituiset/sglang that referenced this pull request Sep 10, 2026
…ect#33335)

EAGLEWorkerV2, StandaloneWorkerV2, MultiLayerEagleWorkerV2 and
FrozenKVMTPWorkerV2 wrote the draft's context_length onto the ServerArgs
instance they share with the target worker, and the scheduler wrote the draft's
load_format onto that same object just before creating them. The target's config
carried draft values from then on, and anything constructed later in the process
inherited them.

Scheduler.maybe_init_draft_worker now makes one draft copy through
draft_server_args_copy() and hands it to both the worker factory and the worker,
so every algorithm gets it — the four built-ins, dflash/dspark (which deepcopy
it again inside build_draft_tp_worker), and anything registered through
SpeculativeAlgorithm.register. The copy starts from the config the process
resolved, not from the pristine seed, so load-time overrides made before this
point (the chunked-prefix gate, the SM100 GDN prefill default) are part of what
the draft sees; context_length and load_format are applied on top.

The construction runs under a preserved publish of that copy, the shape
build_draft_tp_worker already used. Weight loading reads the bags rather than the
instance it was handed — Inkling's ModelOpt scale normalization keys on
load_format — so the draft has to be built with its own config published, and
the target's is back in the slot when construction returns.

The EAGLE hot-token-map write is deleted, not moved. init_token_map runs from
alloc_memory_pool, long after the draft's TpModelWorker built its ModelConfig,
and hot_vocab_size is only ever read off model_config.hf_config, which
json_model_override_args reaches at ModelConfig construction. The write could not
affect the draft model; only the shared instance saw it. hot_token_id is
unchanged, so a draft checkpoint that declares hot_vocab_size behaves as before.

Tests: draft_server_args_copy carries the target context_length, a configured
draft load_format and any load-time override while leaving the target's instance
alone; and the scheduler handoff pins that the factory and the worker both
receive the copy, that the copy is the published config during construction, and
that the target's is restored afterwards.

Writer ratchet 31 -> 26.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant