Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions common/speculative.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2542,6 +2542,9 @@ common_speculative_init_result::common_speculative_init_result(
model_path = params.speculative.draft.mparams.path;
LOG_INF("%s: loading draft model '%s'\n", __func__, model_path.c_str());

// a draft head can leave out the embeddings and lm head and use the target's
mparams.model_shared = model_tgt;

llama_model * model_dft = llama_model_load_from_file(params.model.path.c_str(), mparams);
if (model_dft == NULL) {
LOG_ERR("%s: failed to load draft model, '%s'\n", __func__, model_path.c_str());
Expand Down
4 changes: 2 additions & 2 deletions conversion/bailingmoe3.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,9 @@ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Ca

if is_mtp and cls.no_mtp:
return None
if cls.mtp_only and not is_mtp and name not in (
if cls.mtp_only and not is_mtp and (cls.mtp_shared_embd or name not in (
"model.word_embeddings.weight", "model.norm.weight", "lm_head.weight",
):
)):
return None

return super().filter_tensors((name, gen))
Expand Down
2 changes: 2 additions & 0 deletions conversion/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ class ModelBase:
supports_mtp_export: bool = False
mtp_only: bool = False
no_mtp: bool = False
# with mtp_only, leave the shared embeddings and lm head to the target model
mtp_shared_embd: bool = False

def __init__(self, dir_model: Path, ftype: gguf.LlamaFileType, fname_out: Path, *, is_big_endian: bool = False,
use_temp_file: bool = False, eager: bool = False,
Expand Down
4 changes: 2 additions & 2 deletions conversion/command_r.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,9 @@ def filter_tensors(cls, item):
is_mtp = (m := re.match(r"model\.layers\.(\d+)\.", name)) is not None and int(m.group(1)) >= cls._n_main_layers
if is_mtp and cls.no_mtp:
return None
if cls.mtp_only and not is_mtp and name not in (
if cls.mtp_only and not is_mtp and (cls.mtp_shared_embd or name not in (
"model.embed_tokens.weight", "model.norm.weight", "lm_head.weight",
):
)):
return None

return name, gen
Expand Down
4 changes: 2 additions & 2 deletions conversion/dots3.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,9 @@ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Ca
# --no-mtp: drop the NextN/MTP block; --mtp: keep only that block plus the shared embeddings/norm/lm_head
if is_mtp and cls.no_mtp:
return None
if cls.mtp_only and not is_mtp and name not in (
if cls.mtp_only and not is_mtp and (cls.mtp_shared_embd or name not in (
"model.embed_tokens.weight", "model.norm.weight", "lm_head.weight",
):
)):
return None

return name, gen
Expand Down
12 changes: 6 additions & 6 deletions conversion/glm.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,9 @@ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Ca

if is_mtp and cls.no_mtp:
return None
if cls.mtp_only and not is_mtp and name not in (
if cls.mtp_only and not is_mtp and (cls.mtp_shared_embd or name not in (
"model.embed_tokens.weight", "model.norm.weight", "lm_head.weight",
):
)):
return None

return name, gen
Expand Down Expand Up @@ -292,9 +292,9 @@ def filter_tensors(cls, item):
is_mtp = match is not None and int(match.group(1)) >= cls._n_main_layers
if is_mtp and cls.no_mtp:
return None
if cls.mtp_only and not is_mtp and name not in (
if cls.mtp_only and not is_mtp and (cls.mtp_shared_embd or name not in (
"model.embed_tokens.weight", "model.norm.weight", "lm_head.weight",
):
)):
return None

return name, gen
Expand Down Expand Up @@ -352,9 +352,9 @@ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Ca
return None
# --mtp: keep ONLY NextN-block tensors plus the shared embeddings/
# norm/lm_head (so the resulting GGUF carries just the draft head).
if cls.mtp_only and not is_mtp and name not in (
if cls.mtp_only and not is_mtp and (cls.mtp_shared_embd or name not in (
"model.embed_tokens.weight", "model.norm.weight", "lm_head.weight",
):
)):
return None

return name, gen
Expand Down
2 changes: 1 addition & 1 deletion conversion/qwen.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ def filter_tensors(cls, item):
elif len(parts) == 3 and parts[1] in remapper:
name = f"model.layers.{cls._original_block_count}.{remapper[parts[1]]}.{parts[2]}"
elif cls.mtp_only:
keep = name in (
keep = not cls.mtp_shared_embd and name in (
"model.embed_tokens.weight", "model.norm.weight", "lm_head.weight",
"embed_tokens.weight", "norm.weight",
)
Expand Down
10 changes: 10 additions & 0 deletions convert_hf_to_gguf.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ def parse_args() -> argparse.Namespace:
"--no-nextn", "--no-mtp", dest="no_mtp", action="store_true",
help="Exclude NextN speculative draft tensors from the converted GGUF. Pair with --mtp or --dspark on a second run to publish target and draft as two files.",
)
parser.add_argument(
"--mtp-shared-embd", action="store_true",
help="With --mtp, leave the token embeddings, output norm and LM head out of the draft and take them from the target model at load time. Much smaller draft, but it needs a llama.cpp new enough to read it.",
)
parser.add_argument(
"--dspark", action="store_true",
help="Export only the DeepSeek-V4 DSpark draft tensors as a separate GGUF.",
Expand Down Expand Up @@ -278,6 +282,12 @@ def main() -> None:
if args.mtp:
model_class.mtp_only = True

if args.mtp_shared_embd:
if not args.mtp:
logger.error("--mtp-shared-embd only applies together with --mtp")
sys.exit(1)
model_class.mtp_shared_embd = True
Comment on lines +285 to +289

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 Restrict or implement shared stripping for every MTP exporter

This enables mtp_shared_embd for every architecture supporting --mtp, but several such exporters still unconditionally retain their shared tensors: DeepSeek V3.2/V4, HY V3, Nemotron-H, and Step3.5 have unchanged mtp_only keep lists that do not inspect this flag. For those inputs, --mtp-shared-embd succeeds and writes the shared-target metadata but produces essentially the ordinary large sidecar, contrary to the CLI promise. Either update each supported filter or reject this option for exporters that do not implement stripping.

Useful? React with 👍 / 👎.


model_instance = model_class(dir_model, output_type, fname_out,
is_big_endian=args.bigendian, use_temp_file=args.use_temp_file,
eager=args.no_lazy,
Expand Down
4 changes: 4 additions & 0 deletions include/llama.h
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,10 @@ extern "C" {
// override key-value pairs of the model meta data
const struct llama_model_kv_override * kv_overrides;

// already loaded model to take the shared embeddings and lm head from, for a draft
// head that ships neither. must outlive the model being loaded
const struct llama_model * model_shared;

// Keep the booleans together to avoid misalignment during copy-by-value.
bool vocab_only; // only load the vocabulary, no weights
bool check_tensors; // validate model tensor data
Expand Down
70 changes: 70 additions & 0 deletions src/llama-model-loader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1106,6 +1106,70 @@ bool llama_model_loader::lazy_read::add(const std::string & name, const ggml_ten
return true;
}

// declared in llama-model.h, which this file does not include
const std::vector<std::pair<std::string, ggml_tensor *>> & llama_internal_get_tensor_map(const llama_model * model);

struct ggml_tensor * llama_model_loader::borrow_shared_tensor(const LLM_TN_IMPL & tn, const std::initializer_list<int64_t> & ne) {
// only the tensors a draft head is allowed to leave out, checked first so no other
// tensor in any model costs a metadata lookup
if (tn.tensor != LLM_TENSOR_TOKEN_EMBD && tn.tensor != LLM_TENSOR_OUTPUT && tn.tensor != LLM_TENSOR_OUTPUT_NORM) {
return nullptr;
}

// a file that ships the tensor keeps its own copy
const std::string name = tn.str();
if (get_weight(name.c_str()) != nullptr) {
return nullptr;
}

// a draft that left out its embeddings left out the whole set, so the missing token_embd is
// what marks one. without this an arch that ties the head to its own token_embd when
// output.weight is absent (qwen3.5, qwen3-next) would borrow the target's head instead
if (get_weight("token_embd.weight") != nullptr) {
return nullptr;
}

if (model_shared == nullptr) {
throw std::runtime_error(format("%s: missing tensor '%s'; if this is a draft head that shares "
"the target's embeddings, load it as a draft of its target model, not on its own",
__func__, name.c_str()));
}

ggml_tensor * src = nullptr;
for (const auto & [n, t] : llama_internal_get_tensor_map(model_shared)) {
if (n == name) {
src = t;
break;
}
}
if (src == nullptr) {
throw std::runtime_error(format("%s: draft needs tensor '%s' from the target, which does not have it",
__func__, name.c_str()));
Comment on lines +1145 to +1147

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 Handle tied output weights when borrowing the LM head

When the target has tied embeddings and omits output.weight, its loader falls back to a duplicate named token_embd.weight; consequently this name-based lookup cannot find output.weight. A stripped draft still requests the optional output tensor before its own tie fallback, reaches this exception, and cannot load against an otherwise compatible tied target. Resolve the target model's effective output tensor, including its tied fallback, rather than requiring an entry with the requested GGUF name.

Useful? React with 👍 / 👎.

}

// the draft uses the tensor directly, so the shapes must agree exactly
size_t dim = 0;
for (const int64_t n : ne) {
if (dim >= GGML_MAX_DIMS || src->ne[dim] != n) {
throw std::runtime_error(format("%s: draft and target disagree on '%s': target has %s, draft wants %s",
__func__, name.c_str(), llama_format_tensor_shape(src).c_str(), llama_format_tensor_shape(ne).c_str()));
}
dim++;
}
for (; dim < GGML_MAX_DIMS; dim++) {
if (src->ne[dim] != 1) {
throw std::runtime_error(format("%s: draft and target disagree on '%s': target has %s, draft wants %s",
__func__, name.c_str(), llama_format_tensor_shape(src).c_str(), llama_format_tensor_shape(ne).c_str()));
}
}

LLAMA_LOG_INFO("%s: tensor %s taken from the target model\n", __func__, name.c_str());

// not counted in n_created or size_data: the tensor is not in this file and is neither
// allocated nor freed here
return src;
}

struct ggml_tensor * llama_model_loader::create_tensor(
const llama_hparams & hparams, const buft_list_t * buft_list_cpu, const buft_list_t * buft_list_input, const buft_list_t * buft_list_output,
const buft_list_t * buft_list_layer, const LLM_TN_IMPL & tn, const std::initializer_list<int64_t> & ne, int flags) {
Expand Down Expand Up @@ -1326,6 +1390,12 @@ struct ggml_tensor * llama_model_loader::create_tensor(
return ret;
}

// must run before check_tensor_dims: the tensor is absent from this file by design, and for
// the lm head it must also win over the arch fallback that ties the head to token_embd
if (ggml_tensor * shared = borrow_shared_tensor(tn, ne)) {
return shared;
}

LLAMA_LOG_DEBUG("%s: loading tensor %s\n", __func__, tn.str().c_str());
const struct ggml_tensor * cur = check_tensor_dims(tn.str(), ne, !(flags & TENSOR_NOT_REQUIRED), flags & TENSOR_ALLOW_RESHAPE);
if (cur == NULL) {
Expand Down
9 changes: 9 additions & 0 deletions src/llama-model-loader.h
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ struct llama_model_loader {
std::set<std::string> tensors;
} lazy;

// target model a draft head borrows the shared tensors from, see borrow_shared_tensor()
const struct llama_model * model_shared = nullptr;


llama_files files;
llama_ftype ftype;
llama_fver fver;
Expand Down Expand Up @@ -238,6 +242,11 @@ struct llama_model_loader {
const llama_hparams & hparams, const buft_list_t * buft_list_cpu, const buft_list_t * buft_list_input, const buft_list_t * buft_list_output,
const buft_list_t * buft_list_layer, const LLM_TN_IMPL & tn, const std::initializer_list<int64_t> & ne, int flags);

// a draft head that shares the target's embeddings does not carry its own token_embd,
// output or output_norm; take them from the target model instead. returns null unless the
// file is missing token_embd, so a model that ships its own tensors is never affected
struct ggml_tensor * borrow_shared_tensor(const LLM_TN_IMPL & tn, const std::initializer_list<int64_t> & ne);

void done_getting_tensors(bool partial = false) const;

void init_mappings(bool prefetch = true, llama_mlocks * mlock_mmaps = nullptr);
Expand Down
1 change: 1 addition & 0 deletions src/llama-model.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2692,6 +2692,7 @@ llama_model_params llama_model_default_params() {
/*.progress_callback =*/ nullptr,
/*.progress_callback_user_data =*/ nullptr,
/*.kv_overrides =*/ nullptr,
/*.model_shared =*/ nullptr,
/*.vocab_only =*/ false,
/*.check_tensors =*/ false,
/*.use_extra_bufts =*/ true,
Expand Down
3 changes: 2 additions & 1 deletion src/llama.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,8 @@ static std::pair<int, llama_model *> llama_model_load(struct gguf_context * meta
llama_model_loader ml(metadata, set_tensor_data, set_tensor_data_ud, fname, splits, file, params.load_mode,
params.check_tensors, params.no_alloc, params.load_mtp, params.kv_overrides, params.tensor_buft_overrides);

ml.lazy.mode = params.lazy_mode;
ml.lazy.mode = params.lazy_mode;
ml.model_shared = params.model_shared;

ml.print_info();
std::unique_ptr<llama_model> model_ptr(llama_model_create(ml, params));
Expand Down
Loading