Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4412bc0
Studio: launch a DFlash drafter automatically
danielhanchen Aug 10, 2026
6c5485a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 10, 2026
1664603
Harden the DFlash drafter fallback, pairing and dedupe
danielhanchen Aug 10, 2026
7d474e9
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 10, 2026
3984978
Keep DFlash discovery, the training guard and the hints in step
danielhanchen Aug 10, 2026
cfe10af
Pair the remote DFlash sidecars with the selected weight
danielhanchen Aug 10, 2026
d4933b7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 10, 2026
a10b30a
Validate DFlash candidates and size the extras drafter once
danielhanchen Aug 10, 2026
2ca75d8
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 10, 2026
0b95256
Validate remote DFlash files by header and stop charging unused DFlas…
danielhanchen Aug 10, 2026
812c889
Gate the DFlash stand-down and the guard's sizing on what the load ac…
danielhanchen Aug 10, 2026
87f2373
Apply the load's boundaries to drafter discovery, and size Auto's one…
danielhanchen Aug 10, 2026
a2e02be
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 10, 2026
f003319
Budget a split DFlash sidecar as a set, and reject half a cached one
danielhanchen Aug 10, 2026
3f22e03
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 10, 2026
e4c6a6a
Move drafter naming, ranking and DFlash discovery into a drafters pac…
danielhanchen Aug 10, 2026
ebd0ef2
Give the guard's DFlash bound a name and a home
danielhanchen Aug 10, 2026
852e642
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 10, 2026
32d1227
Studio: fix the DFlash lint gate, and carry over what #6747 got right
danielhanchen Aug 11, 2026
06d8f95
Tighten the DFlash comments for PR #8338
danielhanchen Aug 11, 2026
80c5af0
Apply ruff-format kwarg spacing for PR #8338
danielhanchen Aug 11, 2026
f91bd1f
Merge remote-tracking branch 'origin/main' into pr-8338-head
danielhanchen Aug 11, 2026
2534936
Fix the DFlash download plan and two stale-state reloads for PR #8338
danielhanchen Aug 11, 2026
0460d2c
Carry the DFlash plan bounds into the runtime paths for PR #8338
danielhanchen Aug 11, 2026
9ac0469
Merge remote-tracking branch 'origin/main' into pr-8338-head
danielhanchen Aug 11, 2026
c05c2c2
Make the DFlash size and split rules agree across plan, fetch and gua…
danielhanchen Aug 11, 2026
f3f7201
Price DSpark by shard set and share one split-listing rule for PR #8338
danielhanchen Aug 11, 2026
471ef9e
Tighten the DFlash review-round comments for PR #8338
danielhanchen Aug 11, 2026
744603b
Merge origin/main into studio-dflash-drafter
danielhanchen Aug 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
933 changes: 862 additions & 71 deletions studio/backend/core/inference/llama_cpp.py

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions studio/backend/hub/utils/gguf.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,13 @@ def is_reclaimable_drafter_path(path: str) -> bool:
"""Drafters a repo's last-variant delete may reclaim: MTP, fetched with every
variant, and DSpark, fetched on opt-in. Both are useless once no main GGUF is
left, and companion filtering hides them from the variant menu, so leaving one
behind is an invisible allocation (DSpark is ~11 GB). DFlash is excluded: the
name doubles as a family a user picks for real weights."""
behind is an invisible allocation (DSpark is ~11 GB). DFlash is excluded even
though Auto now launches it: the name doubles as a family a user picks for
real weights, whole repos publish nothing but root-level ``dflash-*.gguf``
(Lucebox/Qwen3.6-27B-DFlash-GGUF), and the two outcomes are not symmetric.
Reclaiming wrongly destroys weights a user chose; not reclaiming leaves
~1.5 GiB, an order of magnitude under the DSpark case this rule was written
for. Locked by test_deleting_the_last_variant_keeps_a_dflash_weight."""
p = path.replace("\\", "/").lower()
if not p.endswith(".gguf"):
return False
Expand Down
107 changes: 106 additions & 1 deletion studio/backend/hub/utils/gguf_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,89 @@ def preferred_mtp_sibling(siblings: Sequence) -> Optional[object]:
return candidates[0] if candidates else None


def preferred_dflash_sibling(
siblings: Sequence,
weight_name: Optional[str] = None,
other_weight_names: Sequence[str] = (),
) -> Optional[object]:
"""The DFlash sidecar to fetch alongside ``weight_name``.

