Skip to content

spec: give the v2 draft workers their own ServerArgs copy - #33239

Closed
ch-wan wants to merge 1 commit into
cheng/gc-wb-1-retire-writesfrom
cheng/gc-wb-2-draft-copy
Closed

ch-wan wants to merge 1 commit into
cheng/gc-wb-1-retire-writesfrom
cheng/gc-wb-2-draft-copy

Conversation

@ch-wan

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

Copy link
Copy Markdown
Collaborator

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: each of the four workers hands its draft a
    distinct ServerArgs and leaves the target's alone.
  • 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 #30771343684
Latest PR Test (Extra): 🚫 Run #30784011979

@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 2, 2026
@ch-wan

ch-wan commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author

Part of the writer-ratchet stack: #33238#33239#33240#33241. Base is the previous PR's branch; review/merge in order.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fc23081700

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +64 to +69
def _draft_load_format_fields() -> dict:
draft_load_format = get_spec().speculative_draft_load_format
if draft_load_format is None:
return {}
logger.info(f"Using draft model load_format: '{draft_load_format}'")
return dict(load_format=draft_load_format)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve draft load-format handling for registered workers

When a plugin-registered speculative algorithm is selected with --speculative-draft-load-format, the scheduler now passes the original target ServerArgs directly to the plugin worker, while this new override is only invoked by private helpers used by the built-in workers. CustomSpecAlgo.create_worker does not apply either helper, so existing plugin workers that read server_args.load_format will silently receive the target format instead of the configured draft format; previously the scheduler applied it before every worker factory. Preserve the universal handoff behavior or provide the custom-worker path with an equivalently overridden copy.

Useful? React with 👍 / 👎.

@@ -1010,10 +1005,8 @@ def __init__(
server_args.speculative_algorithm

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] All four workers rebind the local parameter server_args = draft_server_args_copy(...) after already storing the target instance on self.server_args. Later statements in the same __init__ that still read server_args (e.g. EAGLE's adaptive gate / frozen-KV's adaptive assert) now silently read the draft copy while self.server_args remains the target. Values currently coincide for those fields, so behavior is fine today, but the dual meaning of the name is a footgun for the next edit that assumes one or the other.

Suggestion: Use a distinct name, e.g. draft_server_args = draft_server_args_copy(...), pass that into the draft worker, and keep server_args / self.server_args unambiguously as the target instance. Apply the same pattern in standalone_worker_v2.py, multi_layer_eagle_worker_v2.py, and frozen_kv_mtp_worker_v2.py.

self.assertIsNone(target.context_length)
self.assertEqual(target.load_format, "auto")

def test_the_draft_load_format_applies_only_when_configured(self):

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The regression that motivated moving load_format off the scheduler was "draft load_format lands on the shared ServerArgs." test_the_draft_load_format_applies_only_when_configured asserts the draft receives "dummy" but never asserts the target still has "auto". test_the_target_instance_is_left_alone only seeds load_format="auto" without speculative_draft_load_format, so it would still pass if the helper mutated the target when a draft format is configured. The worker isolation test also only pins context_length.

Suggestion: In the configured branch of test_the_draft_load_format_applies_only_when_configured (or in test_the_target_instance_is_left_alone with both fields seeded), add self.assertEqual(target.load_format, "auto") after the copy. Optionally seed speculative_draft_load_format in the four-worker isolation test and assert the caller's instance keeps its load_format as well.

`build_draft_tp_worker()` get private bags (a preserved publish of the rewritten copy);
drafts constructed directly with `is_draft_worker=True` skip publish and **share the
target's bags** — a draft-side write there poisons the target.
target's bags** — a draft-side write there poisons the target. Their `ServerArgs`

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] The added sentence says v2 draft workers get a private ServerArgs via draft_server_args_copy(). That is true for the four direct-construction workers this PR changes, but dflash/dspark still get their private instance from build_draft_tp_worker's own deepcopy + draft_server_args_overrides, not from draft_server_args_copy(). "Either way" is right; naming only one helper over-generalizes.

