-
Notifications
You must be signed in to change notification settings - Fork 23.8k
models: Qwen3.8-Flash-Next MTP #28243
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
c8c3a5b
57672d9
84f9558
86d321a
30b6537
44c7960
eb65412
44ff803
cc2c59a
2c96729
d1a9235
53b1389
6fd37b7
6fcaa16
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1420,7 +1420,9 @@ struct common_speculative_impl_draft_mtp : public common_speculative_impl { | |
| llama_set_embeddings_nextn(ctx_tgt, true, /*masked*/ false); | ||
| llama_set_embeddings_nextn(ctx_dft, true, /*masked*/ true); | ||
|
|
||
| is_mem_shared = llama_get_ctx_other(ctx_dft) == ctx_tgt; | ||
| char arch[64] = {0}; | ||
| llama_model_meta_val_str(llama_get_model(ctx_dft), "general.architecture", arch, sizeof(arch)); | ||
| is_mem_shared = llama_get_ctx_other(ctx_dft) == ctx_tgt && std::strcmp(arch, "gemma4-assistant") == 0; | ||
| chain_heads = n_mtp_layers > 1 && !is_mem_shared; | ||
|
|
||
| if (chain_heads) { | ||
|
|
@@ -2559,7 +2561,7 @@ 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()); | ||
|
|
||
| llama_model * model_dft = llama_model_load_from_file(params.model.path.c_str(), mparams); | ||
| llama_model * model_dft = llama_model_load_from_file(model_path.c_str(), mparams); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't a fix, both expressions are the same string. I'd suggest just dropping this hunk from the PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ruixiang63 Are you sure about that? See 3fb9b98
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have you encountered any issues with MTP on the current master branch? If so, please open a separate issue. |
||
| if (model_dft == NULL) { | ||
| LOG_ERR("%s: failed to load draft model, '%s'\n", __func__, model_path.c_str()); | ||
| return; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 ( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Based on the suggestion below, I don’t think we need |
||
| "model.word_embeddings.weight", "model.norm.weight", "lm_head.weight", | ||
| ): | ||
| )): | ||
| return None | ||
|
|
||
| return super().filter_tensors((name, gen)) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 ( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this will not be needed as well. |
||
| "model.embed_tokens.weight", "model.norm.weight", "lm_head.weight", | ||
| ): | ||
| )): | ||
| return None | ||
|
|
||
| return name, gen | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,6 @@ | ||||||||||||||||||||||
| from __future__ import annotations | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| from typing import Iterable, cast | ||||||||||||||||||||||
| from typing import Callable, Iterable, cast | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import torch | ||||||||||||||||||||||
| from torch import Tensor | ||||||||||||||||||||||
|
|
@@ -21,20 +21,51 @@ class Qwen4ExpTextModel(_Qwen35MRopeMixin, _LinearAttentionVReorderBase): | |||||||||||||||||||||
| Shares the Qwen3.5 gated delta net and interleaved mrope, and adds three things: | ||||||||||||||||||||||
| hyper-connections in place of every layer norm, QSA sparse attention on the full | ||||||||||||||||||||||
| attention layers, and PLE n-gram hash embeddings on a single layer. | ||||||||||||||||||||||
| The checkpoint also carries a NextN/MTP draft head under `mtp.*`, exported as a | ||||||||||||||||||||||
| trailing block; pass --no-nextn to leave it out. | ||||||||||||||||||||||
| """ | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| model_arch = gguf.MODEL_ARCH.QWEN4EXP | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # the MTP block is a separate draft head; vLLM drops it too | ||||||||||||||||||||||
| supports_mtp_export = False | ||||||||||||||||||||||
| no_mtp = True | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def __init__(self, *args, **kwargs): | ||||||||||||||||||||||
| super().__init__(*args, **kwargs) | ||||||||||||||||||||||
| # only the shard names, so the table itself is never held | ||||||||||||||||||||||
| self._ple_shards: dict[int, str] = {} | ||||||||||||||||||||||
| self._ple_row_dim: int | None = None | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| _MTP_MIXER_PREFIX = "mtp.hyper_connection_mixer." | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @classmethod | ||||||||||||||||||||||
| def filter_tensors(cls, item): | ||||||||||||||||||||||
| name, gen = item | ||||||||||||||||||||||
| _, _, mixer_suffix = name.partition(cls._MTP_MIXER_PREFIX) | ||||||||||||||||||||||
|
Comment on lines
+37
to
+42
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||
| if mixer_suffix: | ||||||||||||||||||||||
| if cls.no_mtp: | ||||||||||||||||||||||
| return None | ||||||||||||||||||||||
| assert cls._original_block_count is not None | ||||||||||||||||||||||
| return f"model.layers.{cls._original_block_count}.hyper_connection_mixer.{mixer_suffix}", gen | ||||||||||||||||||||||
| return super().filter_tensors((name, gen)) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def index_tensors(self, remote_hf_model_id: str | None = None) -> dict[str, Callable[[], Tensor]]: | ||||||||||||||||||||||
| tensors = super().index_tensors(remote_hf_model_id=remote_hf_model_id) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| emb = tensors.pop("mtp.fc_embedding.weight", None) | ||||||||||||||||||||||
| hid = tensors.pop("mtp.fc_hidden.weight", None) | ||||||||||||||||||||||
| if emb is None and hid is None: | ||||||||||||||||||||||
| return tensors | ||||||||||||||||||||||
| if emb is None or hid is None: | ||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||
| "the qwen4exp MTP combiner needs both mtp.fc_embedding.weight and " | ||||||||||||||||||||||
| "mtp.fc_hidden.weight; pass --no-nextn to convert without the draft head" | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| assert self._original_block_count is not None | ||||||||||||||||||||||
| # W_e@e + W_h@h == [W_e|W_h] @ concat(e, h); embedding first, matching the graph's concat. | ||||||||||||||||||||||
| name = f"model.layers.{self._original_block_count}.eh_proj.weight" | ||||||||||||||||||||||
| tensors[name] = lambda: torch.cat([emb(), hid()], dim=1) | ||||||||||||||||||||||
| return tensors | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def _read_hash_constants(self, suffix: str) -> list[int]: | ||||||||||||||||||||||
| """Read an int64 PLE constant straight from the checkpoint. | ||||||||||||||||||||||
|
|
@@ -63,14 +94,14 @@ def set_gguf_parameters(self): | |||||||||||||||||||||
| self.gguf_writer.add_indexer_top_k(hp["indexer_budget"]) | ||||||||||||||||||||||
| ratio = hp["indexer_compress_ratio"] | ||||||||||||||||||||||
| layer_types = hp["layer_types"] | ||||||||||||||||||||||
| self.gguf_writer.add_attention_compress_ratios( | ||||||||||||||||||||||
| [ratio if layer_types[i] == "full_attention" else 0 for i in range(n_layer)] | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| ratios = [ratio if layer_types[i] == "full_attention" else 0 for i in range(n_layer)] | ||||||||||||||||||||||
| # 0 selects dense, which is how the MTP block attends. | ||||||||||||||||||||||
| ratios += [0] * (self.block_count - n_layer) | ||||||||||||||||||||||
| self.gguf_writer.add_attention_compress_ratios(ratios) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # ple_layer_ids is 1-based in the HF config; empty means no n-gram table, | ||||||||||||||||||||||
| # so emit no PLE keys rather than optional ones | ||||||||||||||||||||||
| # ple_layer_ids is 1-based in the HF config; empty means no n-gram table | ||||||||||||||||||||||
| ple_layers = [i - 1 for i in hp["ple_layer_ids"]] | ||||||||||||||||||||||
| if not ple_layers: | ||||||||||||||||||||||
| if not ple_layers or self.mtp_only: | ||||||||||||||||||||||
| return | ||||||||||||||||||||||
| self.gguf_writer.add_ple_layers(ple_layers) | ||||||||||||||||||||||
| self.gguf_writer.add_ple_ngram_size(hp["ngram_size"]) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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.", | ||
| ) | ||
|
Comment on lines
+128
to
+131
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How much space do we save? My estimate is about 1GB of If this is correct, my recommendation is to remove this sharing functionality in order to reduce the complexity. It is not worth it and additionally, am efficient MTP setup actually benefits from not sharing the target tensors and instead using fast low-bit quantizations such as
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also don’t think we need to set this explicitly for MTP. I feel it should be less than 1 GB. During MTP conversion, we can export the MTP head either together with the target model or separately. In the former case, the MTP head does not need its own |
||
| parser.add_argument( | ||
| "--dspark", action="store_true", | ||
| help="Export only the DeepSeek-V4 DSpark draft tensors as a separate GGUF.", | ||
|
|
@@ -282,6 +286,12 @@ def main() -> None: | |
| if args.mtp: | ||
| model_class.mtp_only = True | ||
|
|
||
| if args.mtp_shared_embd: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here. |
||
| if not args.mtp: | ||
| logger.error("--mtp-shared-embd only applies together with --mtp") | ||
| sys.exit(1) | ||
| model_class.mtp_shared_embd = True | ||
|
|
||
| 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, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -152,7 +152,7 @@ llama_context::llama_context( | |
| cparams.ctx_other = params.ctx_other; | ||
| } | ||
|
|
||
| if (model.arch == LLM_ARCH_EAGLE3 || model.arch == LLM_ARCH_DFLASH) { | ||
| if (model.arch == LLM_ARCH_EAGLE3 || model.arch == LLM_ARCH_DFLASH || model.arch == LLM_ARCH_QWEN4EXP) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here is not necessary as well. Let's follow the current MTP support design. |
||
| if (model.tok_embd == nullptr || model.output == nullptr) { | ||
| if (params.ctx_other == nullptr) { | ||
| throw std::runtime_error(model.arch_name() + " requires ctx_other to be set (this warning is normal during memory fitting)"); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see https://github.com/ggml-org/llama.cpp/pull/28243/changes#r4082502758. If we follow how other models support MTP, this will not be needed. e.g. #26725