Skip to content

Studio: drop the speculative drafter under Auto when only the model fits in VRAM - #8435

Merged
danielhanchen merged 28 commits into
mainfrom
studio-drafter-vram-drop
Aug 12, 2026
Merged

danielhanchen merged 28 commits into
mainfrom
studio-drafter-vram-drop

Conversation

@danielhanchen

@danielhanchen danielhanchen commented Aug 11, 2026 •

Copy link
Copy Markdown
Member

Speculative decoding is a speed option, but on a tight card Auto currently makes you pay for it with context length, and under --fit it can make you pay for it with an OOM. This makes Auto give up the drafter first.

The problem

The drafter is treated as non-negotiable in the VRAM fit, and the context is what gives way:

  • _mtp_will_engage is decided first, then _mtp_reserve_bytes is added to the budget alongside weights, KV and mmproj.
  • If it does not all fit, the context is auto-reduced (Context auto-reduced: 32768 -> 8192), and failing that the load falls back to --fit on and offloads layers to host, where decode collapses roughly 3x (Studio: reserve CUDA context and mmproj/MTP soft overhead in the GGUF fit budget #6718).
  • Under --fit, llama.cpp cannot size a separate drafter at all, so its weights are never reserved and the load can simply run out of VRAM.

So on a card that holds the model but not the model plus its drafter, an Auto load either silently halves the context you asked for, or over-commits. In neither case did the user ask for speculative decoding: Auto chose it for them.

The change

Before the fit runs, price the load twice at the context the target alone would get: once without the drafter and once with it. If the target fits and target plus drafter does not, Auto drops the drafter and keeps the context.

  • Only Auto is second-guessed. A drafter chosen in the Settings dropdown, via --speculative-type, or via --spec-type / --model-draft in extra args is honored exactly as before, including today's context reduction.
  • The gate reads the user's own speculative_type, not _mtp_effective. By the time the fit runs, Auto has already been promoted to the drafter kind it resolved, so _mtp_effective can no longer tell a forced dspark from an Auto one.
  • Priced over the whole GPU set, which is the ceiling for any subset the fit later picks, so the drafter is dropped only when no placement could have held both. Where both fit, nothing changes.
  • The decision reaches the launch as drafter_no_vram, which takes the same downgrade the MLA branch already uses: ngram-mod, which costs no VRAM, or spec-off on a build that lacks it. Routing it through _build_speculative_flags rather than blanking the drafter path matters for the embedded-MTP case, where there is no sidecar file to blank and emitting --spec-type draft-mtp against a reserve of zero is exactly the OOM this is meant to prevent.
  • The resolved drafter paths stay recorded, so the UI still names the kind that was dropped and a repeat Apply still dedupes against the running server instead of relaunching it.

Applies to MTP, DSpark and DFlash alike: it lives in the shared fit path and keys on _mtp_will_engage, so it covers the DFlash drafter in #8338 once that lands.

What the user sees

A new spec_fallback_reason, drafter_no_vram, and the amber notice in chat settings reads:

This model fits in VRAM but its DSpark drafter does not, so Auto kept your context length and is running without speculative decoding. Choose DSpark in Settings to force it, at a smaller context.

Unlike binary_no_mtp, it does not offer the llama.cpp update affordance, because an update does not fix a VRAM shortfall.

Not covered

  • --fit loads are unchanged. llama.cpp cannot size the sidecar during fitting, so there is no reserve to compare against and nothing to decide from.
  • The training-coexistence guard in routes/inference.py still charges the sidecar under Auto, so with a training job running a chat load can still be refused for a drafter the loader would now shed. Matching it means replicating the fit math in the route, and loosening it risks evicting a training run, so it is left alone here.

Tests

Four in test_llama_cpp_placement.py, each driving a real load_model against a stubbed 24 GB card and asserting on the composed argv:

  • the drop: target fits, drafter does not, so no --model-draft, no draft-dspark, and -c 8192 survives
  • the guardrail: a drafter that fits still launches, and spec_fallback_reason stays None
  • forcing dspark overrides the drop
  • an embedded MTP head is dropped too, downgrading to ngram-mod

The two new-behaviour tests were confirmed to fail without the change (they launch the drafter); the two guardrails pass either way, which is their point.

Codex follow-ups from #8338

Two items Codex raised on #8338 after it was approved, both landing on the guard this PR already touches:

  • Do not charge a repository drafter the extras replace. With --spec-type draft-dspark and a local --model-draft in extra args, _build_speculative_flags returns before any mode branch, so Studio's own sidecar is neither fetched nor launched; only the named file becomes resident, and it is already charged as _extras_bytes. The guard was billing both and could return a spurious 409. Gated on both halves: owning the spec type while naming no drafter keeps the conservative charge, since a drafter can still arrive by a route the estimate cannot see and this guard protects a running training job.
  • Do not charge a partial DFlash shard set. _download_companion_gguf refuses a split family whose encoded shard count is short, but dflash_budget_bytes was billing whatever the listing showed, so a repo caught mid-publication could be refused a load that fits. The DSpark path already filtered on split_listing_is_complete; the rule now lives in the budget helper where the shard-set logic already is, so the two cannot drift.

One regression test each, both confirmed to fail without their fix.

Two further Codex items from #8338 are not in this PR, because each is a separate surface that deserves its own change and its own tests:

  • Admit root DFlash files covered by a native directory grant. For repo/Q4_K_M/model.gguf, discovery searches repo/ for the sidecar but native_gguf_companion_parent_allowed rejects it, so Auto silently loses DFlash on that layout. The fix has to thread the actual granted root through validation rather than the root derived from the weight path, or it widens the native-grant boundary.
  • Retry DFlash after Hub credentials change. A permanent Hub error such as GatedRepoError sets no retry flag and DFlash is excluded from the drafter_not_found retry arm, so re-applying with a newly valid token dedupes against the drafterless server. Needs credential state in the dedup comparator, which is thrash-sensitive.

@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: c1b3cbfbde

ℹ️ 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".

# Same downgrade as the MLA branch below: ngram-mod costs no VRAM, and
# spec-off when the build lacks it. The drafter paths stay recorded, so
# the UI still names the kind and a repeat Apply still dedupes.
self._spec_fallback_reason = "drafter_no_vram"

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 DSpark identity for reload deduplication

When Auto resolved a DSpark sidecar and this fallback runs, _emit_ngram_mod() changes self._speculative_type to ngram-mod. The duplicate-load comparator identifies an Auto DSpark request only when that field is draft-dspark, so it subsequently compares the incoming MTP path (None) with the stored DSpark sidecar and rejects the match. As a result, every repeated Apply reloads the same healthy drafter-free server; use self._spec_drafter_kind or otherwise retain the resolved DSpark identity in that comparator.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The premise does not hold at head. _spec_drafter_kind is assigned at llama_cpp.py:13712, before any emit branch, from canonical_mode / dspark_draft_path / caps, and drafter_no_vram never touches it, so an Auto DSpark load still carries "dspark". The comparator consults it alongside _speculative_type (llama_cpp.py:4218), and self._mtp_draft_path still records the resolved sidecar, so the compare matches and no reload happens. test_auto_drops_the_drafter_when_only_the_target_fits asserts both.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

@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: 9daf6ba6b4

ℹ️ 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 +11417 to +11418
if _foot_w <= _budget_w:
_both_fit_somewhere = True

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 Check the drafter-adjusted per-device reserve

For an unsized drafter using the flat reserve, _probe_frac(True) lowers every GPU's usable budget, but the new per-device cap is evaluated only with _usable_wo from the target-only fraction. Fresh evidence in the current code is that _foot_w <= _budget_w immediately declares success without checking whether each device holds its pipeline and replicated compute reserve under the drafter-adjusted fraction. On a heterogeneous split the pooled total can pass while the weakest card fails the real planner's _every_gpu_holds_reserve check, so the drafter is kept and the context is still capped; repeat the per-device check using _probe_frac(True) before setting _both_fit_somewhere.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not taking this one, and I checked it properly rather than on the argument. In the sized case _probe_frac(True) is identical to _probe_frac(False), and the target-only per-device check has already run at this same _ctx_wo and capped if needed, so the extra check is provably a no-op. In the unsized case _mtp_bytes is 0, so _foot_w rises by only the 224 MiB draft-graph reserve while _budget_w drops by 5 percent of the subset's TOTAL VRAM: 1229 MiB on a 24 GB card, 4915 MiB on a 96 GB one. The pooled test therefore decides first in every configuration I could construct, and reaching the per-device check needs something like a 96 GB card with 24 GB free carrying a 17 GB compute buffer. I did write the check and then reverted it rather than ship a tightening I cannot demonstrate is reachable.

danielhanchen added a commit to Datta0/unsloth-staging-3 that referenced this pull request Aug 11, 2026
danielhanchen added a commit to danielhanchen/unsloth-staging-2 that referenced this pull request Aug 11, 2026
chatgpt-codex-connector[bot]

This comment was marked as resolved.

@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 11, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 11, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 11, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 11, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 11, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 11, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 11, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 11, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 11, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 11, 2026
chatgpt-codex-connector[bot]

This comment was marked as resolved.

- Draft flags are last-wins, so a repo id followed by --model-draft leaves a
  path as the drafter. _extra_args_mtp_draft_source now returns the value and
  whether the flag that won carried a repo id, so a path is never priced as a
  Hugging Face repository.
- split_listing_is_complete tracks distinct shard indices inside 1..total
  rather than counting matches, so 00001-of-00002 beside a stray 00003-of-00002
  is no longer read as a whole set.
_cached_repo_gguf_bytes scanned huggingface_hub's import-time default, so a
user who moved the cache had their cached drafter missed and priced at the
flat reserve instead. Pass the active hub cache, as the rest of Studio's cache
operations do. Also covers the underscore flag spelling llama.cpp accepts.
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 12, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 12, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 12, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 12, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 12, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 12, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 12, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 12, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 12, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 12, 2026
pre-commit-ci Bot and others added 2 commits August 12, 2026 12:30
The reserve constant carried two overlapping headers, the extras-drafter note
still described the local-file-only rule that was reverted once remote repos
could be priced, and the zero-bound note no longer matched the code.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

- A remote split drafter the listing sized only in part is treated as unsized,
  so the guard measures the cache or falls back to the reserve instead of
  charging the shards it happened to know about. llama-server maps every shard.
- A CPU-offloaded sidecar now clears the byte-accurate mtp_overhead_fn as well
  as the flat fraction. The callback is invoked whatever mtp_engaged says, so
  the fit went on cutting context for GPU bytes no drafter allocates.
- The drafter probe walks both GPU rankings. An unsized drafter costs five
  points of pin fraction, which can reorder heterogeneous cards, and the
  placement loop sorts under the fraction the load ends up with.
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: b0f16e0e79

ℹ️ 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".

pre-commit-ci Bot and others added 2 commits August 12, 2026 13:30
Comment-only pass over the two source files: 231 added comment lines down to
159, with the duplicated zero-bound note and the leftover reserve header gone.
No code or docstring changes (comment_tools.py check passes).
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 12, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 12, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 12, 2026
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@danielhanchen
danielhanchen merged commit a7e073e into main Aug 12, 2026
52 of 58 checks passed
@danielhanchen
danielhanchen deleted the studio-drafter-vram-drop branch August 12, 2026 13:55
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

Reviewed commit: 133acaa691

ℹ️ 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".

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.

1 participant