Repository navigation
model: add dots3-note #27060
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
Merged
Merged
model: add dots3-note #27060
Changes from 4 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
5784322
text: conversion
ngxson 25b654c
init impl
ngxson b78b595
Merge branch 'master' into xsn/dots3-note
ngxson 1090236
address review comments
ngxson 6f2bb72
fix rope
ngxson 6bb8fd0
Merge branch 'master' into xsn/dots3-note
ngxson d9de2b2
move to a new llama_kv_cache_dsa_iswa
ngxson a3f308a
Merge remote-tracking branch 'origin/master' into xsn/dots3-note
ngxson File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,195 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import math | ||
| import re | ||
|
|
||
| from typing import TYPE_CHECKING, Callable, Iterable | ||
|
|
||
| if TYPE_CHECKING: | ||
| from torch import Tensor | ||
|
|
||
| from .base import ModelBase, gguf | ||
|
|
||
| from .deepseek import DeepseekV2Model | ||
|
|
||
|
|
||
| @ModelBase.register("Dots3NoteForCausalLM", "Dots3NoteForConditionalGeneration", "Dots3NoteTextForCausalLM") | ||
| class Dots3NoteModel(DeepseekV2Model): | ||
| model_arch = gguf.MODEL_ARCH.DOTS3NOTE | ||
| skip_mtp = False | ||
| supports_mtp_export = True | ||
|
|
||
| # trunk layer count, stashed before indexing for filter_tensors (mirrors DeepseekV32Model) | ||
| _n_main_layers: int | None = None | ||
|
|
||
| def index_tensors(self, remote_hf_model_id: str | None = None): | ||
| type(self)._n_main_layers = self.hparams["num_hidden_layers"] | ||
| return super().index_tensors(remote_hf_model_id=remote_hf_model_id) | ||
|
|
||
| def __init__(self, *args, **kwargs): | ||
| super().__init__(*args, **kwargs) | ||
|
|
||
| hparams = self.hparams | ||
|
|
||
| # config file doesn't specify MTP block, detect it from model weight | ||
| self.n_nextn = 1 if "model.mtp.embed_tokens.weight" in self.model_tensors else 0 | ||
| if self.n_nextn: | ||
| self.block_count += self.n_nextn | ||
| self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count) | ||
|
|
||
| self.layer_types = hparams["layer_types"] | ||
| if len(self.layer_types) < hparams["num_hidden_layers"]: | ||
| raise ValueError("layer_types is shorter than num_hidden_layers") | ||
|
|
||
| if hparams.get("use_dsa", True) is not True: | ||
| raise ValueError("dots3-note conversion requires use_dsa=true") | ||
| if hparams.get("normalization", "RMSNorm") != "RMSNorm" or hparams.get("final_norm", "RMSNorm") != "RMSNorm": | ||
| raise ValueError("dots3-note conversion only supports RMSNorm") | ||
| if hparams.get("k_rope_only_layernorm", True) is not True: | ||
| raise ValueError("dots3-note conversion requires k_rope_only_layernorm=true") | ||
| if hparams.get("topk_method", "noaux_tc") != "noaux_tc" or hparams.get("scoring_func") != "sigmoid": | ||
| raise ValueError("dots3-note conversion only supports noaux_tc/sigmoid expert gating") | ||
| if hparams.get("n_group", 1) != 1 or hparams.get("topk_group", 1) != 1: | ||
| raise ValueError("dots3-note conversion does not support grouped expert routing") | ||
| if hparams.get("use_dynamic_rsf", False) or hparams.get("moe_gating_fp32", False): | ||
| raise ValueError("dots3-note conversion does not support use_dynamic_rsf/moe_gating_fp32") | ||
| for key in ("attention_gate_type", "swa_attention_gate_type"): | ||
| if hparams.get(key, "headwise") != "headwise": | ||
| raise ValueError(f"dots3-note conversion only supports headwise attention gate, got {key}={hparams.get(key)!r}") | ||
| if hparams["swa_qk_nope_head_dim"] + hparams["swa_qk_rope_head_dim"] != hparams.get("swa_head_dim", 256): | ||
| raise ValueError("swa_head_dim must equal swa_qk_nope_head_dim + swa_qk_rope_head_dim") | ||
| if hparams["swa_qk_rope_head_dim"] != hparams["qk_rope_head_dim"]: | ||
| # both layer kinds share a single rope_dimension_count | ||
| raise ValueError("swa_qk_rope_head_dim must match qk_rope_head_dim") | ||
|
|
||
| self.apply_lora_rescale = hparams.get("apply_mla_qkv_lora_rescale", False) | ||
|
|
||
| def _is_swa_layer(self, bid: int) -> bool: | ||
| if bid >= self.hparams["num_hidden_layers"]: | ||
| # note: the NextN/MTP block uses the sliding-attention MLA | ||
| return True | ||
| return self.layer_types[bid] == "sliding_attention" | ||
|
|
||
| def set_vocab(self): | ||
| from transformers import AutoTokenizer | ||
| tokenizer = AutoTokenizer.from_pretrained(self.dir_model) | ||
| special_vocab = gguf.SpecialVocab(self.dir_model, load_merges=True) | ||
| tokens, toktypes, tokpre = self.get_vocab_base() | ||
| self.gguf_writer.add_tokenizer_model("gpt2") | ||
| self.gguf_writer.add_tokenizer_pre(tokpre) | ||
| self.gguf_writer.add_token_list(tokens) | ||
| self.gguf_writer.add_token_types(toktypes) | ||
| special_vocab._set_special_token("eot", tokenizer.get_added_vocab()["<|endofassistant|>"]) # ty: ignore[unresolved-attribute] | ||
| special_vocab.add_to_gguf(self.gguf_writer) | ||
|
|
||
| @classmethod | ||
| def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None: | ||
| if (titem := super().filter_tensors(item)) is None: | ||
| return None | ||
| name, gen = titem | ||
| if name.startswith(("vision_encoder.", "audio_encoder.")): | ||
| return None | ||
|
|
||
| assert cls._n_main_layers is not None | ||
| is_mtp = name.startswith("model.mtp.") or \ | ||
| ((m := re.match(r"model\.layers\.(\d+)\.", name)) is not None and int(m.group(1)) >= cls._n_main_layers) | ||
|
|
||
| # --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 ( | ||
| "model.embed_tokens.weight", "model.norm.weight", "lm_head.weight", | ||
| ): | ||
| return None | ||
|
|
||
| return name, gen | ||
|
|
||
| def set_gguf_parameters(self): | ||
| hparams = self.hparams | ||
|
|
||
| # head_count is a per-layer array because the two layer kinds have different head counts | ||
| n_layer = hparams["num_hidden_layers"] | ||
| hparams["num_attention_heads"] = [ | ||
| hparams["swa_num_attention_heads"] if self._is_swa_layer(il) else hparams["num_attention_heads"] | ||
| for il in range(self.block_count) | ||
| ] | ||
|
|
||
| # prevent the base class from emitting key/value_length from the unused head_dim | ||
| hparams.pop("head_dim", None) | ||
|
|
||
| super().set_gguf_parameters() | ||
|
|
||
| # MLA geometry of the sliding-window layers (rope.freq_base_swa is emitted by the base class) | ||
| swa_kv_lora_rank = hparams["swa_kv_lora_rank"] | ||
| self.gguf_writer.add_sliding_window(hparams["sliding_window_size"]) | ||
| self.gguf_writer.add_sliding_window_pattern([self._is_swa_layer(il) for il in range(n_layer)]) | ||
| self.gguf_writer.add_kv_lora_rank_swa(swa_kv_lora_rank) | ||
| self.gguf_writer.add_key_length_swa(swa_kv_lora_rank + hparams["swa_qk_rope_head_dim"]) | ||
| self.gguf_writer.add_value_length_swa(swa_kv_lora_rank) | ||
| self.gguf_writer.add_key_length_mla_swa(hparams["swa_qk_nope_head_dim"] + hparams["swa_qk_rope_head_dim"]) | ||
| self.gguf_writer.add_value_length_mla_swa(hparams["swa_v_head_dim"]) | ||
| if hparams["swa_q_lora_rank"] != hparams["q_lora_rank"]: | ||
| raise ValueError("dots3-note conversion assumes a shared q_lora_rank for both layer kinds") | ||
|
|
||
| if self.n_nextn: | ||
| self.gguf_writer.add_nextn_predict_layers(self.n_nextn) | ||
|
|
||
| # DSA indexer (full-attention layers only) | ||
| self.gguf_writer.add_indexer_head_count(hparams["index_n_heads"]) | ||
| self.gguf_writer.add_indexer_key_length(hparams["index_head_dim"]) | ||
| self.gguf_writer.add_indexer_top_k(hparams["index_topk"]) | ||
| self.gguf_writer.add_indexer_types([not self._is_swa_layer(il) for il in range(n_layer)]) | ||
|
|
||
| def prepare_metadata(self, vocab_only: bool): | ||
| from_dir = self.fname_out.is_dir() | ||
| super().prepare_metadata(vocab_only=vocab_only) | ||
|
|
||
| if not self.mtp_only or not from_dir: | ||
| return | ||
|
|
||
| output_type: str = self.ftype.name.partition("_")[2] | ||
| fname_default: str = gguf.naming_convention( | ||
| self.metadata.name, self.metadata.basename, self.metadata.finetune, | ||
| self.metadata.version, size_label=None, output_type=output_type, model_type=None) | ||
| self.fname_out = self.fname_out.parent / f"mtp-{fname_default}.gguf" | ||
|
|
||
| def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]: | ||
| # move the MTP token embedding into the NextN block so the standard nextn mapping picks it up | ||
| if name == "model.mtp.embed_tokens.weight": | ||
| name = f"model.layers.{self.hparams['num_hidden_layers']}.embed_tokens.weight" | ||
| bid = self.hparams["num_hidden_layers"] | ||
|
|
||
| # fold the activation rescale sqrt(n_embd/lora_rank) into the preceding RMSNorm weight | ||
| # this also covers the indexer wq_b, which reads the same rescaled q_lora activation | ||
| if self.apply_lora_rescale and bid is not None: | ||
| if name.endswith("q_a_layernorm.weight"): | ||
| data_torch = data_torch * math.sqrt(self.hparams["hidden_size"] / self.hparams["q_lora_rank"]) | ||
| elif name.endswith("kv_a_layernorm.weight"): | ||
| rank = self.hparams["swa_kv_lora_rank"] if self._is_swa_layer(bid) else self.hparams["kv_lora_rank"] | ||
| data_torch = data_torch * math.sqrt(self.hparams["hidden_size"] / rank) | ||
|
|
||
| # MLA absorption: split kv_b_proj into k_b (transposed) and v_b, per-layer-kind geometry | ||
| if name.endswith("kv_b_proj.weight"): | ||
| assert bid is not None | ||
| if self._is_swa_layer(bid): | ||
| n_head = self.hparams["swa_num_attention_heads"] | ||
| qk_nope_head_dim = self.hparams["swa_qk_nope_head_dim"] | ||
| v_head_dim = self.hparams["swa_v_head_dim"] | ||
| else: | ||
| n_head = self.hparams["num_attention_heads"] | ||
| qk_nope_head_dim = self.hparams["qk_nope_head_dim"] | ||
| v_head_dim = self.hparams["v_head_dim"] | ||
| if isinstance(n_head, list): # set_gguf_parameters turns this into a per-layer array | ||
| n_head = n_head[bid] | ||
|
|
||
| assert data_torch.shape[0] == n_head * (qk_nope_head_dim + v_head_dim) | ||
|
|
||
| kv_b = data_torch.view(n_head, qk_nope_head_dim + v_head_dim, data_torch.shape[-1]) | ||
| k_b, v_b = kv_b.split([qk_nope_head_dim, v_head_dim], dim=1) | ||
| k_b = k_b.transpose(1, 2) | ||
|
|
||
| yield from ModelBase.modify_tensors(self, k_b, name.replace("kv_b_proj", "k_b_proj"), bid) | ||
| yield from ModelBase.modify_tensors(self, v_b, name.replace("kv_b_proj", "v_b_proj"), bid) | ||
| return | ||
|
|
||
| yield from super().modify_tensors(data_torch, name, bid) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.