Root level only, like preferred_mtp_sibling: detect_dflash_file never offers a
nested ``quants/dflash-*.gguf``, and a listing cannot read a header, so matching
the basename would plan a whole ordinary weight nothing could reject in time.

Ordered by dflash_repo_preference_key, as the download, snapshot reuse and offline
cache are, so the manifest promises the file the loader launches.
"""
from utils.models.drafters import dflash_repo_preference_key

candidates = [
s
for s in siblings
if (name := _gguf_rfilename(s)) and "/" not in name and name.lower().startswith("dflash-")
]
if not candidates:
return None
return min(
candidates,
key = lambda s: dflash_repo_preference_key(
getattr(s, "rfilename"), weight_name, other_weight_names
),
)


def dflash_plan_files(
siblings: Sequence,
weight_name: Optional[str] = None,
other_weight_names: Sequence[str] = (),
*,
max_bytes: int = 0,
) -> tuple[ExpectedFile, ...]:
"""Every shard of the DFlash sidecar to plan alongside ``weight_name``, or ().

Whole shard family, not the ranked file alone: the loader refuses an incomplete
split set, so planning shard 1 reports the variant complete and then loses DFlash.
A half-published family is dropped for the same reason.

Bounded by ``max_bytes``, the variant's own weights. ``dflash-`` is a prefix real
weights carry (Lucebox/Qwen3.6-27B-DFlash-GGUF) and a listing cannot read the
``general.architecture`` the loader rejects them by, but a drafter is a few layers
of its target and cannot outweigh it. An unknown size stays out.

