Skip to content
Merged
2 changes: 1 addition & 1 deletion common/speculative.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1291,7 +1291,7 @@ struct common_speculative_impl_draft_mtp : public common_speculative_impl {
GGML_ASSERT(ctx_tgt && ctx_dft && "MTP requires ctx_tgt and ctx_dft to be set");

n_embd = llama_model_n_embd_out(llama_get_model(ctx_dft));
GGML_ASSERT(n_embd == llama_model_n_embd(llama_get_model(ctx_tgt)) &&
GGML_ASSERT(n_embd == llama_model_n_embd_out(llama_get_model(ctx_tgt)) &&
"MTP input row width must match the target h_nextn width");
n_mtp_layers = std::max(1, (int) llama_model_n_layer_nextn(llama_get_model(ctx_dft)));

Expand Down
116 changes: 110 additions & 6 deletions conversion/deepseek.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,10 @@ def set_gguf_parameters(self):
@ModelBase.register("DeepseekV4ForCausalLM")
class DeepseekV4Model(TextModel):
model_arch = gguf.MODEL_ARCH.DEEPSEEK4
supports_mtp_export = True
_skipped_mtp_tensors = 0
_dsv4_main_layers: int | None = None
_dsv4_nextn_layers: int = 0

def __init__(self, *args, **kwargs):
type(self)._skipped_mtp_tensors = 0
Expand All @@ -487,6 +490,8 @@ def __init__(self, *args, **kwargs):
self.hparams.setdefault(key, value)

self.block_count = self.hparams["num_hidden_layers"]
if self.mtp_only:
self.block_count += self.hparams.get("num_nextn_predict_layers", 0)
self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count)

self._dsv4_fp8_dequantized: set[str] = set()
Expand All @@ -504,13 +509,63 @@ def __init__(self, *args, **kwargs):
with open(template_path, "r", encoding="utf-8") as f:
self.gguf_writer.add_chat_template(f.read())

def index_tensors(self, remote_hf_model_id: str | None = None) -> dict[str, Callable[[], Tensor]]:
type(self)._dsv4_main_layers = self.hparams["num_hidden_layers"]
type(self)._dsv4_nextn_layers = self.hparams.get("num_nextn_predict_layers", 0)
return super().index_tensors(remote_hf_model_id=remote_hf_model_id)

@classmethod
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
name, _ = item
name, gen = item
if name.startswith("mtp."):
cls._skipped_mtp_tensors += 1
return None
return super().filter_tensors(item)
if not cls.mtp_only:
cls._skipped_mtp_tensors += 1
return None

assert cls._dsv4_main_layers is not None
parts = name.split(".", 2)
if len(parts) < 3 or not parts[1].isdecimal():
raise ValueError(f"Unexpected DeepSeek-V4 MTP tensor {name!r}")

mtp_idx = int(parts[1])
if mtp_idx >= cls._dsv4_nextn_layers:
raise ValueError(f"Unexpected DeepSeek-V4 MTP layer {mtp_idx}")

bid = cls._dsv4_main_layers + mtp_idx
suffix = parts[2]
root_hc_head = {
"hc_head_fn",
"hc_head_base",
"hc_head_scale",
}
if suffix in root_hc_head:
name = suffix
elif suffix in (
"e_proj.weight", "e_proj.scale",
"h_proj.weight", "h_proj.scale",
):
name = f"layers.{bid}.nextn.{suffix}"
elif suffix == "enorm.weight":
name = f"layers.{bid}.nextn.enorm.weight"
elif suffix == "hnorm.weight":
name = f"layers.{bid}.nextn.hnorm.weight"
elif suffix == "norm.weight":
name = f"layers.{bid}.nextn.shared_head_norm.weight"
else:
name = f"layers.{bid}.{suffix}"
return name, gen

if cls.mtp_only:
keep = name in (
"embed.weight",
"norm.weight",
"head.weight",
"head.scale",
)
if not keep:
return None

return super().filter_tensors((name, gen))

@staticmethod
def _float8_dtypes() -> tuple[torch.dtype, ...]:
Expand Down Expand Up @@ -565,6 +620,9 @@ def set_gguf_parameters(self):
self.gguf_writer.add_hyper_connection_sinkhorn_iterations(hparams["hc_sinkhorn_iters"])
self.gguf_writer.add_hyper_connection_epsilon(hparams["hc_eps"])
self.gguf_writer.add_hash_layer_count(hparams["num_hash_layers"])
self.gguf_writer.add_embedding_length_out(hparams["hidden_size"] * hparams["hc_mult"])
if self.mtp_only and (num_nextn_predict_layers := hparams.get("num_nextn_predict_layers", 0)) > 0:
self.gguf_writer.add_nextn_predict_layers(num_nextn_predict_layers)

def dequant_model(self):
fp8_dtypes = self._float8_dtypes()
Expand Down Expand Up @@ -669,12 +727,37 @@ def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]:
if self._dsv4_mxfp4_generated:
return ()