Suggestion: Phrase as: private ServerArgs via draft_server_args_copy() (eagle/standalone/multi-layer/frozen-kv) or via build_draft_tp_worker()'s deepcopy (dflash/dspark).

@ch-wan ch-wan removed the run-ci label Aug 2, 2026
@ch-wan
ch-wan force-pushed the cheng/gc-wb-2-draft-copy branch from e544954 to c4fbeb9 Compare August 2, 2026 08:56
@ch-wan

ch-wan commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author

Round 2 — the finding is real and is fixed in c4fbeb91a6.

@chatgpt-codex-connector is right that handing the draft its own instance is not enough: model-level weight loading reads the bags, not the instance it was constructed with (InklingBatchDenseMLP._shared_scales is the concrete case), and the direct-construction workers never published the copy. The scheduler's old instance write happened to make the process-global object carry the draft format, so this regressed with the write removed.

Scheduler.maybe_init_draft_worker now builds the draft under a preserved publication of the copy, the same shape build_draft_tp_worker uses:

with get_context().preserve_config():
    get_context().set_server_args(draft_server_args)
    self.draft_worker = DraftWorkerClass(**draft_worker_kwargs)

so every algorithm — built-in, dflash/dspark (which nest their own publish) and plugin — loads its draft under the draft config, and the target's config is back in the slot afterwards. A test asserts get_model().load_format is "dummy" inside the worker factory and "auto" again after it returns.

@chatgpt-codex-connector

Copy link
Copy Markdown

Summary

Testing

  • python -m py_compile python/sglang/srt/managers/scheduler.py python/sglang/srt/speculative/draft_worker_common.py test/registered/unit/spec/test_spec_worker_draft_isolation.py
  • git diff --check 5abdee25ebf0c21e3e443f2dadf04fd5a5c8845b..HEAD
  • git status --short
  • ⚠️ PYTHONPATH=python python -m pytest -q test/registered/unit/spec/test_draft_server_args_copy.py test/registered/unit/spec/test_spec_worker_draft_isolation.py (test collection is blocked because the environment lacks the required numpy dependency)