Both rules filter BEFORE the ranking, so an oversized or half-published name at the
top steps aside for a usable sidecar behind it.
"""
from utils.models.drafters import dflash_repo_preference_key, split_listing_is_complete

families: dict[str, list[ExpectedFile]] = {}
for sibling in siblings:
name = _gguf_rfilename(sibling)
if not name or "/" in name or not name.lower().startswith("dflash-"):
continue
file = expected_file_from_sibling(sibling)
if file is not None:
families.setdefault(gguf_variant_family(name), []).append(file)

eligible: dict[str, tuple[ExpectedFile, ...]] = {}
for family, files in families.items():
shards = tuple(sorted(files, key = lambda file: file.path))
if not split_listing_is_complete([f.path for f in shards], shards[0].path):
continue
total = sum(max(0, int(file.size or 0)) for file in shards)
if not total or max_bytes <= 0 or total >= max_bytes:
continue
eligible[family] = shards
if not eligible:
return ()
best = min(
eligible,
key = lambda family: dflash_repo_preference_key(
eligible[family][0].path, weight_name, other_weight_names
),
)
return eligible[best]


def build_gguf_variant_plans(siblings: Sequence) -> dict[str, GgufVariantPlan]:
main: dict[str, list] = {}
all_mmproj = mmproj_siblings(siblings)
Expand Down Expand Up @@ -166,13 +249,35 @@ def build_gguf_variant_plans(siblings: Sequence) -> dict[str, GgufVariantPlan]:
main.setdefault(quant, []).append(sibling)

plans: dict[str, GgufVariantPlan] = {}
# Every weight in the listing, so the ranking can tell a sidecar naming a
# neighbouring family from one naming this variant's.
all_weight_names = [
name.rsplit("/", 1)[-1]
for quant_siblings in main.values()
for sibling in quant_siblings
if (name := _gguf_rfilename(sibling))
]
for quant, target_main_siblings in main.items():
main_expected = tuple(
file
for sibling in target_main_siblings
if (file := expected_file_from_sibling(sibling)) is not None
)
expected_files = (*main_expected, *companions_expected)
# Per variant, unlike mmproj and the MTP drafter: ranked against the weight
# being fetched, so a multi-family repo does not hand B the drafter naming A.
# Against the family plan_from_expected_files KEEPS, not the listing's first,
# or a two-family variant key pairs the discarded one's sidecar.
kept_main = _one_shard_family(main_expected)
target_weight_name = (
min(file.path for file in kept_main).rsplit("/", 1)[-1] if kept_main else None
)
dflash_expected = dflash_plan_files(
siblings,
target_weight_name,
[n for n in all_weight_names if n != target_weight_name],
max_bytes = sum(max(0, int(file.size or 0)) for file in kept_main),
)
expected_files = (*main_expected, *companions_expected, *dflash_expected)
plans[quant] = plan_from_expected_files(
quant,
expected_files,
Expand Down
20 changes: 12 additions & 8 deletions studio/backend/models/inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,15 @@ def normalize_blank_chat_template_override(cls, value: Optional[str]) -> Optiona
description = (
"Speculative decoding mode for GGUF models. Canonical values: "
"'auto' (platform-aware: DSpark when the model ships a sidecar, "
"else MTP on MTP GGUFs, ngram-mod fallback for sub-3B), "
"else DFlash when it ships one, else MTP on MTP GGUFs, ngram-mod "
"fallback for sub-3B), "
"'mtp' (force draft-mtp only on both GPU and CPU), "
"'dspark' (force a draft-dspark sidecar), "
"'dflash' (force a draft-dflash sidecar), "
"'ngram' (force ngram-mod only), 'mtp+ngram' (force "
"ngram-mod+draft-mtp chain on both platforms), 'off' (disabled). "
"Legacy values 'default' (-> auto), 'draft-mtp' (-> mtp), "
"'draft-dspark' (-> dspark), "
"'draft-dspark' (-> dspark), 'draft-dflash' (-> dflash), "
"'ngram-mod' (-> ngram), and 'ngram-simple' (kept as-is) are "
"still accepted. Ignored for non-GGUF models."
),
Expand All @@ -129,11 +131,12 @@ def normalize_blank_chat_template_override(cls, value: Optional[str]) -> Optiona
ge = 1,
le = 16,
description = (
"Max draft tokens per step for MTP or DSpark speculative decoding "
"(--spec-draft-n-max). Defaults to 2 on GPU and 3 on CPU/Mac "
"when unset (upstream-bench sweet spot for dense Qwen3.6 MTP "
"quants). Only applied when speculative_type resolves to "
"'mtp', 'mtp+ngram', or 'dspark'."
"Max draft tokens per step for MTP, DSpark or DFlash speculative "
"decoding (--spec-draft-n-max). Defaults to 2 on GPU and 3 on "
"CPU/Mac when unset (upstream-bench sweet spot for dense Qwen3.6 "
"MTP quants, and the measured sweet spot for DFlash too). Only "
"applied when speculative_type resolves to 'mtp', 'mtp+ngram', "
"'dspark' or 'dflash'."
),
)
n_parallel: Optional[int] = Field(
Expand Down Expand Up @@ -856,7 +859,8 @@ class InferenceStatusResponse(_InferenceRuntimeFields):
spec_drafter_kind: Optional[str] = Field(
None,
description = (
"Which drafter the resolution was about, 'mtp' or 'dspark'. Needed "
"Which drafter the resolution was about: 'mtp', 'dspark' or "
"'dflash'. Needed "
"because Auto resolves the kind itself, so speculative_type still "
"reads 'auto', and a fallback leaves the engaged type at 'default': "
"neither still says which file the UI should tell the user to fix."
Expand Down
Loading
Loading