consumed: list[str] = self._write_hash_routing_tensors()
consumed: list[str] = []
main_layers = self.hparams["num_hidden_layers"]
if not self.mtp_only:
consumed.extend(self._write_hash_routing_tensors())
elif self.hparams["num_hash_layers"] > 0:
for bid in range(self.hparams["num_hash_layers"]):
name = f"layers.{bid}.ffn.gate.tid2eid"
if name in self.model_tensors:
consumed.extend(self._write_hash_routing_tensors())
break

for bid in range(self.block_count):
if self.mtp_only and bid < main_layers:
continue
consumed.extend(self._write_mxfp4_expert_tensor(bid, "w1", gguf.MODEL_TENSOR.FFN_GATE_EXP))
consumed.extend(self._write_mxfp4_expert_tensor(bid, "w2", gguf.MODEL_TENSOR.FFN_DOWN_EXP))
consumed.extend(self._write_mxfp4_expert_tensor(bid, "w3", gguf.MODEL_TENSOR.FFN_UP_EXP))

for bid in range(main_layers, self.block_count):
e_name = f"layers.{bid}.nextn.e_proj.weight"
h_name = f"layers.{bid}.nextn.h_proj.weight"
if e_name not in self.model_tensors and h_name not in self.model_tensors:
continue
if e_name not in self.model_tensors or h_name not in self.model_tensors:
raise KeyError(f"Missing DeepSeek-V4 MTP e/h projection pair for block {bid}")

e_proj = LazyTorchTensor.to_eager(self.model_tensors[e_name]())
h_proj = LazyTorchTensor.to_eager(self.model_tensors[h_name]())
yield (f"layers.{bid}.nextn.eh_proj.weight", torch.cat((e_proj, h_proj), dim=1).contiguous())
consumed.extend((e_name, h_name))

for name in consumed:
del self.model_tensors[name]

Expand Down Expand Up @@ -737,6 +820,12 @@ def _map_dsv4_tensor_name(self, name: str, bid: int | None) -> tuple[gguf.MODEL_
"ffn.shared_experts.w1.weight": (gguf.MODEL_TENSOR.FFN_GATE_SHEXP, ".weight"),
"ffn.shared_experts.w2.weight": (gguf.MODEL_TENSOR.FFN_DOWN_SHEXP, ".weight"),
"ffn.shared_experts.w3.weight": (gguf.MODEL_TENSOR.FFN_UP_SHEXP, ".weight"),
"nextn.eh_proj.weight": (gguf.MODEL_TENSOR.NEXTN_EH_PROJ, ".weight"),
"nextn.enorm.weight": (gguf.MODEL_TENSOR.NEXTN_ENORM, ".weight"),
"nextn.hnorm.weight": (gguf.MODEL_TENSOR.NEXTN_HNORM, ".weight"),
"nextn.shared_head_norm.weight": (gguf.MODEL_TENSOR.NEXTN_SHARED_HEAD_NORM, ".weight"),
"nextn.embed_tokens.weight": (gguf.MODEL_TENSOR.NEXTN_EMBED_TOKENS, ".weight"),
"nextn.shared_head_head.weight": (gguf.MODEL_TENSOR.NEXTN_SHARED_HEAD_HEAD, ".weight"),
}

tensor_name = match.group(2)
Expand All @@ -759,17 +848,32 @@ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iter
return [(self._format_dsv4_tensor_name(tensor_key, bid, suffix), data_torch)]

def tensor_force_quant(self, name: str, new_name: str, bid: int | None, n_dims: int) -> gguf.GGMLQuantizationType | bool:
del new_name, bid # unused
del bid # unused

if name in self._dsv4_fp8_dequantized and n_dims >= 2:
return gguf.GGMLQuantizationType.Q8_0
if new_name.endswith(".nextn.eh_proj.weight"):
return gguf.GGMLQuantizationType.Q8_0
if name in self._dsv4_f32_tensors:
return gguf.GGMLQuantizationType.F32
if name in self._dsv4_bf16_tensors and n_dims >= 2:
return gguf.GGMLQuantizationType.BF16

return False

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 prepare_tensors(self):
super().prepare_tensors()
self._is_mxfp4 = True
Expand Down
6 changes: 6 additions & 0 deletions gguf-py/gguf/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -3331,6 +3331,12 @@ class MODEL_TENSOR(IntEnum):
MODEL_TENSOR.FFN_GATE_SHEXP,
MODEL_TENSOR.FFN_DOWN_SHEXP,
MODEL_TENSOR.FFN_UP_SHEXP,
MODEL_TENSOR.NEXTN_EH_PROJ,
MODEL_TENSOR.NEXTN_EMBED_TOKENS,
MODEL_TENSOR.NEXTN_ENORM,
MODEL_TENSOR.NEXTN_HNORM,
MODEL_TENSOR.NEXTN_SHARED_HEAD_HEAD,
MODEL_TENSOR.NEXTN_SHARED_HEAD_NORM,
],
MODEL_ARCH.ERNIE4_5_MOE: [
MODEL_TENSOR.TOKEN_EMBD,
Expand Down
2 changes: 2 additions & 0 deletions src/llama-arch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,7 @@ bool llm_arch_is_hybrid(const llm_arch & arch) {
case LLM_ARCH_KIMI_LINEAR:
case LLM_ARCH_QWEN35:
case LLM_ARCH_QWEN35MOE:
case LLM_ARCH_DEEPSEEK4:
return true;
default:
return false;
Expand All @@ -990,6 +991,7 @@ bool llm_arch_supports_rs_rollback(const llm_arch & arch) {
switch (arch) {
case LLM_ARCH_QWEN35:
case LLM_ARCH_QWEN35MOE:
case LLM_ARCH_DEEPSEEK4:
return true;
default:
return false;
Expand Down
24 changes: 14 additions & 10 deletions src/llama-context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,9 @@ llama_context::llama_context(
cparams.no_perf = params.no_perf;
cparams.warmup = false;

cparams.embeddings_layer_inp.resize(hparams.n_layer(), false);
embd_layer_inp.resize(hparams.n_layer());
// +1: id n_layer() taps the output of the last layer ("input" of the head)
cparams.embeddings_layer_inp.resize(hparams.n_layer() + 1, false);
embd_layer_inp.resize(hparams.n_layer() + 1);

cparams.ctx_type = params.ctx_type;
cparams.pooling_type = params.pooling_type;
Expand Down Expand Up @@ -1164,7 +1165,7 @@ void llama_context::set_embeddings_nextn(bool value, bool masked) {
void llama_context::set_embeddings_layer_inp(uint32_t lid, bool enable) {
LLAMA_LOG_DEBUG("%s: lid = %d, enable = %d\n", __func__, lid, enable);

GGML_ASSERT(lid < model.hparams.n_layer());
GGML_ASSERT(lid <= model.hparams.n_layer());

cparams.embeddings_layer_inp[lid] = enable;

Expand Down Expand Up @@ -1716,7 +1717,8 @@ int llama_context::decode(const llama_batch & batch_inp) {
const auto & hparams = model.hparams;

const int64_t n_vocab = vocab.n_tokens();
const int64_t n_embd = hparams.n_embd_inp();
const bool mtp_embd = cparams.ctx_type == LLAMA_CONTEXT_TYPE_MTP && batch_inp.embd;
const int64_t n_embd = mtp_embd ? hparams.n_embd_out() : hparams.n_embd_inp();

// when computing embeddings, all tokens are output
const bool output_all = cparams.embeddings;
Expand Down Expand Up @@ -2275,8 +2277,9 @@ void llama_context::extract_layer_inputs(const llm_graph_result * res, size_t to
}

void llama_context::output_reorder() {
const uint64_t n_vocab = model.vocab.n_tokens();
const uint64_t n_embd = model.hparams.n_embd;
const uint64_t n_vocab = model.vocab.n_tokens();
const uint64_t n_embd = model.hparams.n_embd;
const uint64_t n_embd_out = model.hparams.n_embd_out();

for (size_t s = 0; s < output_swaps.size(); ++s) {
const uint64_t i0 = output_swaps[s].i0;
Expand All @@ -2289,14 +2292,14 @@ void llama_context::output_reorder() {
}

if (embd.size > 0) {
for (uint64_t k = 0; k < n_embd; k++) {
std::swap(embd.data[i0*n_embd + k], embd.data[i1*n_embd + k]);
for (uint64_t k = 0; k < n_embd_out; k++) {
std::swap(embd.data[i0*n_embd_out + k], embd.data[i1*n_embd_out + k]);
}
}

if (embd_nextn.size > 0) {
for (uint64_t k = 0; k < n_embd; k++) {
std::swap(embd_nextn.data[i0*n_embd + k], embd_nextn.data[i1*n_embd + k]);
for (uint64_t k = 0; k < n_embd_out; k++) {
std::swap(embd_nextn.data[i0*n_embd_out + k], embd_nextn.data[i1*n_embd_out + k]);
}
}

Expand Down Expand Up @@ -2351,6 +2354,7 @@ uint32_t llama_context::graph_max_nodes(uint32_t n_tokens) const {
model.arch == LLM_ARCH_QWEN35 ||
model.arch == LLM_ARCH_QWEN35MOE ||
model.arch == LLM_ARCH_DEEPSEEK4 ||
(model.arch == LLM_ARCH_DFLASH && model.hparams.dsv4_hc_mult > 0) ||
model.arch == LLM_ARCH_NANBEIGE ||
model.arch == LLM_ARCH_MINIMAX_M3) {
return std::max<uint32_t>(n_tokens * 40, 32u * model.n_tensors());
Expand Down
Loading
Loading