View task →

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c4fbeb91a6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +869 to +871
draft_server_args = draft_server_args_copy(
self.server_args, self.tp_worker.model_runner.model_config
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass the draft copy to registered worker factories

When a registered plugin factory inspects server_args.load_format—for example, to select a loader-specific worker—the factory still receives self.server_args in the subsequent create_worker call, even though the constructed worker now receives this draft copy. Previously the scheduler applied --speculative-draft-load-format before invoking the factory, so a target format of auto and draft format of dummy now makes the factory select behavior for auto while its worker is initialized for dummy. Fresh evidence in the current tree is that the worker handoff has been corrected to use draft_server_args, but the factory handoff remains on the target object; pass the same copy to create_worker as well.

Useful? React with 👍 / 👎.

@ch-wan

ch-wan commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author

Re-review (Round 2 — c4fbeb91a6)

Read the author reply and verified on the new head.

Codex / bag-read regression is fixed. maybe_init_draft_worker now matches build_draft_tp_worker:

with get_context().preserve_config():
    get_context().set_server_args(draft_server_args)
    self.draft_worker = DraftWorkerClass(**draft_worker_kwargs)
  • Algorithm selection still uses the target (create_worker(self.server_args) outside the scope).
  • Instance isolation + bag publication during construction are both covered (test_the_draft_config_is_published_while_the_draft_is_built asserts get_model().load_format is "dummy" inside the factory and "auto" after restore).

No open issues from this pass. Speculative e2e CI remains the real gate for EAGLE/MTP, as noted in the PR body.

@ch-wan
ch-wan force-pushed the cheng/gc-wb-2-draft-copy branch from c4fbeb9 to 32be9c0 Compare August 2, 2026 09:04
@ch-wan

ch-wan commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author

Round 3 — one genuinely new finding, fixed in 32be9c01d6.

Factory handoff (@chatgpt-codex-connector): right, and it is the same regression one call earlier. create_worker still received self.server_args while the worker it returns received the copy, so a registered algorithm that picks its worker class from load_format would choose for auto and then initialize for dummy. Before this PR the scheduler's write reached the factory too, because it mutated the shared object first.

create_worker(draft_server_args) now, and a test drives a fake factory and asserts it sees "dummy". Built-in create_worker only reads disable_overlap_schedule / enable_multi_layer_eagle, which are identical in the copy, so nothing else moves.

The other comments in this batch are re-posts of the round-2 items after the force-push; those fixes are on the current tips — preserve_config around the draft build, _MANAGER_OWNED_FIELDS shared by config_value / resolved_config_dict, the list-shaped rid-cleanup fixture, and get_server_args().pp_size back in the DSA indexer.

@chatgpt-codex-connector

Copy link
Copy Markdown

Summary

  • Re-reviewed commit 32be9c01d6 and confirmed the Round 3 factory-handoff regression is fixed. The scheduler now passes draft_server_args to both the registered algorithm’s create_worker factory and the resulting worker constructor. python/sglang/srt/managers/scheduler.pyL868-L885
  • Confirmed draft construction still temporarily publishes the copied configuration under preserve_config(), restoring the target configuration afterward. python/sglang/srt/managers/scheduler.pyL881-L885
  • Confirmed the regression test uses a fake registered factory, records its load_format, and asserts that it receives "dummy". test/registered/unit/spec/test_spec_worker_draft_isolation.pyL97-L118
  • No additional code changes were necessary. The fix is already committed at 32be9c01d6, and the working tree is clean; therefore, no redundant commit or pull request was created.

Testing

  • python -m py_compile python/sglang/srt/managers/scheduler.py python/sglang/srt/speculative/draft_worker_common.py test/registered/unit/spec/test_spec_worker_draft_isolation.py test/registered/unit/spec/test_draft_server_args_copy.py
  • git diff --check 5abdee25ebf0c21e3e443f2dadf04fd5a5c8845b..HEAD
  • git status --short
  • ⚠️ PYTHONPATH=python python -m pytest -q test/registered/unit/spec/test_draft_server_args_copy.py test/registered/unit/spec/test_spec_worker_draft_isolation.py (collection is blocked because the environment lacks the required numpy dependency)

View task →

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 32be9c01d6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +101 to +104
draft_server_args.override(
"draft_worker.copy",
context_length=target_model_config.context_len,
**_draft_load_format_fields(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Carry the resolved chunked-prefix gate into direct drafts

When the target runner dynamically sets disable_chunked_prefix_cache=True because its MLA attention backend does not support the cached-prefix layout (model_runner_components/misc_utils.py:28-37), this copy republishes the pristine ServerArgs value instead of that resolved bag value. Direct EAGLE drafts such as MistralLarge3ForCausalLMEagle construct DeepSeek attention while this stale copy is published, and DeepseekMHAForwardMixin.init_mha_forward permanently captures the false value (forward_mha.py:147-149), so long-prefix requests can later select the unsupported MHA chunked-KV path. draft_server_args_overrides already preserves this resolved gate for DFLASH/DSPARK; apply the same override to this direct-draft copy.

Useful? React with 👍 / 👎.

@ch-wan
ch-wan force-pushed the cheng/gc-wb-2-draft-copy branch from 32be9c0 to 3ab0285 Compare August 2, 2026 09:15
@ch-wan

ch-wan commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author

Round 4 — the finding is real and the root cause is wider than the one field; fixed in 3ab0285c37.

@chatgpt-codex-connector is right that the copy carried the pristine disable_chunked_prefix_cache. The reason matters: the copy is a deepcopy of the pristine instance, and publishing it (added last round) therefore dropped every post-publish override the process had accumulated by then — the chunked-prefix gate is one, and the SM100 GDN prefill default recorded in #33238 is another.

So rather than adding one more field to the list, draft_server_args_copy now starts from the config the process actually resolved: it replays get_context().overrides_log() onto the copy, then applies the draft-specific values on top (context_length, load_format), which keep winning. That is the same view the draft used to get by reading the target's bags.

Two tests: a load-time get_context().override before the copy shows up on the draft and not on the target, and the draft-specific fields still beat a resolved value for the same key.

@chatgpt-codex-connector

Copy link
Copy Markdown

Summary

  • Re-reviewed Round 4 at commit 3ab0285c37d1e1deca6d7dbea8cab25fa7aa2725; the reported root cause is addressed.
  • draft_server_args_copy() now deep-copies the pristine target instance, replays all post-publication overrides in order, and then applies draft-specific context_length and load_format values so those values retain precedence. python/sglang/srt/speculative/draft_worker_common.pyL93-L117
  • The scheduler passes that resolved copy to both the speculative worker factory and worker constructor, while temporarily publishing it during draft construction. python/sglang/srt/managers/scheduler.pyL868-L885
  • Regression coverage confirms that a load-time override reaches the draft without mutating the target, and that the draft-specific context length wins over a resolved value for the same field. test/registered/unit/spec/test_draft_server_args_copy.pyL55-L70
  • No additional changes were necessary. The fix is already committed at the current head and the working tree is clean, so no redundant commit or pull request was created.

Testing

  • python -m py_compile python/sglang/srt/managers/scheduler.py python/sglang/srt/speculative/draft_worker_common.py test/registered/unit/spec/test_draft_server_args_copy.py test/registered/unit/spec/test_spec_worker_draft_isolation.py
  • git diff --check 5abdee25ebf0c21e3e443f2dadf04fd5a5c8845b..HEAD
  • git status --short
  • ⚠️ PYTHONPATH=python python -m pytest -q test/registered/unit/spec/test_draft_server_args_copy.py test/registered/unit/spec/test_spec_worker_draft_isolation.py (collection is blocked because the environment lacks the required numpy dependency)

View task →

@ch-wan
ch-wan force-pushed the cheng/gc-wb-2-draft-copy branch from 3ab0285 to 1385e39 Compare August 2, 2026 18:22
@ch-wan
ch-wan force-pushed the cheng/gc-wb-2-draft-copy branch from 1385e39 to a0176dd Compare August 2, 2026 22:43
@ch-wan

ch-wan commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author

Commit message and the SKILL bullet corrected (a0176ddc52), both stale as the review found:

  • The message claimed test_spec_worker_draft_isolation.py asserts each of the four workers hands its draft a distinct instance. It stubs the worker factory and pins the scheduler handoff — which is the right thing to pin now that the copy is made once, before create_worker. The message says that.
  • The SKILL still opened with "Only drafts built through build_draft_tp_worker() get private bags". Now: every draft is built under a preserved publish of its own config — the scheduler makes and publishes the copy around the worker factory, build_draft_tp_worker() nests the same shape for dflash/dspark — and the publish ends when construction does, so anything the draft reads later (alloc_memory_pool, attention-backend init, cuda-graph capture) is back on the target's bags.

Also fixed the double "Using draft model load_format" log the review spotted: it is emitted once, in draft_server_args_copy, and _draft_load_format_fields() is now a pure field builder.

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 a0176dd to 1c7bb56 Compare August 2, 2026 23:00
@ch-wan ch-wan added run-ci ready-to-merge The PR is ready to merge after the CI is green. run-ci-extra and removed run-ci labels Aug 3, 2026
@ch-wan

ch-wan commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

Closing unmerged and reopening against main directly.

GitHub classifies a chained-base series as a stack, and in that mode it refuses base retargeting (Cannot change the base branch because the pull request is part of a stack), the classic merge API (must be merged using the asynchronous merge REST API), and the plain REST merge (403). The async endpoint accepts the request but honours branch protection, and it has no bypass parameter — so this series could not be merged in order.

The replacement PR carries the identical commit; the review history, the six rounds of comment triage and the validation notes stay here for reference. Link posted below.

@ch-wan

ch-wan commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

Reopened as #33335 (base main, identical commit). Review context stays here.

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 ready-to-merge The PR is ready to merge after the CI is green. run-ci run-ci-extra

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant