Skip to content

download: find MTP heads published in a dedicated MTP/ folder - #166

Closed
danielhanchen wants to merge 2 commits into
base/upstream-9723942adfrom
mtp/sidecar-folder-discovery
Closed

download: find MTP heads published in a dedicated MTP/ folder#166
danielhanchen wants to merge 2 commits into
base/upstream-9723942adfrom
mtp/sidecar-folder-discovery

Conversation

@danielhanchen

@danielhanchen danielhanchen commented Sep 1, 2026

Copy link
Copy Markdown
Member

Problem

find_best_sibling in common/download.cpp only accepts a sidecar whose directory components are a
prefix of the model's, so a top-level MTP/ folder is unreachable. It matches neither a model at the
repository root nor one in a quant subfolder.

Both of our GGUF repos publish their draft heads that way, and on the 27B the miss is not silent.
With no sidecar found, params.speculative.draft.mparams.path stays empty and the target is used as
its own draft, so the server refuses to start:

common_speculative_init_result: creating MTP draft context against the target model '.../Qwen3.8-27B-UD-IQ1_S.gguf'
llama_init_from_model: context type MTP requested but model doesn't contain MTP layers
common_speculative_init_result: failed to create MTP context
srv    load_model: failed to create MTP context

Measured on the shipped b10715-mix-86bd2d3 prebuilt:

repo layout --spec-type draft-mtp, no -md
unsloth/Qwen3.8-27B-GGUF model at root, sidecar in MTP/ nothing found, server fails to start
unsloth/Qwen3.8-Flash-Next-GGUF model in UD-IQ1_S/, sidecar in MTP/ fetches the 3 model shards and root mmproj-BF16.gguf, no MTP file

The second row is also the control that shows sibling discovery itself is fine: mmproj-BF16.gguf
sits at the repo root while the model is in UD-IQ1_S/, and it is discovered. The MTP/ directory
is specifically what defeats the match.

Note --mtp is registered only for LLAMA_EXAMPLE_DOWNLOAD, so on llama-server the switch that
enables discovery is --spec-type draft-mtp.

Change

common/download.cpp and tests/test-model-resolution.cpp. git diff --stat -- ggml/ is empty.

find_best_sibling gains an optional sidecar_dir that additionally admits a candidate sitting in a
single top-level folder of that name, case insensitive. It ranks as depth 0, so a genuine
same-directory sibling still wins. An empty sidecar_dir is exactly the previous behaviour, which is
what leaves find_best_mmproj, find_best_dflash, find_best_eagle3 and find_best_dspark
untouched. Only find_best_mtp passes one, "MTP".

Having more than one head in that folder forces two selection details:

  • prefer_bits picks a designated default when nothing matches the requested tag exactly, instead of
    whichever head is arithmetically nearest. For MTP that is Q8_0. Without it a UD-IQ1_S user would
    be handed the Q4_K_M head on bit proximity, and a BF16 user the bf16 head, which is the worst of
    the three: it is 1.88x larger than Q8_0 and slower, because a draft step is dominated by the LM
    head and that head is cheaper to execute at 8 bits. An exact tag match still wins.
  • a head carrying its own token_embd/output is preferred over one that borrows them from the
    target, so the choice does not depend on repository listing order. The self contained file loads
    against any build.

Both new rules are gated on the candidate having come from the dedicated folder, and a
directory-prefix match ranks ahead of a dedicated-folder one. So any layout that resolved before this
existed resolves to the same file by construction, and a head sharing the model's own directory keeps
winning even when only the folder holds the exact tag.

That gating is the second commit rather than the first, and test-model-resolution is what forced
it. The first version applied both rules to every mtp- candidate, which broke hole default anchor: an untagged Q4_K_M primary expects mtp-model-Q4_0 on quantization distance, and the
preferred quant overrode it to mtp-model-Q8_0.

The ranking keys are now a tuple instead of a chained condition. Order and semantics of the existing
keys are unchanged: greater depth, then exact tag match, then smallest quantization distance.

Verification

Built CPU only from this branch. Selection was read by mapping the blobs the downloader actually
fetched back to repo paths through the HF tree API, so each assertion is about the chosen path rather
than about a download having happened.

test-model-resolution covers the new path with no network, on 19 cases, each replayed on the
harness's listing reorderings:

case asserts
mtp dir flat the folder is found from a model at the repo root
mtp dir subdir and from a model in a quant subdirectory, choosing the preferred quant and the self contained head
mtp dir exact tag an exact tag still beats the preferred quant
mtp dir loses to sibling a same-directory head outranks the folder even when only the folder holds the exact tag
mtp dir near miss MTPX/ is not treated as the dedicated folder

The 14 pre-existing cases are unchanged and still pass.

Live checks against the two real repos, with selection read by mapping the blobs the downloader
actually fetched back to repo paths through the HF tree API, so each assertion is about the chosen
path rather than about a download having happened:

arm expected result
Flash-Next :UD-IQ1_S Q8_0 head, not Q4_K_M and not a shared- one pass
Flash-Next :BF16 exact tag wins, bf16 head pass
Flash-Next :Q8_0 exact tag wins, Q8_0 head pass
positive control, all three arms root mmproj-BF16.gguf still discovered pass
negative control, shipped b10715 binary still finds no MTP file pass
27B :UD-IQ1_S end to end server starts pass, it fails to start today
Llama-3.2-1B-Instruct-GGUF:Q8_0, no --spec-type starts normally pass

The 27B arm is the sharpest one: today it fails outright, so a clean start is only reachable if the
sidecar was found. A repo with no MTP folder still errors under --spec-type draft-mtp, which is
unchanged pre-existing behaviour, not something this introduces.

gguf_filename_is_model already excludes basenames containing mtp-, so publishing several
mtp-*.gguf files cannot affect primary model selection. Confirmed by -hf ...:Q8_0 still resolving
the model to Q8_0/Qwen3.8-Flash-Next-Q8_0-*.gguf.

Scope

Fork only for now. find_best_sibling is byte identical in ggml-org master and fork master, so this
is an upstream limitation being patched locally rather than a fork divergence being repaired. Worth
proposing upstream separately once it has run in a few nightlies.

find_best_sibling only accepts a candidate whose directory components are a prefix of the model's,
so a top-level MTP/ folder is unreachable: it matches neither a model at the repo root nor one in a
quant subfolder. unsloth/Qwen3.8-27B-GGUF and unsloth/Qwen3.8-Flash-Next-GGUF both publish their
draft heads that way, and on the first the miss is not silent, the server refuses to start because
it falls back to the target itself and the target has no MTP layers.

Give find_best_sibling an optional sidecar_dir that additionally admits a candidate sitting in a
single top-level folder of that name, ranked as depth 0 so a genuine same-directory sibling still
wins. Empty sidecar_dir is exactly the old behaviour, which is what leaves mmproj, dflash, eagle3
and dspark untouched.

Two selection details follow from having more than one head in that folder:

- prefer_bits picks the designated default when nothing matches the requested tag exactly, rather
  than whichever head is arithmetically nearest. For MTP that is Q8_0, which measured both the
  fastest and the most accurate head, since a draft step is dominated by the LM head and that head
  is cheaper to execute at 8 bits than at bf16. An exact tag match still wins.
- a head carrying its own token_embd/output is preferred over one that borrows them from the
  target, so the choice does not depend on repository listing order.

The ranking keys are now a tuple rather than a chained condition. Order and semantics of the
existing keys are unchanged: greater depth, then exact tag, then smallest quantization distance.

Assisted-by: Claude
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-01T04:34:19.900034Z 18a6ae6 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

… cover it in the tests

The first version applied prefer_bits and the self-contained preference to every mtp- candidate, not
just to the ones the dedicated folder admits. test-model-resolution caught it: `hole default anchor`
resolves an untagged Q4_K_M primary and expects mtp-model-Q4_0 on quantization distance, and the
preferred quant overrode that to mtp-model-Q8_0.

Rank a directory-prefix match ahead of a dedicated-folder one and gate both new rules on not being a
prefix match. Any layout that resolved before this existed now resolves to the same file by
construction rather than by inspection, and a head sharing the model's own directory keeps winning.

Five cases added, replayed on the harness's listing reorderings like the rest:

- a dedicated folder found from a model at the repo root, and from one in a quant subdirectory
- the preferred quant winning when nothing matches the tag, and losing to an exact tag when one does
- a same-directory head outranking the folder even when only the folder holds the exact tag
- MTPX/ not being treated as the dedicated folder

Assisted-by: Claude
danielhanchen added a commit that referenced this pull request Sep 1, 2026
#166 makes sidecar discovery find MTP heads in a dedicated MTP/ folder, which is how both
Qwen3.8-27B-GGUF and Qwen3.8-Flash-Next-GGUF publish them. Without it `--spec-type draft-mtp`
finds nothing on either repo, and on the 27B the server refuses to start.

The 27941 pin was still 6b2b85c. That branch has moved twice since, to 52bc9ff and then to
21c13c4, so the nightly has been building a commit that predates both the -kv-unified NaN
collapse fix and the indexer ext.x/ext.y restore fix. Repin to the current head.

Assisted-by: Claude

@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: 18a6ae61b2

ℹ️ 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 thread common/download.cpp
const std::string & model,
const std::string & tag = "") {
return find_best_sibling(files, model, "mtp-", tag);
return find_best_sibling(files, model, "mtp-", tag, "MTP", 8);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep Q8 preference scoped to dedicated MTP folders

When no HF tag is supplied for an existing flat/root layout, passing prefer_bits = 8 changes ordinary sibling selection as well as the new MTP/ selection. Running test-model-resolution on this commit aborts in hole default anchor: the code selects mtp-model-Q8_0.gguf instead of the established nearest-quant mtp-model-Q4_0.gguf. Apply the Q8 default only to candidates admitted through the dedicated-folder exception so existing layouts retain their behavior.

Useful? React with 👍 / 👎.

Comment thread common/download.cpp Outdated
const std::string path_upper = to_upper(f.path);

bool exact = !tag_upper.empty() && path_upper.find("-" + tag_upper + ".") != std::string::npos;
bool preferred = prefer_bits != 0 && bits == prefer_bits;

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 Rank the Q8_0 tag instead of every 8-bit format

When a repository offers Q8_0 together with another 8-bit MTP format such as Q8_K_XL, extract_quant_bits() returns 8 for both, so both candidates are marked preferred and the selection can fall back to repository listing order. That does not reliably choose the specific Q8_0 format whose measured behavior motivates this default; compare the complete quantization tag instead of only its numerical bit width.

Useful? React with 👍 / 👎.

Comment thread common/download.cpp Outdated
sib_parts.begin(), sib_dir);
if (dir != sib_dir) {

size_t depth = 0;

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 Rank root siblings above dedicated-folder candidates

When the primary model is at the repository root, an ordinary root sidecar also has depth 0, exactly like a newly admitted MTP/ candidate. In repositories containing both layouts, the folder candidate can therefore beat the model-adjacent sidecar on quantization or tie with it and make the result listing-order dependent, despite the stated same-directory precedence. Add a separate rank indicating that normal prefix matching succeeded.

Useful? React with 👍 / 👎.

Comment thread common/download.cpp
Comment on lines +579 to +583
// `sidecar_dir` additionally admits candidates in a dedicated top-level folder of that name, which
// shares no directory prefix with the model and is otherwise unreachable: unsloth publishes MTP
// heads as MTP/mtp-<model>-<quant>.gguf, and without this the folder is silently never matched
// `prefer_bits` wins over proximity when no candidate matches `tag` exactly, so a repo offering
// several heads has one designated default rather than whichever is arithmetically nearest

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Shorten and un-wrap the sidecar comment

This five-line block hard-wraps two sentences and embeds deployment-specific rationale, while the repository standard limits comments to usually 1-2 lines and explicitly forbids hard-wrapping. Keep only a concise generic description of the new ranking inputs and move the Unsloth example out of the code comment.

AGENTS.md reference: AGENTS.md:L74-L77

Useful? React with 👍 / 👎.

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